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.

In my case, the frontend is performing a mass operation, so I iterate through an array of inputs, perform a POST, gather the promises, and then need to separate the successful ones from the failures in order to show the user:

    inputs->Belt.Array.map(doPostOp)->Js.Promise.all->Js.Promise.then_(results => ...)

Perhaps the following is a more general function that could be added to the library if it could be helpful? One thing I like about this variant is that it strips the Ok/Error constructors and returns the underlying array<'a> and array<'b>, which is something I find myself doing all the time after splitting the Results.

@ocaml.doc(
  "Splits an array of Result values into an array of the underlying Ok values and one of the Error values."
)
let partition = (results: array<Belt.Result.t<'a, 'b>>): (array<'a>, array<'b>) => {
  Belt.Array.reduce(results, ([], []), ((oks, errs), x) =>
    switch x {
      | Belt.Result.Ok(ok) => Js.Array2.push(oks, ok)->ignore; (oks, errs)
      | Belt.Result.Error(err) => Js.Array2.push(errs, err)->ignore; (oks, errs)
    })
}

I think using the JS push function is more efficient than using Belt.Array.concat because the latter creates a new array and copies the original array every time, so partition would become an O(n^2) operation. But is there a better way to do it?