Library for handling Async operations that might fail. Mainly gives you the type AsyncResult and helper functions for creating and working with this type.
We wanted to practice Railway Oriented Programming in the Async world which is tricky in F# without helper functions.
A common pattern in the real world is that a service needs to parse input, call different services, do DB-lookups, do conversions, store and return something. These steps can usually fail in different ways. E.g. Invalid input, external request failed, etc. In F# we have the Result type for things that might fail and Async for things that are asynchronous but we usually need the combination of these two, AsyncResult, which is not a type that exists in F#. This makes it tedious to work with and requires a lot of plumbing (see example below).
This package is inspired by Scott Wlaschin's book - Domain Modeling Made Functional and the associated code repository
The AsyncResult type is an alias
type AsyncResult<'success, 'error> = Async<Result<'success, 'error>>module AsyncResultExample
open Insurello.AsyncExtra
// SETUP EXAMPLE-FUNCTIONS
let fetchPersonIds: unit -> Async<List<int>> =
fun () ->
async {
do! Async.Sleep(3000)
return [ 31; 27; 92 ]
}
let personName: string -> Async<Result<string, string>> =
fun id ->
async {
do! Async.Sleep(3000)
return match id with
| "31" -> Ok "Alice"
| "27" -> Ok "Bob"
| "92" -> Ok "Scott"
| _ -> Error "No person found"
}
let firstId: List<int> -> Result<int, string> =
fun ids ->
ids
|> List.tryHead
|> function
| Some id -> Ok id
| None -> Error "Empty list of Ids"
// SETUP EXAMPLE-FUNCTIONS DONE
// Without AsyncExtra
let firstPersonsName: unit -> Async<Result<string, string>> =
fun () ->
async {
let! personIds = fetchPersonIds()
let id =
personIds
|> firstId
|> Result.map string
let! name = match id with
| Ok id -> personName id
| Error error -> async.Return(Error error)
return name
}
// With AsyncExtra
let firstPersonsNameWithAsyncResult: unit -> AsyncResult<string, string> =
fun () ->
fetchPersonIds()
|> Async.map firstId
|> AsyncResult.map string
|> AsyncResult.bind personName
The functions need to be prefixed with AsyncResult..
singleton : 'a -> AsyncResult<'a, 'err>The easiest way to create a new AsyncResult with an Ok value.
fromResult : Result<'a, 'err> -> AsyncResult<'a, 'err>Convert a Result into a AsyncResult.
fromOption : 'err -> Option<'a> -> AsyncResult<'a, 'err>Convert an Option into an AsyncResult. The first argument is the Error value that should be used if the option is None.
fromTask : (unit -> System.Threading.Tasks.Task<'a>) -> AsyncResult<'a, string>Convert a Task with a value into a AsyncResult.
fromUnitTask : (unit -> System.Threading.Tasks.Task) -> AsyncResult<unit, string>Convert a unit Task into a AsyncResult.
map : ('a -> 'b) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err>You will most likely want to change the value in the AsyncResult by running functions. This is where map comes in handy. You send in a transformation function that will transform the value and map will then take the value from your AsyncResult run the function and return a brand new AsyncResult containing the new value. Worth keeping in mind is that the transformation function will only run if there is an Ok AsyncResult. If the AsyncResult contains an Error nothing will happen.
If the transform function will return an AsyncResult you might want to use bind instead.
let increase x = x + 1
let xA = AsyncResult.singleton 2
let eA = AsyncResult.fromResult (Error 2)
AsyncResult.map increase xA // Async<Ok 3>
AsyncResult.map increase eA // Async<Error 2>mapError : ('errX -> 'errY) -> AsyncResult<'a, 'errX> -> AsyncResult<'a, 'errY>Similar to map but will instead apply the transformation function to the Error.
bind : ('a -> AsyncResult<'b, 'err>) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err>Sometimes you want to use a transform function that will return an AsyncResult but you don't want to end up with a nested AsyncResult<AsyncResult<'b, 'err>, 'err>. This is where bind shines. It will transform the value, in the same way as map, but will flatten the returning AsyncResult and will return an AsyncResult<'b, 'err>. bind is also known as andThen in Elm and chain in JavaScript.
fetchUser : int -> AsyncResult<User, string>
fetchFriends : User -> AsyncResult<string list, string>
fetchUser 1 // Async<Ok { name: "Mary"; friends: [2; 3] }>
|> AsyncResult.bind fetchFriends // Async<Ok ["Peter"; "Paul"]>
fetchUser 5 // Async<Error "Can't find a user with that id">
|> AsyncResult.bind fetchFriends // Will never run
fetchUser 4 // Async<Ok { name: "The Grinch"; friends: [] }>
|> AsyncResult.bind fetchFriends // Async<Error "Can't find any friends">bindError : ('errX -> AsyncResult<'a, 'errY>) -> AsyncResult<'a, 'errX> -> AsyncResult<'a, 'errY>Similar to bind but will instead apply the AsyncResult returning transformation function to the Error.
apply : AsyncResult<('a -> 'b), 'err> -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err>Is used to apply a AsyncResult value to a function in an AsyncResult.
When either AsyncResult is Error, apply will return a new Error instance containing the Error value. This can be used to safely combine multiple values under a given combination function. If any of the inputs result in an Error then the computation will return an Error AsyncResult.
map2 : ('a -> 'b -> 'c) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err>Sometimes you want to transform the result of two AsyncResult. This is were map2 comes into play. It requires a transformation function that is expecting two arguments. The order of the arguments are determined by the order of the two AsyncResult. The first value is applied first and the second value is applied as the second argument.
let add x y = x + y
let xA = AsyncResult.singleton 3
let yA = AsyncResult.singleton 4
AsyncResult.map2 add xA yA // Async<Ok 7>map3 : ('a -> 'b -> 'c -> 'd) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err>Solves the same problem as map2 but for three arguments.
map4 : ('a -> 'b -> 'c -> 'd -> 'e) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err> -> AsyncResult<'e, 'err>Solves the same problem as map2 but for four arguments.
map5 : ('a -> 'b -> 'c -> 'd -> 'e -> 'f) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err> -> AsyncResult<'e, 'err> -> AsyncResult<'f, 'err>Solves the same problem as map2 but for five arguments.
andMap : AsyncResult<'a, 'err> -> AsyncResult<('a -> 'b), 'err> -> AsyncResult<'b, 'err>In most cases map2-map5 should be enough but in those cases you want to apply more arguments you can use andMap. Technically, andMap is the same as apply but the order of the arguments are reversed. While apply takes the function first and then the value, andMap takes the value first and then the function. This allow you to have a similar structure to your code as you would have using mapX.
let add3 = AsyncResult.singleton (fun a b c -> a + b + c)
let xA = AsyncResult.singleton 10
let yA = AsyncResult.singleton 20
let zA = AsyncResult.singleton 30
add3 // Async<Ok (fun a b c -> a + b + c)>
|> AsyncResult.andMap xA // Async<Ok (fun b c -> 10 + b + c)>
|> AsyncResult.andMap yA // Async<Ok (fun c -> 10 + 20 + c)>
|> AsyncResult.andMap zA // Async<Ok 60>bind2 : ('a -> 'b -> AsyncResult<'c, 'err>) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err>You can use bind2 to solve the same problem as map2 if your transformation function returns an AsyncResult. In other words, their relationship is the same as map and bind.
bind3 : ('a -> 'b -> 'c -> AsyncResult<'d, 'err>) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err>Solves the same problem as bind2 but for three arguments.
bind4 : ('a -> 'b -> 'c -> 'd -> AsyncResult<'e, 'err>) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err> -> AsyncResult<'e, 'err>Solves the same problem as bind2 but for four arguments.
bind5 : ('a -> 'b -> 'c -> 'd -> 'e -> AsyncResult<'f, 'err>) -> AsyncResult<'a, 'err> -> AsyncResult<'b, 'err> -> AsyncResult<'c, 'err> -> AsyncResult<'d, 'err> -> AsyncResult<'e, 'err> -> AsyncResult<'f, 'err>Solves the same problem as bind2 but for five arguments.
sequence : List<AsyncResult<'a, 'error>> -> AsyncResult<'a list, 'error>From time to time you will find yourself having a list of AsyncResult (List<AsyncResult<'a, 'err>) but you would rather have an AsyncResult with a list of values (AsyncResult<'a list, 'error>). In those cases sequence can help you. sequence will make an early return if it reaches an Error.
fetchUser : int -> AsyncResult<User, string>
userIds // [1; 2; 3]
|> List.map fetchUser // [Async<Ok User>; Async<Ok User>; Async<Ok User>]
|> AsyncResult.sequence // Async<Ok [User; User; User]>traverse : ('a -> AsyncResult<'b, 'err>) -> List<'a> -> AsyncResult<'b list, 'err>Similar to sequence, traverse will also change the type from a list of AsyncResult to an AsyncResult with a list. The difference is that traverse takes a transformation function that takes an 'a and returns an AsyncResults<'a, 'err> and a list of 'a's instead of a list of AsyncResults<'a, 'err>. traverse will make an early return if it reaches an Error.
By sending in id as the transform function you have implemented sequence. Let's have a look how we can solve the example in the sequence description using traverse instead.
fetchUser : int -> AsyncResult<User, string>
let userIds = [1; 2; 3]
AsyncResult.traverse fetchUser userIds // Async<Ok [User; User; User]>