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

AsyncValidation #215

Merged
merged 4 commits into from
Jun 21, 2023
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
11 changes: 10 additions & 1 deletion gitbook/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,17 @@
* [ofResult](validation/ofResult.md)
* [Operators](validation/operators.md)
* [Result.tryCreate](validation/tryCreate.md)
* [Computation Expression](validation/ce.md)

* [AsyncValidation](asyncValidation/index.md)
* [map2](asyncValidation/map2.md)
* [map3](asyncValidation/map3.md)
* [apply](asyncValidation/apply.md)
* [ofResult](asyncValidation/ofResult.md)
* [Operators](asyncValidation/operators.md)
* [Computation Expression](asyncValidation/ce.md)

* [Test](test/index.md)
* [testing](test/testing.md)
* [testing-nested1](test/testing-nested1.md)
* [testing-nested2](test/testing-nested2.md)
* [testing-nested2](test/testing-nested2.md)
13 changes: 13 additions & 0 deletions gitbook/asyncValidation/apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## AsyncValidation.apply

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

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

## Examples
43 changes: 43 additions & 0 deletions gitbook/asyncValidation/ce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## AsyncValidation Computation Expression

Namespace: `FsToolkit.ErrorHandling`

The `AsyncValidation` type is defined as:

```fsharp
type AsyncValidation<'a,'err> = Async<Result<'a, 'err list>>
```

