From 779b07371dfd9c2af53619c4e90a183d67da845a Mon Sep 17 00:00:00 2001 From: Jay Goldstein Date: Sat, 29 Mar 2025 16:11:01 +0100 Subject: [PATCH 1/3] WIP src and test cases --- src/FsToolkit.ErrorHandling/Option.fs | 35 +++++++++++ tests/FsToolkit.ErrorHandling.Tests/Option.fs | 63 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/FsToolkit.ErrorHandling/Option.fs b/src/FsToolkit.ErrorHandling/Option.fs index 50495d26..4624d977 100644 --- a/src/FsToolkit.ErrorHandling/Option.fs +++ b/src/FsToolkit.ErrorHandling/Option.fs @@ -321,7 +321,11 @@ module Option = opt + /// /// Converts a Option> to an Async> + /// + /// Documentation is found here: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/sequenceasync + /// let inline sequenceAsync (optAsync: Option>) : Async> = async { match optAsync with @@ -333,6 +337,8 @@ module Option = /// /// Maps an Async function over an Option, returning an Async Option. + /// + /// Documentation is found here: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traverseasync /// /// The function to map over the Option. /// The Option to map over. @@ -343,6 +349,35 @@ module Option = : Async> = sequenceAsync ((map f) opt) + /// + /// Converts a Option>> to an Async,'error>> + /// + let inline sequenceAsyncResult + (optAsyncResult: Option>>) + : Async, 'E>> = + async { + match optAsyncResult with + | Some asncRes -> + let! xRes = asncRes + + return + xRes + |> Result.map Some + | None -> return Ok None + } + + /// + /// Maps an AsyncResult function over an Option, returning an AsyncResult Option. + /// + /// The function to map over the Option. + /// The Option to map over. + /// An AsyncResult Option with the mapped value. + let inline traverseAsyncResult + ([] f: 'T -> Async>) + (opt: Option<'T>) + : Async, 'E>> = + sequenceAsyncResult ((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 e4ed03ce..f024bbd5 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Option.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Option.fs @@ -211,6 +211,52 @@ let sequenceAsyncTests = } ] +let sequenceAsyncResultTests = + ftestList "Option.sequenceAsyncResult Tests" [ + testCaseAsync "sequenceAsyncResult returns the async Ok value if Some" + <| async { + let optAsyncOk = + async { return Ok "foo" } + |> Some + + let! valueRes = + optAsyncOk + |> Option.sequenceAsyncResult + + let value = Expect.wantOk valueRes "Expect to get back OK" + Expect.equal value (Some "foo") "Expect to get back value" + } + + testCaseAsync "sequenceAsyncResult returns the async Error value if Some" + <| async { + let optAsyncOk = + async { return Error "error" } + |> Some + + let! valueRes = + optAsyncOk + |> Option.sequenceAsyncResult + + let errorValue = Expect.wantError valueRes "Expect to get back Error" + Expect.equal errorValue "error" "Expect to get back the error value" + } + + testCaseAsync "sequenceAsyncResult returns None if None" + <| async { + let optAsyncNone = None + + let! valueRes = + optAsyncNone + |> Option.sequenceAsyncResult + + let valueNone = Expect.wantOk valueRes "Expect to get back OK" + Expect.isNone valueNone "Expect to get back None" + + // check things actually fail when fail + Expect.isSome valueNone "This should fail" + } + ] + let traverseAsyncTests = testList "Option.traverseAsync Tests" [ testCaseAsync "traverseAsync returns the async value if Some" @@ -259,6 +305,23 @@ let traverseResultTests = |> Expect.hasOkValue (Some validLng) ] +// to do +// let traverseAsyncResultTests = +// testList "Option.traverseAsyncResult Tests" [ +// testCaseAsync "traverseAsyncResult with valid latitute data" +// <| async { +// let tryCreateLatAsync = fun l -> async { return Latitude.TryCreate l } +// +// let! valueRes = +// Some lat +// |> Option.traverseAsyncResult tryCreateLatAsync +// +// let value = Expect.wantOk valueRes "Expect to get OK" +// Expect.equal value (Some validLat) "Expect to get valid latitute" +// +// } +// +// ] let tryParseTests = testList "Option.tryParse" [ From b7fdd991c1ad627c4c6fc14994617187491e26e5 Mon Sep 17 00:00:00 2001 From: Jay Goldstein Date: Sun, 30 Mar 2025 17:23:13 +0200 Subject: [PATCH 2/3] test cases for sequence and traverse --- tests/FsToolkit.ErrorHandling.Tests/Option.fs | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/tests/FsToolkit.ErrorHandling.Tests/Option.fs b/tests/FsToolkit.ErrorHandling.Tests/Option.fs index f024bbd5..0eaaad4f 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Option.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Option.fs @@ -212,7 +212,7 @@ let sequenceAsyncTests = ] let sequenceAsyncResultTests = - ftestList "Option.sequenceAsyncResult Tests" [ + testList "Option.sequenceAsyncResult Tests" [ testCaseAsync "sequenceAsyncResult returns the async Ok value if Some" <| async { let optAsyncOk = @@ -251,9 +251,6 @@ let sequenceAsyncResultTests = let valueNone = Expect.wantOk valueRes "Expect to get back OK" Expect.isNone valueNone "Expect to get back None" - - // check things actually fail when fail - Expect.isSome valueNone "This should fail" } ] @@ -305,23 +302,60 @@ let traverseResultTests = |> Expect.hasOkValue (Some validLng) ] -// to do -// let traverseAsyncResultTests = -// testList "Option.traverseAsyncResult Tests" [ -// testCaseAsync "traverseAsyncResult with valid latitute data" -// <| async { -// let tryCreateLatAsync = fun l -> async { return Latitude.TryCreate l } -// -// let! valueRes = -// Some lat -// |> Option.traverseAsyncResult tryCreateLatAsync -// -// let value = Expect.wantOk valueRes "Expect to get OK" -// Expect.equal value (Some validLat) "Expect to get valid latitute" -// -// } -// -// ] +let traverseAsyncResultTests = + testList "Option.traverseAsyncResult Tests" [ + testCaseAsync "traverseAsyncResult with valid latitute data" + <| async { + let tryCreateLatAsync = fun l -> async { return Latitude.TryCreate l } + + let! valueRes = + Some lat + |> Option.traverseAsyncResult tryCreateLatAsync + + let value = Expect.wantOk valueRes "Expect to get OK" + Expect.equal value (Some validLat) "Expect to get valid latitute" + } + + testCaseAsync "traverseAsyncResult id returns async Ok value if Some" + <| async { + let optAsyncOk = + async { return Ok "foo" } + |> Some + + let! valueRes = + optAsyncOk + |> Option.traverseAsyncResult id + + let value = Expect.wantOk valueRes "Expect to get back OK" + Expect.equal value (Some "foo") "Expect to get back value" + } + + testCaseAsync "traverseAsyncResult id returns the async Error value if Some" + <| async { + let optAsyncOk = + async { return Error "error" } + |> Some + + let! valueRes = + optAsyncOk + |> Option.traverseAsyncResult id + + let errorValue = Expect.wantError valueRes "Expect to get back Error" + Expect.equal errorValue "error" "Expect to get back the error value" + } + + testCaseAsync "traverseAsyncResult id returns None if None" + <| async { + let optAsyncNone = None + + let! valueRes = + optAsyncNone + |> Option.traverseAsyncResult id + + let valueNone = Expect.wantOk valueRes "Expect to get back OK" + Expect.isNone valueNone "Expect to get back None" + } + ] let tryParseTests = testList "Option.tryParse" [ @@ -490,8 +524,10 @@ let optionOperatorsTests = let allTests = testList "Option Tests" [ sequenceAsyncTests + sequenceAsyncResultTests traverseAsyncTests traverseResultTests + traverseAsyncResultTests tryParseTests tryGetValueTests ofResultTests From d414fb08d5c064c629dff67a3c7ff2ec71da17dd Mon Sep 17 00:00:00 2001 From: Jay Goldstein Date: Sun, 30 Mar 2025 18:53:52 +0200 Subject: [PATCH 3/3] add gitbook pages --- gitbook/option/sequenceAsyncResult.md | 31 +++++++++++++++++++ gitbook/option/traverseAsyncResult.md | 44 +++++++++++++++++++++++++++ src/FsToolkit.ErrorHandling/Option.fs | 16 ++++++---- 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 gitbook/option/sequenceAsyncResult.md create mode 100644 gitbook/option/traverseAsyncResult.md diff --git a/gitbook/option/sequenceAsyncResult.md b/gitbook/option/sequenceAsyncResult.md new file mode 100644 index 00000000..58a45a46 --- /dev/null +++ b/gitbook/option/sequenceAsyncResult.md @@ -0,0 +1,31 @@ +## Option.sequenceAsyncResult + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +Async> option -> Async, 'e> +``` + +Note that `sequence` is the same as `traverse id`. See also [Option.traverseAsyncResult](traverseAsyncResult.md). + +See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/). + +## Examples + +### Example 1 + +```fsharp +let r1 : Async> = + Some (async { return Ok 42 }) |> Option.sequenceAsyncResult +// async { return Ok (Some 42) } + +let r2 : Async> = + Some (async { return Error "something went wrong" }) |> Option.sequenceAsyncResult +// async { return Error "something went wrong" } + +let r3 : Async> = + None |> Option.sequenceAsyncResult +// async { return Ok None } +``` diff --git a/gitbook/option/traverseAsyncResult.md b/gitbook/option/traverseAsyncResult.md new file mode 100644 index 00000000..b0f73d6f --- /dev/null +++ b/gitbook/option/traverseAsyncResult.md @@ -0,0 +1,44 @@ +## Option.traverseAsyncResult + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +('a -> Async>) -> 'a option -> Async> +``` + +Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceAsyncResult](sequenceAsyncResult.md). + +See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/). + +## Examples + +### Example 1 + +Say we have a function to get a number from a database (asynchronously), and multiply our input by that number if it's found: + +```fsharp +let tryMultiplyWithDatabaseValue: float -> Async> = // ... +``` + +If we start with an optional vaue, then we could map this funciton using `Option.traverseAsyncResult` as follows: + +```fsharp +let input = Some 1.234 + +input // float option +|> Option.traverseAsyncResult tryMultiplyWithDatabaseValue // Async> +``` + +If we combine this with the [AsyncResult computation expression](../asyncResult/ce.md), we could directly `let!` the output: + +```fsharp +asyncResult { + let input = Some 1.234 + + let! output = // float option + input // float option + |> Option.traverseAsyncResult tryMultiplyWithDatabaseValue // Async> +} +``` diff --git a/src/FsToolkit.ErrorHandling/Option.fs b/src/FsToolkit.ErrorHandling/Option.fs index 4624d977..4b3e15c0 100644 --- a/src/FsToolkit.ErrorHandling/Option.fs +++ b/src/FsToolkit.ErrorHandling/Option.fs @@ -350,11 +350,13 @@ module Option = sequenceAsync ((map f) opt) /// - /// Converts a Option>> to an Async,'error>> + /// Converts a Async> option to an Async> + /// + /// Documentation is found here: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/sequenceasyncresult /// let inline sequenceAsyncResult - (optAsyncResult: Option>>) - : Async, 'E>> = + (optAsyncResult: Async> option) + : Async> = async { match optAsyncResult with | Some asncRes -> @@ -367,15 +369,17 @@ module Option = } /// - /// Maps an AsyncResult function over an Option, returning an AsyncResult Option. + /// Maps an AsyncResult function over an option, returning an AsyncResult option. + /// + /// /// Documentation is found here: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traverseasyncresult /// /// The function to map over the Option. /// The Option to map over. /// An AsyncResult Option with the mapped value. let inline traverseAsyncResult ([] f: 'T -> Async>) - (opt: Option<'T>) - : Async, 'E>> = + (opt: 'T option) + : Async> = sequenceAsyncResult ((map f) opt) ///