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]>