Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert Swift Array<Result<X,Error>> in to Result<Array<X>, Error>

Tags:

generics

swift

I have an array of Swift Result, like this:

let tuple: [Result<Term, TermError>] = /* code here */

I want to turn this inside out, pulling the results out to give a single result, with the array pushed inside it.

let tuple2: Result<[Term], TermError> = /* How? */

tuple2 should be .failure if any of tuple are .failure. Otherwise it is .success([tuple-elements-in-here]).

I think I can come up with something that will make this work, but I feel like there ought to be a fairly clean way to achieve this?

like image 351
Benjohn Avatar asked Jan 26 '23 00:01

Benjohn


2 Answers

You are actually just trying to recreate the sequence function for Haskell Monads in Swift, for the Result monad. We can implement it exactly the same way Haskell implemented it.

sequence       :: Monad m => [m a] -> m [a] 
sequence       =  foldr mcons (return [])
                    where mcons p q = p >>= \x -> q >>= \y -> return (x:y)

In Swift, this would look like:

func sequence<T, E: Error>(_ arrayOfResults: [Result<T, E>]) -> Result<[T], E> {
    return arrayOfResults.reduce(.success([])) { (p, q) in
        return p.flatMap { x in return q.map { y in return x + [y] } }
    }
}

Usage:

let tuple2 = sequence(tuple)
like image 129
Sweeper Avatar answered Jan 28 '23 14:01

Sweeper


You can define an extension on Array that converts an Array<Result<Value,Error>> toResult,Error>` like below.

extension Array {
    func flatMapResult<Value, Error>() -> Result<Array<Value>,Error> where Element == Result<Value,Error> {
        let valuesAndErrors = self.map { element -> (value: Value?, error: Error?) in
            switch element {
            case .failure(let error):
                return (nil, error)
            case .success(let value):
                return (value, nil)
            }
        }
        if let firstElementWithError = valuesAndErrors.first(where: {$0.error != nil}), let firstError = firstElementWithError.error {
            return .failure(firstError)
        } else {
            let values = valuesAndErrors.compactMap { $0.value }
            return .success(values)
        }
    }
}

Code sample for usage:

enum MyError: Error {
    case err
}

let arrayOfResults: Array<Result<Int,MyError>> = [.success(8), .success(2), .success(3)] // success([8, 2, 3])
let arrayOfResultsWithFailure = arrayOfResults + [.failure(.err)] // failure(__lldb_expr_2.MyError.err)

arrayOfResults.flatMapResult()
arrayOfResultsWithFailure.flatMapResult()
like image 35
Dávid Pásztor Avatar answered Jan 28 '23 15:01

Dávid Pásztor