Skip to content

Add Option.sequenceAsyncResult and Option.traverseAsyncResult #321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions gitbook/option/sequenceAsyncResult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Option.sequenceAsyncResult

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```fsharp
Async<Result<'a, 'e>> option -> Async<Result<'a option>, '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<Result<int option, string>> =
Some (async { return Ok 42 }) |> Option.sequenceAsyncResult
// async { return Ok (Some 42) }

let r2 : Async<Result<int option, string>> =
Some (async { return Error "something went wrong" }) |> Option.sequenceAsyncResult
// async { return Error "something went wrong" }

let r3 : Async<Result<int option, string>> =
None |> Option.sequenceAsyncResult
// async { return Ok None }
```
44 changes: 44 additions & 0 deletions gitbook/option/traverseAsyncResult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Option.traverseAsyncResult

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```fsharp
('a -> Async<Result<'b,'c>>) -> 'a option -> Async<Result<'b option, 'c>>
```

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<Result<float, string>> = // ...
```

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<Result<float option, string>>
```

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<Result<float option, string>>
}
```
39 changes: 39 additions & 0 deletions src/FsToolkit.ErrorHandling/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,11 @@ module Option =

opt

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

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

/// <summary>
/// Converts a Async<Result<'ok,'error>> option to an Async<Result<'ok option,'error>>
///
/// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/sequenceasyncresult</href>
/// </summary>
let inline sequenceAsyncResult
(optAsyncResult: Async<Result<'T, 'E>> option)
: Async<Result<'T option, 'E>> =
async {
match optAsyncResult with
| Some asncRes ->
let! xRes = asncRes

return
xRes
|> Result.map Some
| None -> return Ok None
}

/// <summary>
/// Maps an AsyncResult function over an option, returning an AsyncResult option.
///
/// /// Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/option/traverseasyncresult</href>
/// </summary>
/// <param name="f">The function to map over the Option.</param>
/// <param name="opt">The Option to map over.</param>
/// <returns>An AsyncResult Option with the mapped value.</returns>
let inline traverseAsyncResult
([<InlineIfLambda>] f: 'T -> Async<Result<'U, 'E>>)
(opt: 'T option)
: Async<Result<'U option, 'E>> =
sequenceAsyncResult ((map f) opt)

/// <summary>
/// Creates an option from a boolean value and a value of type 'a.
/// If the boolean value is true, returns <c>Some</c> value.
Expand Down
99 changes: 99 additions & 0 deletions tests/FsToolkit.ErrorHandling.Tests/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,49 @@ let sequenceAsyncTests =
}
]

let sequenceAsyncResultTests =
testList "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"
}
]

let traverseAsyncTests =
testList "Option.traverseAsync Tests" [
testCaseAsync "traverseAsync returns the async value if Some"
Expand Down Expand Up @@ -259,6 +302,60 @@ let traverseResultTests =
|> Expect.hasOkValue (Some validLng)
]

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" [
Expand Down Expand Up @@ -427,8 +524,10 @@ let optionOperatorsTests =
let allTests =
testList "Option Tests" [
sequenceAsyncTests
sequenceAsyncResultTests
traverseAsyncTests
traverseResultTests
traverseAsyncResultTests
tryParseTests
tryGetValueTests
ofResultTests
Expand Down
Loading