Skip to content

Commit

Permalink
Add Option.traverseAsync and Option.sequenceAsync (#298)
Browse files Browse the repository at this point in the history
* Implement Option.traverseAsync and Option.sequenceAsync and add tests

* Add examples to documentation

* Fix typo in doc

* One more doc fix

* One more doc fix

* One more doc fix
  • Loading branch information
tw0po1nt authored Jan 6, 2025
1 parent 1d2d310 commit c48b7a1
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 0 deletions.
27 changes: 27 additions & 0 deletions gitbook/option/sequenceAsync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Option.sequenceAsync

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```fsharp
Async<'a> option -> Async<'a option>
```

Note that `sequence` is the same as `traverse id`. See also [Option.traverseAsync](traverseAsync.md).

See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).

## Examples

### Example 1

```fsharp
let a1 : Async<int option> =
Option.sequenceAsync (Some (Async.singleton 42))
// async { return Some 42 }
let a2 : Async<int option> =
Option.sequenceAsync None
// async { return None }
```
45 changes: 45 additions & 0 deletions gitbook/option/traverseAsync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Option.traverseAsync

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```fsharp
('a -> Async<'b>) -> 'a option -> Async<'b option>
```

Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceAsync](sequenceAsync.md).

See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).

## Examples

### Example 1

Let's assume we have a type `Customer`:

```fsharp
type Customer = {
Id : int
Email : string
}
```

And we have a function called `getCustomerByEmail` that retrieves a `Customer` by email address asynchronously from some external source -- a database, a web service, etc:

```fsharp
// string -> Async<Customer>
let getCustomerByEmail email : Async<Customer> = async {
return { Id = 1; Email = "[email protected]" } // return a constant for simplicity
}
```

If we have a value of type `string option` and want to call the `getCustomerByEmail` function, we can achieve it using the `traverseAsync` function as below:

```fsharp
Some "[email protected]" |> Option.traverseAsync getCustomerByEmail
// async { return Some { Id = 1; Email = "[email protected]" } }
None |> Option.traverseAsync getCustomerByEmail
// async { return None }
```
22 changes: 22 additions & 0 deletions src/FsToolkit.ErrorHandling/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,28 @@ module Option =

opt

/// Converts a Option<Async<_>> to an Async<Option<_>>
let inline sequenceAsync (optAsync: Option<Async<'T>>) : Async<Option<'T>> =
async {
match optAsync with
| Some asnc ->
let! x = asnc
return Some x
| None -> return None
}

/// <summary>
/// Maps an Async function over an Option, returning an Async Option.
/// </summary>
/// <param name="f">The function to map over the Option.</param>
/// <param name="opt">The Option to map over.</param>
/// <returns>An Async Option with the mapped value.</returns>
let inline traverseAsync
([<InlineIfLambda>] f: 'T -> Async<'T>)
(opt: Option<'T>)
: Async<Option<'T>> =
sequenceAsync ((map f) opt)

/// <summary>
/// Creates an option from a boolean value and a value of type 'a.
/// If the boolean value is true, returns <c>Some</c> value.
Expand Down
62 changes: 62 additions & 0 deletions tests/FsToolkit.ErrorHandling.Tests/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,66 @@ let teeIfTests =
Expect.equal foo "foo" ""
]

let sequenceAsyncTests =
testList "Option.sequenceAsync Tests" [
testCaseAsync "sequenceAsync returns the async value if Some"
<| async {
let optAsync =
async { return "foo" }
|> Some

let! value =
optAsync
|> Option.sequenceAsync

Expect.equal value (Some "foo") ""
}

testCaseAsync "sequenceAsync returns None if None"
<| async {
let optAsync = None

let! value =
optAsync
|> Option.sequenceAsync

Expect.equal value None ""
}
]

let traverseAsyncTests =
testList "Option.traverseAsync Tests" [
testCaseAsync "traverseAsync returns the async value if Some"
<| async {
let optAsync = Some "foo"

let optFunc =
id
>> Async.singleton

let! value =
(optFunc, optAsync)
||> Option.traverseAsync

Expect.equal value (Some "foo") ""
}

testCaseAsync "traverseAsync returns None if None"
<| async {
let optAsync = None

let optFunc =
id
>> Async.singleton

let! value =
(optFunc, optAsync)
||> Option.traverseAsync

Expect.equal value None ""
}
]

let traverseResultTests =
testList "Option.traverseResult Tests" [
testCase "traverseResult with Some of valid data"
Expand Down Expand Up @@ -366,6 +426,8 @@ let optionOperatorsTests =

let allTests =
testList "Option Tests" [
sequenceAsyncTests
traverseAsyncTests
traverseResultTests
tryParseTests
tryGetValueTests
Expand Down

0 comments on commit c48b7a1

Please sign in to comment.