From 5ab3c098ba864ec2fe170fb2f1d49b07d5e46114 Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Tue, 15 Apr 2025 14:31:13 -0500 Subject: [PATCH 1/3] Implement sequence/traverse operations for Task and TaskResult in the Option module --- gitbook/SUMMARY.md | 4 + gitbook/option/sequenceTask.md | 27 +++ gitbook/option/sequenceTaskResult.md | 31 +++ gitbook/option/traverseTask.md | 45 +++++ gitbook/option/traverseTaskResult.md | 44 +++++ src/FsToolkit.ErrorHandling/Option.fs | 65 +++++++ tests/FsToolkit.ErrorHandling.Tests/Option.fs | 177 ++++++++++++++++++ 7 files changed, 393 insertions(+) create mode 100644 gitbook/option/sequenceTask.md create mode 100644 gitbook/option/sequenceTaskResult.md create mode 100644 gitbook/option/traverseTask.md create mode 100644 gitbook/option/traverseTaskResult.md diff --git a/gitbook/SUMMARY.md b/gitbook/SUMMARY.md index f56d69b8..ff859312 100644 --- a/gitbook/SUMMARY.md +++ b/gitbook/SUMMARY.md @@ -44,9 +44,13 @@ * [map3](option/map3.md) * [sequenceAsync](option/sequenceAsync.md) * [sequenceResult](option/sequenceResult.md) + * [sequenceTask](option/sequenceTask.md) + * [sequenceTaskResult](option/sequenceTaskResult.md) * [tee Functions](option/teeFunctions.md) * [traverseAsync](option/traverseAsync.md) * [traverseResult](option/traverseResult.md) + * [traverseTask](option/traverseTask.md) + * [traverseTaskResult](option/traverseTaskResult.md) * [zip](option/zip.md) * Lists * [traverseOptionM](option/traverseOptionM.md) diff --git a/gitbook/option/sequenceTask.md b/gitbook/option/sequenceTask.md new file mode 100644 index 00000000..1c701ee1 --- /dev/null +++ b/gitbook/option/sequenceTask.md @@ -0,0 +1,27 @@ +## Option.sequenceTask + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +Task<'a> option -> Task<'a option> +``` + +Note that `sequence` is the same as `traverse id`. See also [Option.traverseTask](traverseTask.md). + +See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/). + +## Examples + +### Example 1 + +```fsharp +let a1 : Task = + Option.sequenceTask (Some (Task.singleton 42)) +// async { return Some 42 } + +let a2 : Task = + Option.sequenceTask None +// async { return None } +``` diff --git a/gitbook/option/sequenceTaskResult.md b/gitbook/option/sequenceTaskResult.md new file mode 100644 index 00000000..5d5b405f --- /dev/null +++ b/gitbook/option/sequenceTaskResult.md @@ -0,0 +1,31 @@ +## Option.sequenceTaskResult + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +Task> option -> Task, 'e> +``` + +Note that `sequence` is the same as `traverse id`. See also [Option.traverseTaskResult](traverseTaskResult.md). + +See also Scott Wlaschin's [Understanding traverse and sequence](https://fsharpforfunandprofit.com/posts/elevated-world-4/). + +## Examples + +### Example 1 + +```fsharp +let r1 : Task> = + Some (task { return Ok 42 }) |> Option.sequenceTaskResult +// task { return Ok (Some 42) } + +let r2 : Task> = + Some (task { return Error "something went wrong" }) |> Option.sequenceTaskResult +// task { return Error "something went wrong" } + +let r3 : Task> = + None |> Option.sequenceTaskResult +// task { return Ok None } +``` diff --git a/gitbook/option/traverseTask.md b/gitbook/option/traverseTask.md new file mode 100644 index 00000000..34bdd819 --- /dev/null +++ b/gitbook/option/traverseTask.md @@ -0,0 +1,45 @@ +## Option.traverseTask + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +('a -> Task<'b>) -> 'a option -> Task<'b option> +``` + +Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceTask](sequenceTask.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 -> Task +let getCustomerByEmail email : Task = task { + 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 `traverseTask` function as below: + +```fsharp +Some "test@test.com" |> Option.traverseTask getCustomerByEmail +// task { return Some { Id = 1; Email = "test@test.com" } } + +None |> Option.traverseTask getCustomerByEmail +// task { return None } +``` diff --git a/gitbook/option/traverseTaskResult.md b/gitbook/option/traverseTaskResult.md new file mode 100644 index 00000000..8728304e --- /dev/null +++ b/gitbook/option/traverseTaskResult.md @@ -0,0 +1,44 @@ +## Option.traverseTaskResult + +Namespace: `FsToolkit.ErrorHandling` + +Function Signature: + +```fsharp +('a -> Task>) -> 'a option -> Task> +``` + +Note that `traverse` is the same as `map >> sequence`. See also [Option.sequenceTaskResult](sequenceTaskResult.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 -> Task> = // ... +``` + +If we start with an optional value, then we could map this function using `Option.traverseTaskResult` as follows: + +```fsharp +let input = Some 1.234 + +input // float option +|> Option.traverseTaskResult tryMultiplyWithDatabaseValue // Task> +``` + +If we combine this with the [TaskResult computation expression](../taskResult/ce.md), we could directly `let!` the output: + +```fsharp +taskResult { + let input = Some 1.234 + + let! output = // float option + input // float option + |> Option.traverseTaskResult tryMultiplyWithDatabaseValue // Task> +} +``` diff --git a/src/FsToolkit.ErrorHandling/Option.fs b/src/FsToolkit.ErrorHandling/Option.fs index 4b3e15c0..c92aec4c 100644 --- a/src/FsToolkit.ErrorHandling/Option.fs +++ b/src/FsToolkit.ErrorHandling/Option.fs @@ -321,6 +321,71 @@ module Option = opt +#if !FABLE_COMPILER + open System.Threading.Tasks + + /// + /// Converts an Option<Task<_>> to an Task<Option<_>>
+ /// + /// Documentation is found here: + ///
+ let inline sequenceTask (optTask: Option>) : Task> = + task { + match optTask with + | Some tsk -> + let! x = tsk + return Some x + | None -> return None + } + + /// + /// Maps a Task function over an option, returning a Task<'T option>
+ /// + /// Documentation is found here: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traversetask + ///
+ /// The function to map over the option. + /// The option to map over. + /// A Task<'T option> with the mapped value. + let inline traverseTask + ([] f: 'T -> Task<'T>) + (opt: Option<'T>) + : Task> = + sequenceTask ((map f) opt) + + /// + /// Converts an Async<Result<'ok,'error>> option to an Async<Result<'ok option,'error>>
+ /// + /// Documentation is found here: + ///
+ let inline sequenceTaskResult + (optTaskResult: Task> option) + : Task> = + task { + match optTaskResult with + | Some taskRes -> + let! xRes = taskRes + + return + xRes + |> Result.map Some + | None -> return Ok None + } + + /// + /// Maps a TaskResult function over an option, returning a Task<Result<'U option, 'E>>.
+ /// + /// Documentation is found here: + ///
+ /// The function to map over the option. + /// The option to map over. + /// A Task<Result<'U option, 'E>> with the mapped value. + let inline traverseTaskResult + ([] f: 'T -> Task>) + (opt: 'T option) + : Task> = + sequenceTaskResult ((map f) opt) +#endif + /// /// Converts a Option> to an Async> /// diff --git a/tests/FsToolkit.ErrorHandling.Tests/Option.fs b/tests/FsToolkit.ErrorHandling.Tests/Option.fs index 0eaaad4f..4b2a2c01 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Option.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Option.fs @@ -184,6 +184,177 @@ let teeIfTests = Expect.equal foo "foo" "" ] +#if !FABLE_COMPILER +let sequenceTaskTests = + testList "Option.sequenceTask Tests" [ + testCaseTask "sequenceTask returns the task value if Some" + <| fun () -> + task { + let optTask = + task { return "foo" } + |> Some + + let! value = + optTask + |> Option.sequenceTask + + Expect.equal value (Some "foo") "" + } + + testCaseTask "sequenceTask returns None if None" + <| fun () -> + task { + let optTask = None + + let! value = + optTask + |> Option.sequenceTask + + Expect.equal value None "" + } + ] + +let sequenceTaskResultTests = + testList "Option.sequenceTaskResult Tests" [ + testCaseTask "sequenceTaskResult returns the take Ok value if Some" + <| fun () -> + task { + let optTaskOk = + task { return Ok "foo" } + |> Some + + let! valueRes = + optTaskOk + |> Option.sequenceTaskResult + + let value = Expect.wantOk valueRes "Expect to get back OK" + Expect.equal value (Some "foo") "Expect to get back value" + } + + testCaseTask "sequenceTaskResult returns the task Error value if Some" + <| fun () -> + task { + let optTaskOk = + task { return Error "error" } + |> Some + + let! valueRes = + optTaskOk + |> Option.sequenceTaskResult + + let errorValue = Expect.wantError valueRes "Expect to get back Error" + Expect.equal errorValue "error" "Expect to get back the error value" + } + + testCaseTask "sequenceTaskResult returns None if None" + <| fun () -> + task { + let optTaskNone = None + + let! valueRes = + optTaskNone + |> Option.sequenceTaskResult + + let valueNone = Expect.wantOk valueRes "Expect to get back OK" + Expect.isNone valueNone "Expect to get back None" + } + ] + +let traverseTaskTests = + testList "Option.traverseTask Tests" [ + testCaseTask "traverseTask returns the task value if Some" + <| fun () -> + task { + let optTask = Some "foo" + + let optFunc = + id + >> Task.singleton + + let! value = + (optFunc, optTask) + ||> Option.traverseTask + + Expect.equal value (Some "foo") "" + } + + testCaseTask "traverseTask returns None if None" + <| fun() -> + task { + let optTask = None + + let optFunc = + id + >> Task.singleton + + let! value = + (optFunc, optTask) + ||> Option.traverseTask + + Expect.equal value None "" + } + ] + +let traverseTaskResultTests = + testList "Option.traverseTaskResult Tests" [ + testCaseTask "traverseTaskResult with valid latitute data" + <| fun () -> + task { + let tryCreateLatTask = fun l -> task { return Latitude.TryCreate l } + + let! valueRes = + Some lat + |> Option.traverseTaskResult tryCreateLatTask + + let value = Expect.wantOk valueRes "Expect to get OK" + Expect.equal value (Some validLat) "Expect to get valid latitute" + } + + testCaseTask "traverseTaskResult id returns async Ok value if Some" + <| fun () -> + task { + let optTaskOk = + task { return Ok "foo" } + |> Some + + let! valueRes = + optTaskOk + |> Option.traverseTaskResult id + + let value = Expect.wantOk valueRes "Expect to get back OK" + Expect.equal value (Some "foo") "Expect to get back value" + } + + testCaseTask "traverseTaskResult id returns the async Error value if Some" + <| fun () -> + task { + let optTaskOk = + task { return Error "error" } + |> Some + + let! valueRes = + optTaskOk + |> Option.traverseTaskResult id + + let errorValue = Expect.wantError valueRes "Expect to get back Error" + Expect.equal errorValue "error" "Expect to get back the error value" + } + + testCaseTask "traverseTaskResult id returns None if None" + <| fun () -> + task { + let optTaskNone = None + + let! valueRes = + optTaskNone + |> Option.traverseTaskResult id + + let valueNone = Expect.wantOk valueRes "Expect to get back OK" + Expect.isNone valueNone "Expect to get back None" + } + ] +#endif + let sequenceAsyncTests = testList "Option.sequenceAsync Tests" [ testCaseAsync "sequenceAsync returns the async value if Some" @@ -523,6 +694,12 @@ let optionOperatorsTests = let allTests = testList "Option Tests" [ + #if !FABLE_COMPILER + sequenceTaskTests + sequenceTaskResultTests + traverseTaskTests + traverseTaskResultTests + #endif sequenceAsyncTests sequenceAsyncResultTests traverseAsyncTests From 287060e89303904c493fb430a8e3b2590d4f9235 Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Tue, 15 Apr 2025 18:00:10 -0500 Subject: [PATCH 2/3] Format code --- tests/FsToolkit.ErrorHandling.Tests/Option.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/FsToolkit.ErrorHandling.Tests/Option.fs b/tests/FsToolkit.ErrorHandling.Tests/Option.fs index 4b2a2c01..d828d167 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Option.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Option.fs @@ -279,7 +279,7 @@ let traverseTaskTests = } testCaseTask "traverseTask returns None if None" - <| fun() -> + <| fun () -> task { let optTask = None @@ -694,12 +694,12 @@ let optionOperatorsTests = let allTests = testList "Option Tests" [ - #if !FABLE_COMPILER +#if !FABLE_COMPILER sequenceTaskTests sequenceTaskResultTests traverseTaskTests traverseTaskResultTests - #endif +#endif sequenceAsyncTests sequenceAsyncResultTests traverseAsyncTests From 8fd1b0666c6c2f09554e4508ec8a31ddbf85415f Mon Sep 17 00:00:00 2001 From: Matthew Watt Date: Thu, 1 May 2025 06:40:28 -0500 Subject: [PATCH 3/3] Typo in doc comment --- src/FsToolkit.ErrorHandling/Option.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsToolkit.ErrorHandling/Option.fs b/src/FsToolkit.ErrorHandling/Option.fs index c92aec4c..608e7d3e 100644 --- a/src/FsToolkit.ErrorHandling/Option.fs +++ b/src/FsToolkit.ErrorHandling/Option.fs @@ -353,7 +353,7 @@ module Option = sequenceTask ((map f) opt) /// - /// Converts an Async<Result<'ok,'error>> option to an Async<Result<'ok option,'error>>
+ /// Converts an Task<Result<'ok,'error>> option to an Task<Result<'ok option,'error>>
/// /// Documentation is found here: ///