Best way to convert an Array of Belt.Result.t into a Belt.Result.t of Array

I have an array like array<Belt.Result.t<int, string>> and I want to convert it to Belt.Result.t<<array<int>, string>

to achieve this I wrote this code

let x : array<Belt.Result.t<int, string>> = [Ok(1), Ok(2), Ok(3)]
if x -> Belt.Array.every(item => 
  switch item {
  | Ok(_) => true
  | Error(_) => false
  }
) {
Ok(x -> Belt.Array.map(item => 
    switch item {
    | Ok(val) => val
    | _ => failwith("should not come here because of every")
    }))
} else {
  Error("Array contains errors")
} 

But I don’t like that I have to iterate the array twice. Any idea of how to do this better?

1 Like

This should work:

let input: array<result<int, string>> = [Ok(1), Ok(2), Ok(3)]

let rec loop = (oks, i) =>
  switch input->Belt.Array.get(i) {
  | Some(Ok(x)) =>
    oks->Belt.Array.push(x)
    loop(oks, i + 1)
  | Some(Error(_) as err) => err
  | None => Ok(oks)
  }

let output: result<array<int>, string> = loop([], 0)
3 Likes

Question: won’t array.push create a new array? given that everything is immutable?

Behind the scenes it’s still a JS array, so it’s not actually immutable. If you see unit in the return type you’ll know it most likely mutates something

You can do the same thing using concat and an inner loop, I typically do something very similar in my codebase

let sequence = results => {
  let rec loop = (i, oks) =>
    switch results[i] {
    | None => Ok(oks)
    | Some(Ok(x)) => loop(i + 1, Js.Array.concat(oks, [x]))
    | Some(Error(_) as e) => e
    }
  loop(0, [])
}
2 Likes

By the way, it is not the exact signature that you asked for, but I have found a function like this to be very useful in cases where you need to keep track of all the errors and not only the first one.

let combineErrors: 
  array<result<'ok, 'error>> => result<array<'ok>, array<'error>>

Here’s an example (playground):

let combineErrors = l => {
  let oks = []
  let errors = []

  Js.Array2.forEach(l, v => {
    switch v {
    | Ok(v) => Js.Array2.push(oks, v)->ignore
    | Error(e) => Js.Array2.push(errors, e)->ignore
    }
  })

  switch errors {
  | [] => Ok(oks)
  | _ => Error(errors)
  }
}

let a = [Ok(1), Error("oops"), Ok(3), Error("darn")]

switch combineErrors(a) {
| Ok(_) => ()
| Error(all_errors) => Js.log2("Errors: ", all_errors)
}

Edit: if you’re interested, I “stole” this signature from this OCaml function.

Quick question, as this has come up in other contexts: how do you end up with a array of result?
Generally, you use result when you care about the error, but pretty much any instance when you convert an array of result to a result of array you’re throwing away any info of what the error was.

So I would question the use of result to begin with: in addition to all that ceremony of carrying around all the error information, you end up throwing it away, which seems puzzling.

3 Likes

Like in example given by Ryan I could collect all the errors in array or string.

In my experience, this is common when decoding remote data, e.g. a list of posts where each item could potentially fail.

Depending on the importance of the data I might then either just drop the failed ones or store the errors in an array similar to Ryan’s example.

1 Like

Should be one of the fastest versions:

1 Like

Yet another option!

let arrayOfResultsToResultOfArray = (results: array<result<'ok, 'err>>): result<
  array<'ok>,
  'err,
> =>
  results->Belt.Array.reduce(Ok([]), (overallResult, nextResult) =>
    switch (overallResult, nextResult) {
    | (Error(_) as err, _) // short-circuit, we already failed.
    | (_, Error(_) as err) => err
    | (Ok(arr), Ok(next)) => Ok(arr->Belt.Array.concat([next]))
    }
  )
1 Like

Usually from using Promise.all, where I’m typically unwrapping it immediately after

let results = await Promise.all(doSomething)
sequence(results)

Comes up more in node code with disk I/O than UI stuff

2 Likes

Others have given some examples, but another I have found helpful is parsing config files and other user input. Gather up all the errors the user made and dump them all to a log or however you want to handle them.

1 Like

These pull requests to rescript-core are directly related, and may be of interest:

As for throwing data away, Result.fromArrayMap in my PR is somewhat lazy. It evaluates each input in turn for errors and stops calculating when an error is found.