This CE can take advantage of the [and! operator](https://github.com/fsharp/fslang-suggestions/issues/579) to join multiple error results into a list.

## Examples

The example from [AsyncValidation.map3](../asyncValidation/map3.md#example-1) can be solved using the `asyncValidation` computation expression as below:

```fsharp
// Result<string, string> -> Async<Result<string, string>>
let downloadAsync stuff = async {
return stuff
}

// AsyncValidation<string, string>
let addResult = asyncValidation {
let! x = downloadAsync (Ok "I")
and! y = downloadAsync (Ok "am")
and! z = downloadAsync (Ok "async!")
return sprintf "%s %s %s" x y z
}

// result = async { return Ok "I am async!" }

// AsyncValidation<string, string>
let addResult = asyncValidation {
let! x = downloadAsync (Error "Am")
and! y = downloadAsync (Error "I")
and! z = downloadAsync (Error "async?")
return sprintf "%s %s %s" x y z
}

// result = async { return Error [ "Am"; "I"; "async?" ] }
```

5 changes: 5 additions & 0 deletions gitbook/asyncValidation/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## AsyncValidation

Namespace: `FsToolkit.ErrorHandling`

This module provides utility functions and infix operators to work with `Async<Result<'a, 'b list>>`.
16 changes: 16 additions & 0 deletions gitbook/asyncValidation/map2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## AsyncValidation.map2

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

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

Like [Result.map2](../result/map2.md), but collects the errors from both arguments.

## Examples
17 changes: 17 additions & 0 deletions gitbook/asyncValidation/map3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## AsyncAsyncValidation.map3

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```
('a -> 'b -> 'c -> 'd)
-> Async<Result<'a, 'e list>>
-> Async<Result<'b, 'e list>>
-> Async<Result<'c, 'e list>>
-> Async<Result<'d, 'e list>>
```

Like [Result.map3](../result/map3.md), but collects the errors from all arguments.

## Examples
13 changes: 13 additions & 0 deletions gitbook/asyncValidation/ofResult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## AsyncValidation.ofResult

Namespace: `FsToolkit.ErrorHandling`

Function Signature:

```fsharp
Result<'a, 'b> -> Async<Result<'a, 'b list>>
```

Simply wraps the error in a list and makes the result async.

## Examples
72 changes: 72 additions & 0 deletions gitbook/asyncValidation/operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
## AsyncValidation Infix Operators

Namespace: `FsToolkit.ErrorHandling.Operator.AsyncValidation`

FsToolkit.ErrorHandling provides the standard infix operators for `map` (`<!>`), `apply` (`<*>`), and `bind` (`>>=`) to work with `Result<'a, 'b list>`.

There are also variants of the `map` and `apply` operators (`<!^>` and `<*^>`) that accept `Result<'a, 'b>` (non-list) as the right-hand argument.

## Examples

### Example 1

Assume that we have following types and functions:

```fsharp
type Latitude = private Latitude of float with
// float -> Async<Result<Latitude, string list>>
static member TryCreate (lat : float) =
// ...

type Longitude = private Longitude of float with
// float -> Async<Result<Longitude, string list>>
static member TryCreate (lng : float) =
// ...

type Tweet = private Tweet of string with
// string -> Async<Result<Tweet, string list>>
static member TryCreate (tweet : string) =
// ...

// Latitude -> Longitude -> Tweet -> CreatePostRequest
let createPostRequest lat long tweet =
// ...
```

We can make use of the standard operators in the AsyncValidation Operators module to perform the asyncValidation of the incoming request and capture all the errors as shown below:

```fsharp
open FsToolkit.ErrorHandling.Operator.AsyncValidation

// float -> float -> string -> Async<Result<CreatePostRequest, string list>>
let validateCreatePostRequest lat lng tweet =
createPostRequest
<!> Latitude.TryCreate lat
<*> Longitude.TryCreate lng
<*> Tweet.TryCreate tweet
```

By using the `AsyncValidation` operators instead of the `Result` operators, we collect all the errors:
```fsharp
validateCreatePostRequest 300. 400. ""
// Error
["300.0 is a invalid latitude value"
"400.0 is a invalid longitude value"
"Tweet shouldn't be empty"]
```

### Example 2

In the above example, all the `TryCreate` functions return a string list as the error type (`Async<Result<'a, string list>>`). If these functions instead returned `Async<Result<'a, string>>` (only a single error), we can use `<*^>` and `<!^>` to get the same result:


```fsharp
open FsToolkit.ErrorHandling.Operator.AsyncValidation

// float -> float -> string -> Async<Result<CreatePostRequest, string list>>
let validateCreatePostRequest lat lng tweet =
createPostRequest
<!^> Latitude.TryCreate lat
<*^> Longitude.TryCreate lng
<*^> Tweet.TryCreate tweet
```
190 changes: 190 additions & 0 deletions src/FsToolkit.ErrorHandling/AsyncValidation.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
namespace FsToolkit.ErrorHandling

/// AsyncValidation<'a, 'err> is defined as Async<Result<'a, 'err list>> meaning you can use many of the functions found in the Result and Async module.
type AsyncValidation<'ok, 'error> = Async<Result<'ok, 'error list>>

[<RequireQualifiedAccess>]
module AsyncValidation =
let inline ok (value: 'ok) : AsyncValidation<'ok, 'error> =
Ok value
|> async.Return

let inline error (error: 'error) : AsyncValidation<'ok, 'error> =
Error [ error ]
|> async.Return

let inline ofResult (result: Result<'ok, 'error>) : AsyncValidation<'ok, 'error> =
Result.mapError List.singleton result
|> async.Return

let inline ofChoice (choice: Choice<'ok, 'error>) : AsyncValidation<'ok, 'error> =
match choice with
| Choice1Of2 x -> ok x
| Choice2Of2 e -> error e

let inline apply
(applier: AsyncValidation<'okInput -> 'okOutput, 'error>)
(input: AsyncValidation<'okInput, 'error>)
: AsyncValidation<'okOutput, 'error> =
async {
let! applier = applier
let! input = input

return
match applier, input with
| Ok f, Ok x -> Ok(f x)
| Error errs, Ok _
| Ok _, Error errs -> Error errs
| Error errs1, Error errs2 ->
Error(
errs1
@ errs2
)
}

let inline retn (value: 'ok) : AsyncValidation<'ok, 'error> = ok value

let inline returnError (error: 'error) : AsyncValidation<'ok, 'error> =
Error [ error ]
|> async.Return

let inline orElse
(ifError: AsyncValidation<'ok, 'errorOutput>)
(result: AsyncValidation<'ok, 'errorInput>)
: AsyncValidation<'ok, 'errorOutput> =
async {
let! result = result

return!
result
|> Result.either ok (fun _ -> ifError)
}

let inline orElseWith
([<InlineIfLambda>] ifErrorFunc: 'errorInput list -> AsyncValidation<'ok, 'errorOutput>)
(result: AsyncValidation<'ok, 'errorInput>)
: AsyncValidation<'ok, 'errorOutput> =
async {
let! result = result

return!
match result with
| Ok x -> ok x
| Error err -> ifErrorFunc err
}

let inline map
([<InlineIfLambda>] mapper: 'okInput -> 'okOutput)
(input: AsyncValidation<'okInput, 'error>)
: AsyncValidation<'okOutput, 'error> =
async {
let! input = input
return Result.map mapper input
}

let inline map2
([<InlineIfLambda>] mapper: 'okInput1 -> 'okInput2 -> 'okOutput)
(input1: AsyncValidation<'okInput1, 'error>)
(input2: AsyncValidation<'okInput2, 'error>)
: AsyncValidation<'okOutput, 'error> =
async {
let! input1 = input1
let! input2 = input2

return
match input1, input2 with
| Ok x, Ok y -> Ok(mapper x y)
| Ok _, Error errs -> Error errs
| Error errs, Ok _ -> Error errs
| Error errs1, Error errs2 ->
Error(
errs1
@ errs2
)
}

let inline map3
([<InlineIfLambda>] mapper: 'okInput1 -> 'okInput2 -> 'okInput3 -> 'okOutput)
(input1: AsyncValidation<'okInput1, 'error>)
(input2: AsyncValidation<'okInput2, 'error>)
(input3: AsyncValidation<'okInput3, 'error>)
: AsyncValidation<'okOutput, 'error> =
async {
let! input1 = input1
let! input2 = input2
let! input3 = input3

return
match input1, input2, input3 with
| Ok x, Ok y, Ok z -> Ok(mapper x y z)
| Error errs, Ok _, Ok _ -> Error errs
| Ok _, Error errs, Ok _ -> Error errs
| Ok _, Ok _, Error errs -> Error errs
| Error errs1, Error errs2, Ok _ ->
Error(
errs1
@ errs2
)
| Ok _, Error errs1, Error errs2 ->
Error(
errs1
@ errs2
)
| Error errs1, Ok _, Error errs2 ->
Error(
errs1
@ errs2
)
| Error errs1, Error errs2, Error errs3 ->
Error(
errs1
@ errs2
@ errs3
)
}

let inline mapError
([<InlineIfLambda>] errorMapper: 'errorInput -> 'errorOutput)
(input: AsyncValidation<'ok, 'errorInput>)
: AsyncValidation<'ok, 'errorOutput> =
async {
let! input = input
return Result.mapError (List.map errorMapper) input
}

let inline mapErrors
([<InlineIfLambda>] errorMapper: 'errorInput list -> 'errorOutput list)
(input: AsyncValidation<'ok, 'errorInput>)
: AsyncValidation<'ok, 'errorOutput> =
async {
let! input = input
return Result.mapError errorMapper input
}

let inline bind
([<InlineIfLambda>] binder: 'okInput -> AsyncValidation<'okOutput, 'error>)
(input: AsyncValidation<'okInput, 'error>)
: AsyncValidation<'okOutput, 'error> =
async {
let! input = input

match input with
| Ok x -> return! binder x
| Error e -> return Error e
}

let inline zip
(left: AsyncValidation<'left, 'error>)
(right: AsyncValidation<'right, 'error>)
: AsyncValidation<'left * 'right, 'error> =
async {
let! left = left
let! right = right

return
match left, right with
| Ok x1res, Ok x2res -> Ok(x1res, x2res)
| Error e, Ok _ -> Error e
| Ok _, Error e -> Error e
| Error e1, Error e2 -> Error(e1 @ e2)
}
Loading