Skip to content

Commit c48b7a1

Browse files
authored
Add Option.traverseAsync and Option.sequenceAsync (#298)
* 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
1 parent 1d2d310 commit c48b7a1

File tree

4 files changed

+156
-0
lines changed

4 files changed

+156
-0
lines changed

gitbook/option/sequenceAsync.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## Option.sequenceAsync
2+
3+
Namespace: `FsToolkit.ErrorHandling`
4+
5+
Function Signature:
6+
7+
```fsharp
8+
Async<'a> option -> Async<'a option>
9+
```
10+
11+
Note that `sequence` is the same as `traverse id`. See also [Option.traverseAsync](traverseAsync.md).
12+
13+
See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).
14+
15+
## Examples
16+
17+
### Example 1
18+
19+
```fsharp
20+
let a1 : Async<int option> =
21+
Option.sequenceAsync (Some (Async.singleton 42))
22+
// async { return Some 42 }
23+
24+
let a2 : Async<int option> =
25+
Option.sequenceAsync None
26+
// async { return None }
27+
```

gitbook/option/traverseAsync.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## Option.traverseAsync
2+
3+
Namespace: `FsToolkit.ErrorHandling`
4+
5+
Function Signature:
6+
7+
```fsharp
8+
('a -> Async<'b>) -> 'a option -> Async<'b option>
9+
```
10+
11+
Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceAsync](sequenceAsync.md).
12+
13+
See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/).
14+
15+
## Examples
16+
17+
### Example 1
18+
19+
Let's assume we have a type `Customer`:
20+
21+
```fsharp
22+
type Customer = {
23+
Id : int
24+
Email : string
25+
}
26+
```
27+
28+
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:
29+
30+
```fsharp
31+
// string -> Async<Customer>
32+
let getCustomerByEmail email : Async<Customer> = async {
33+
return { Id = 1; Email = "[email protected]" } // return a constant for simplicity
34+
}
35+
```
36+
37+
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:
38+
39+
```fsharp
40+
Some "[email protected]" |> Option.traverseAsync getCustomerByEmail
41+
// async { return Some { Id = 1; Email = "[email protected]" } }
42+
43+
None |> Option.traverseAsync getCustomerByEmail
44+
// async { return None }
45+
```

src/FsToolkit.ErrorHandling/Option.fs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,28 @@ module Option =
321321

322322
opt
323323

324+
/// Converts a Option<Async<_>> to an Async<Option<_>>
325+
let inline sequenceAsync (optAsync: Option<Async<'T>>) : Async<Option<'T>> =
326+
async {
327+
match optAsync with
328+
| Some asnc ->
329+
let! x = asnc
330+
return Some x
331+
| None -> return None
332+
}
333+
334+
/// <summary>
335+
/// Maps an Async function over an Option, returning an Async Option.
336+
/// </summary>
337+
/// <param name="f">The function to map over the Option.</param>
338+
/// <param name="opt">The Option to map over.</param>
339+
/// <returns>An Async Option with the mapped value.</returns>
340+
let inline traverseAsync
341+
([<InlineIfLambda>] f: 'T -> Async<'T>)
342+
(opt: Option<'T>)
343+
: Async<Option<'T>> =
344+
sequenceAsync ((map f) opt)
345+
324346
/// <summary>
325347
/// Creates an option from a boolean value and a value of type 'a.
326348
/// If the boolean value is true, returns <c>Some</c> value.

tests/FsToolkit.ErrorHandling.Tests/Option.fs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,66 @@ let teeIfTests =
184184
Expect.equal foo "foo" ""
185185
]
186186

187+
let sequenceAsyncTests =
188+
testList "Option.sequenceAsync Tests" [
189+
testCaseAsync "sequenceAsync returns the async value if Some"
190+
<| async {
191+
let optAsync =
192+
async { return "foo" }
193+
|> Some
194+
195+
let! value =
196+
optAsync
197+
|> Option.sequenceAsync
198+
199+
Expect.equal value (Some "foo") ""
200+
}
201+
202+
testCaseAsync "sequenceAsync returns None if None"
203+
<| async {
204+
let optAsync = None
205+
206+
let! value =
207+
optAsync
208+
|> Option.sequenceAsync
209+
210+
Expect.equal value None ""
211+
}
212+
]
213+
214+
let traverseAsyncTests =
215+
testList "Option.traverseAsync Tests" [
216+
testCaseAsync "traverseAsync returns the async value if Some"
217+
<| async {
218+
let optAsync = Some "foo"
219+
220+
let optFunc =
221+
id
222+
>> Async.singleton
223+
224+
let! value =
225+
(optFunc, optAsync)
226+
||> Option.traverseAsync
227+
228+
Expect.equal value (Some "foo") ""
229+
}
230+
231+
testCaseAsync "traverseAsync returns None if None"
232+
<| async {
233+
let optAsync = None
234+
235+
let optFunc =
236+
id
237+
>> Async.singleton
238+
239+
let! value =
240+
(optFunc, optAsync)
241+
||> Option.traverseAsync
242+
243+
Expect.equal value None ""
244+
}
245+
]
246+
187247
let traverseResultTests =
188248
testList "Option.traverseResult Tests" [
189249
testCase "traverseResult with Some of valid data"
@@ -366,6 +426,8 @@ let optionOperatorsTests =
366426

367427
let allTests =
368428
testList "Option Tests" [
429+
sequenceAsyncTests
430+
traverseAsyncTests
369431
traverseResultTests
370432
tryParseTests
371433
tryGetValueTests

0 commit comments

Comments
 (0)