Skip to content
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

Add Option.traverseAsync and Option.sequenceAsync #298

Merged
merged 6 commits into from
Jan 6, 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
27 changes: 27 additions & 0 deletions gitbook/option/sequenceAsync.md
Original file line number Diff line number Diff line change
@@ -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.traverseAsync](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<int option> =
Option.sequenceAsync (Some (Async.singleton 42))
// async { return Some 42 }

let a2 : Async<int option> =
Option.sequenceAsync None
// async { return None }
```
45 changes: 45 additions & 0 deletions gitbook/option/traverseAsync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Option.traverseAsync

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<Customer>
let getCustomerByEmail email : Async<Customer> = async {
return { Id = 1; Email = "[email protected]" } // 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 "[email protected]" |> Option.traverseAsync getCustomerByEmail
// async { return Some { Id = 1; Email = "[email protected]" } }

None |> Option.traverseAsync getCustomerByEmail
// async { return None }
```
22 changes: 22 additions & 0 deletions src/FsToolkit.ErrorHandling/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,28 @@ module Option =

opt

/// Converts a Option<Async<_>> to an Async<Option<_>>
let inline sequenceAsync (optAsync: Option<Async<'T>>) : Async<Option<'T>> =
async {
match optAsync with
| Some asnc ->
let! x = asnc
return Some x
| None -> return None
}

/// <summary>
/// Maps an Async function over an Option, returning an Async Option.
/// </summary>
/// <param name="f">The function to map over the Option.</param>
/// <param name="opt">The Option to map over.</param>
/// <returns>An Async Option with the mapped value.</returns>
let inline traverseAsync
([<InlineIfLambda>] f: 'T -> Async<'T>)
(opt: Option<'T>)
: Async<Option<'T>> =
sequenceAsync ((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
62 changes: 62 additions & 0 deletions tests/FsToolkit.ErrorHandling.Tests/Option.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -366,6 +426,8 @@ let optionOperatorsTests =

let allTests =
testList "Option Tests" [
sequenceAsyncTests
traverseAsyncTests
traverseResultTests
tryParseTests
tryGetValueTests
Expand Down
Loading