From 1bd3a22beed98ecad75eb014d7237dc5ae8275a1 Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 11:25:38 -0600 Subject: [PATCH 1/6] Implement Option.traverseAsync and Option.sequenceAsync and add tests --- src/FsToolkit.ErrorHandling/Option.fs | 22 +++++++ tests/FsToolkit.ErrorHandling.Tests/Option.fs | 62 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/FsToolkit.ErrorHandling/Option.fs b/src/FsToolkit.ErrorHandling/Option.fs index 5ab7d473..32a8066a 100644 --- a/src/FsToolkit.ErrorHandling/Option.fs +++ b/src/FsToolkit.ErrorHandling/Option.fs @@ -321,6 +321,28 @@ module Option = opt + /// Converts a Option> to an Async> + let inline sequenceAsync (optAsync: Option>) : Async> = + async { + match optAsync with + | Some asnc -> + let! x = asnc + return Some x + | None -> return None + } + + /// + /// Maps an Async function over an Option, returning an Async Option. + /// + /// The function to map over the Option. + /// The Option to map over. + /// An Async Option with the mapped value. + let inline traverseAsync + ([] f: 'T -> Async<'T>) + (opt: Option<'T>) + : Async> = + sequenceAsync ((map f) opt) + /// /// Creates an option from a boolean value and a value of type 'a. /// If the boolean value is true, returns Some value. diff --git a/tests/FsToolkit.ErrorHandling.Tests/Option.fs b/tests/FsToolkit.ErrorHandling.Tests/Option.fs index 219566a4..e4ed03ce 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Option.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Option.fs @@ -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" @@ -366,6 +426,8 @@ let optionOperatorsTests = let allTests = testList "Option Tests" [ + sequenceAsyncTests + traverseAsyncTests traverseResultTests tryParseTests tryGetValueTests From a65ffebf6789b2df0b0cf309132681ac44481e9c Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 12:41:06 -0600 Subject: [PATCH 2/6] Add examples to documentation --- gitbook/option/sequenceAsync.md | 27 ++++++++++++++++++++ gitbook/option/traverseAsync.md | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 gitbook/option/sequenceAsync.md create mode 100644 gitbook/option/traverseAsync.md diff --git a/gitbook/option/sequenceAsync.md b/gitbook/option/sequenceAsync.md new file mode 100644 index 00000000..bd4f50ae --- /dev/null +++ b/gitbook/option/sequenceAsync.md @@ -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.traverseAsyc](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 = + sequenceResult (Some (Async.singleton 42)) +// async { return Some 42 } + +let a2 : Async = + sequenceAsync None +// async { return None } +``` diff --git a/gitbook/option/traverseAsync.md b/gitbook/option/traverseAsync.md new file mode 100644 index 00000000..ad6b4ac0 --- /dev/null +++ b/gitbook/option/traverseAsync.md @@ -0,0 +1,45 @@ +## Option.traverseResult + +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 +let getCustomerByEmail email : Async = async { + return { Id = 1; Email = "test@test.com" } // 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 "test@test.com" |> Option.traverseAsync getCustomerByEmail +// async { return Some { Id = 1; Email = "test@test.com" } } + +None |> Option.traverseResult tryParseInt +// async { return None } +``` From c618a10a88f0119131dd15c3ba12e86202da71a3 Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 12:46:51 -0600 Subject: [PATCH 3/6] Fix typo in doc --- gitbook/option/sequenceAsync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitbook/option/sequenceAsync.md b/gitbook/option/sequenceAsync.md index bd4f50ae..8ed1c3f1 100644 --- a/gitbook/option/sequenceAsync.md +++ b/gitbook/option/sequenceAsync.md @@ -8,7 +8,7 @@ Function Signature: Async<'a> option -> Async<'a option> ``` -Note that `sequence` is the same as `traverse id`. See also [Option.traverseAsyc](traverseAsync.md). +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/). From 65b17e81164e2c5cc72d036cb8c67c1a37293dda Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 13:03:34 -0600 Subject: [PATCH 4/6] One more doc fix --- gitbook/option/sequenceAsync.md | 4 ++-- gitbook/option/traverseAsync.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gitbook/option/sequenceAsync.md b/gitbook/option/sequenceAsync.md index 8ed1c3f1..75ff4c1a 100644 --- a/gitbook/option/sequenceAsync.md +++ b/gitbook/option/sequenceAsync.md @@ -18,10 +18,10 @@ See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpfo ```fsharp let a1 : Async = - sequenceResult (Some (Async.singleton 42)) + Option.sequenceResult (Some (Async.singleton 42)) // async { return Some 42 } let a2 : Async = - sequenceAsync None + Option.sequenceAsync None // async { return None } ``` diff --git a/gitbook/option/traverseAsync.md b/gitbook/option/traverseAsync.md index ad6b4ac0..348aeb61 100644 --- a/gitbook/option/traverseAsync.md +++ b/gitbook/option/traverseAsync.md @@ -40,6 +40,6 @@ If we have a value of type `string option` and want to call the `getCustomerByEm Some "test@test.com" |> Option.traverseAsync getCustomerByEmail // async { return Some { Id = 1; Email = "test@test.com" } } -None |> Option.traverseResult tryParseInt +None |> Option.traverseAsync getCustomerByEmail // async { return None } ``` From 570efef5ead975e9a3e31ee791987200b9dcab20 Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 13:04:16 -0600 Subject: [PATCH 5/6] One more doc fix --- gitbook/option/traverseAsync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitbook/option/traverseAsync.md b/gitbook/option/traverseAsync.md index 348aeb61..6cde92da 100644 --- a/gitbook/option/traverseAsync.md +++ b/gitbook/option/traverseAsync.md @@ -1,4 +1,4 @@ -## Option.traverseResult +## Option.traverseAsync Namespace: `FsToolkit.ErrorHandling` From 902aac3f0ec033279d4824fd306cceb08a1d54cd Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 2 Jan 2025 13:05:13 -0600 Subject: [PATCH 6/6] One more doc fix --- gitbook/option/sequenceAsync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitbook/option/sequenceAsync.md b/gitbook/option/sequenceAsync.md index 75ff4c1a..7b54395a 100644 --- a/gitbook/option/sequenceAsync.md +++ b/gitbook/option/sequenceAsync.md @@ -18,7 +18,7 @@ See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpfo ```fsharp let a1 : Async = - Option.sequenceResult (Some (Async.singleton 42)) + Option.sequenceAsync (Some (Async.singleton 42)) // async { return Some 42 } let a2 : Async =