Skip to content

Commit ee6b1b8

Browse files
Add Option.sequenceAsyncResult and Option.traverseAsyncResult (#321)
* WIP src and test cases * test cases for sequence and traverse * add gitbook pages
1 parent 3244da4 commit ee6b1b8

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

gitbook/option/sequenceAsyncResult.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Option.sequenceAsyncResult
2+
3+
Namespace: `FsToolkit.ErrorHandling`
4+
5+
Function Signature:
6+
7+
```fsharp
8+
Async<Result<'a, 'e>> option -> Async<Result<'a option>, 'e>
9+
```
10+
11+
Note that `sequence` is the same as `traverse id`. See also [Option.traverseAsyncResult](traverseAsyncResult.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 r1 : Async<Result<int option, string>> =
21+
Some (async { return Ok 42 }) |> Option.sequenceAsyncResult
22+
// async { return Ok (Some 42) }
23+
24+
let r2 : Async<Result<int option, string>> =
25+
Some (async { return Error "something went wrong" }) |> Option.sequenceAsyncResult
26+
// async { return Error "something went wrong" }
27+
28+
let r3 : Async<Result<int option, string>> =
29+
None |> Option.sequenceAsyncResult
30+
// async { return Ok None }
31+
```

gitbook/option/traverseAsyncResult.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
## Option.traverseAsyncResult
2+
3+
Namespace: `FsToolkit.ErrorHandling`
4+
5+
Function Signature:
6+
7+
```fsharp
8+
('a -> Async<Result<'b,'c>>) -> 'a option -> Async<Result<'b option, 'c>>
9+
```
10+
11+
Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceAsyncResult](sequenceAsyncResult.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+
Say we have a function to get a number from a database (asynchronously), and multiply our input by that number if it's found:
20+
21+
```fsharp
22+
let tryMultiplyWithDatabaseValue: float -> Async<Result<float, string>> = // ...
23+
```
24+
25+
If we start with an optional vaue, then we could map this funciton using `Option.traverseAsyncResult` as follows:
26+
27+
```fsharp
28+
let input = Some 1.234
29+
30+
input // float option
31+
|> Option.traverseAsyncResult tryMultiplyWithDatabaseValue // Async<Result<float option, string>>
32+
```
33+
34+
If we combine this with the [AsyncResult computation expression](../asyncResult/ce.md), we could directly `let!` the output:
35+
36+
```fsharp
37+
asyncResult {
38+
let input = Some 1.234
39+
40+
let! output = // float option
41+
input // float option
42+
|> Option.traverseAsyncResult tryMultiplyWithDatabaseValue // Async<Result<float option, string>>
43+
}
44+
```

src/FsToolkit.ErrorHandling/Option.fs

+39
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,11 @@ module Option =
321321

322322
opt
323323

324+
/// <summary>
324325
/// Converts a Option<Async<_>> to an Async<Option<_>>
326+
///
327+
/// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/sequenceasync</href>
328+
/// </summary>
325329
let inline sequenceAsync (optAsync: Option<Async<'T>>) : Async<Option<'T>> =
326330
async {
327331
match optAsync with
@@ -333,6 +337,8 @@ module Option =
333337

334338
/// <summary>
335339
/// Maps an Async function over an Option, returning an Async Option.
340+
///
341+
/// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traverseasync</href>
336342
/// </summary>
337343
/// <param name="f">The function to map over the Option.</param>
338344
/// <param name="opt">The Option to map over.</param>
@@ -343,6 +349,39 @@ module Option =
343349
: Async<Option<'T>> =
344350
sequenceAsync ((map f) opt)
345351

352+
/// <summary>
353+
/// Converts a Async<Result<'ok,'error>> option to an Async<Result<'ok option,'error>>
354+
///
355+
/// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/sequenceasyncresult</href>
356+
/// </summary>
357+
let inline sequenceAsyncResult
358+
(optAsyncResult: Async<Result<'T, 'E>> option)
359+
: Async<Result<'T option, 'E>> =
360+
async {
361+
match optAsyncResult with
362+
| Some asncRes ->
363+
let! xRes = asncRes
364+
365+
return
366+
xRes
367+
|> Result.map Some
368+
| None -> return Ok None
369+
}
370+
371+
/// <summary>
372+
/// Maps an AsyncResult function over an option, returning an AsyncResult option.
373+
///
374+
/// /// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traverseasyncresult</href>
375+
/// </summary>
376+
/// <param name="f">The function to map over the Option.</param>
377+
/// <param name="opt">The Option to map over.</param>
378+
/// <returns>An AsyncResult Option with the mapped value.</returns>
379+
let inline traverseAsyncResult
380+
([<InlineIfLambda>] f: 'T -> Async<Result<'U, 'E>>)
381+
(opt: 'T option)
382+
: Async<Result<'U option, 'E>> =
383+
sequenceAsyncResult ((map f) opt)
384+
346385
/// <summary>
347386
/// Creates an option from a boolean value and a value of type 'a.
348387
/// If the boolean value is true, returns <c>Some</c> value.

tests/FsToolkit.ErrorHandling.Tests/Option.fs

+99
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,49 @@ let sequenceAsyncTests =
211211
}
212212
]
213213

214+
let sequenceAsyncResultTests =
215+
testList "Option.sequenceAsyncResult Tests" [
216+
testCaseAsync "sequenceAsyncResult returns the async Ok value if Some"
217+
<| async {
218+
let optAsyncOk =
219+
async { return Ok "foo" }
220+
|> Some
221+
222+
let! valueRes =
223+
optAsyncOk
224+
|> Option.sequenceAsyncResult
225+
226+
let value = Expect.wantOk valueRes "Expect to get back OK"
227+
Expect.equal value (Some "foo") "Expect to get back value"
228+
}
229+
230+
testCaseAsync "sequenceAsyncResult returns the async Error value if Some"
231+
<| async {
232+
let optAsyncOk =
233+
async { return Error "error" }
234+
|> Some
235+
236+
let! valueRes =
237+
optAsyncOk
238+
|> Option.sequenceAsyncResult
239+
240+
let errorValue = Expect.wantError valueRes "Expect to get back Error"
241+
Expect.equal errorValue "error" "Expect to get back the error value"
242+
}
243+
244+
testCaseAsync "sequenceAsyncResult returns None if None"
245+
<| async {
246+
let optAsyncNone = None
247+
248+
let! valueRes =
249+
optAsyncNone
250+
|> Option.sequenceAsyncResult
251+
252+
let valueNone = Expect.wantOk valueRes "Expect to get back OK"
253+
Expect.isNone valueNone "Expect to get back None"
254+
}
255+
]
256+
214257
let traverseAsyncTests =
215258
testList "Option.traverseAsync Tests" [
216259
testCaseAsync "traverseAsync returns the async value if Some"
@@ -259,6 +302,60 @@ let traverseResultTests =
259302
|> Expect.hasOkValue (Some validLng)
260303
]
261304

305+
let traverseAsyncResultTests =
306+
testList "Option.traverseAsyncResult Tests" [
307+
testCaseAsync "traverseAsyncResult with valid latitute data"
308+
<| async {
309+
let tryCreateLatAsync = fun l -> async { return Latitude.TryCreate l }
310+
311+
let! valueRes =
312+
Some lat
313+
|> Option.traverseAsyncResult tryCreateLatAsync
314+
315+
let value = Expect.wantOk valueRes "Expect to get OK"
316+
Expect.equal value (Some validLat) "Expect to get valid latitute"
317+
}
318+
319+
testCaseAsync "traverseAsyncResult id returns async Ok value if Some"
320+
<| async {
321+
let optAsyncOk =
322+
async { return Ok "foo" }
323+
|> Some
324+
325+
let! valueRes =
326+
optAsyncOk
327+
|> Option.traverseAsyncResult id
328+
329+
let value = Expect.wantOk valueRes "Expect to get back OK"
330+
Expect.equal value (Some "foo") "Expect to get back value"
331+
}
332+
333+
testCaseAsync "traverseAsyncResult id returns the async Error value if Some"
334+
<| async {
335+
let optAsyncOk =
336+
async { return Error "error" }
337+
|> Some
338+
339+
let! valueRes =
340+
optAsyncOk
341+
|> Option.traverseAsyncResult id
342+
343+
let errorValue = Expect.wantError valueRes "Expect to get back Error"
344+
Expect.equal errorValue "error" "Expect to get back the error value"
345+
}
346+
347+
testCaseAsync "traverseAsyncResult id returns None if None"
348+
<| async {
349+
let optAsyncNone = None
350+
351+
let! valueRes =
352+
optAsyncNone
353+
|> Option.traverseAsyncResult id
354+
355+
let valueNone = Expect.wantOk valueRes "Expect to get back OK"
356+
Expect.isNone valueNone "Expect to get back None"
357+
}
358+
]
262359

263360
let tryParseTests =
264361
testList "Option.tryParse" [
@@ -427,8 +524,10 @@ let optionOperatorsTests =
427524
let allTests =
428525
testList "Option Tests" [
429526
sequenceAsyncTests
527+
sequenceAsyncResultTests
430528
traverseAsyncTests
431529
traverseResultTests
530+
traverseAsyncResultTests
432531
tryParseTests
433532
tryGetValueTests
434533
ofResultTests

0 commit comments

Comments
 (0)