Skip to content

Commit e22fba8

Browse files
Add Source overloads to taskResultOption CE for interop with Result, TaskResult, Option, Task, and Async (#351)
* Initial plan * Add Source overloads to taskResultOption CE for Result, TaskResult, TaskOption, Task, Async compatibility Co-authored-by: TheAngryByrd <1490044+TheAngryByrd@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TheAngryByrd <1490044+TheAngryByrd@users.noreply.github.com>
1 parent d10f132 commit e22fba8

4 files changed

Lines changed: 282 additions & 0 deletions

File tree

gitbook/taskResultOption/ce.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,39 @@ taskResultOption {
1616
return userTweet post user
1717
}
1818
```
19+
20+
### Example 2
21+
22+
The `taskResultOption` CE supports binding from multiple compatible types, allowing you to mix `TaskResultOption`, `TaskResult`, `Result`, `Option`, `Task`, and `Async` values without manual conversion:
23+
24+
```fsharp
25+
// parseMode: string -> Result<Mode, string>
26+
// loadUniverse: Mode -> Task<Result<Universe, string>>
27+
28+
// Task<Result<Report option, string>>
29+
taskResultOption {
30+
// Bind from Result<Mode, string> directly (no manual conversion needed)
31+
let! mode = parseMode "fast"
32+
33+
// Bind from Task<Result<Universe, string>> directly
34+
let! universe = loadUniverse mode
35+
36+
return generateReport universe
37+
}
38+
```
39+
40+
### Supported Source Types
41+
42+
The `taskResultOption` CE supports `let!` and `return!` with the following types:
43+
44+
| Type | Behavior |
45+
|------|----------|
46+
| `Task<Result<'ok option, 'error>>` | Identity (the CE's native type) |
47+
| `Task<Result<'ok, 'error>>` | `Ok` value is wrapped in `Some` |
48+
| `Task<'ok option>` | `Some`/`None` is wrapped in `Ok` |
49+
| `Task<'ok>` | Value is wrapped in `Some` and then `Ok` |
50+
| `Result<'ok, 'error>` | `Ok` value is wrapped in `Some`, lifted to `Task` |
51+
| `Choice<'ok, 'error>` | Converted to `Result`, then same as `Result` |
52+
| `'ok option` | `Some`/`None` wrapped in `Ok`, lifted to `Task` |
53+
| `Async<Result<'ok, 'error>>` | `Ok` value is wrapped in `Some` |
54+
| `Async<'ok>` | Value is wrapped in `Some` and then `Ok` |

src/FsToolkit.ErrorHandling/TaskResultOption.fs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,42 @@ module TaskResultOption =
3333
let inline ignore<'ok, 'error> (tro: Task<Result<'ok option, 'error>>) =
3434
tro
3535
|> map ignore<'ok>
36+
37+
/// <summary>
38+
/// Transforms a <c>Result&lt;'ok, 'error&gt;</c> into a <c>Task&lt;Result&lt;'ok option, 'error&gt;&gt;</c>.
39+
/// </summary>
40+
let inline ofResult (result: Result<'ok, 'error>) : Task<Result<'ok option, 'error>> =
41+
result
42+
|> Result.map Some
43+
|> Task.singleton
44+
45+
/// <summary>
46+
/// Transforms a <c>Task&lt;Result&lt;'ok, 'error&gt;&gt;</c> into a <c>Task&lt;Result&lt;'ok option, 'error&gt;&gt;</c>.
47+
/// </summary>
48+
let inline ofTaskResult
49+
(taskResult: Task<Result<'ok, 'error>>)
50+
: Task<Result<'ok option, 'error>> =
51+
taskResult
52+
|> TaskResult.map Some
53+
54+
/// <summary>
55+
/// Transforms a <c>'ok option</c> into a <c>Task&lt;Result&lt;'ok option, 'error&gt;&gt;</c>.
56+
/// </summary>
57+
let inline ofOption (option: 'ok option) : Task<Result<'ok option, 'error>> =
58+
option
59+
|> Ok
60+
|> Task.singleton
61+
62+
/// <summary>
63+
/// Transforms a <c>Task&lt;'ok option&gt;</c> into a <c>Task&lt;Result&lt;'ok option, 'error&gt;&gt;</c>.
64+
/// </summary>
65+
let inline ofTaskOption (taskOption: Task<'ok option>) : Task<Result<'ok option, 'error>> =
66+
taskOption
67+
|> Task.map Ok
68+
69+
/// <summary>
70+
/// Transforms a <c>Task&lt;'ok&gt;</c> into a <c>Task&lt;Result&lt;'ok option, 'error&gt;&gt;</c>.
71+
/// </summary>
72+
let inline ofTask (task: Task<'ok>) : Task<Result<'ok option, 'error>> =
73+
task
74+
|> Task.map (Some >> Ok)

src/FsToolkit.ErrorHandling/TaskResultOptionCE.fs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,94 @@ module TaskResultOptionCE =
250250
TaskResultOptionBuilder.RunDynamic(code)
251251

252252
let taskResultOption = TaskResultOptionBuilder()
253+
254+
[<AutoOpen>]
255+
module TaskResultOptionCEExtensionsLowPriority =
256+
// Low priority extensions - generic catch-alls
257+
type TaskResultOptionBuilder with
258+
259+
/// <summary>
260+
/// Method lets us transform data types into our internal representation.
261+
/// </summary>
262+
member inline _.Source(task: Task<'ok>) : TaskResultOption<'ok, 'error> =
263+
TaskResultOption.ofTask task
264+
265+
/// <summary>
266+
/// Method lets us transform data types into our internal representation.
267+
/// </summary>
268+
member inline _.Source(computation: Async<'ok>) : TaskResultOption<'ok, 'error> =
269+
computation
270+
|> Async.StartImmediateAsTask
271+
|> TaskResultOption.ofTask
272+
273+
[<AutoOpen>]
274+
module TaskResultOptionCEExtensions =
275+
// Medium-low priority extensions
276+
type TaskResultOptionBuilder with
277+
278+
/// <summary>
279+
/// Needed to allow `for..in` and `for..do` functionality
280+
/// </summary>
281+
member inline _.Source(s: #seq<'value>) : #seq<'value> = s
282+
283+
/// <summary>
284+
/// Method lets us transform data types into our internal representation.
285+
/// </summary>
286+
member inline _.Source(result: Result<'ok, 'error>) : TaskResultOption<'ok, 'error> =
287+
TaskResultOption.ofResult result
288+
289+
/// <summary>
290+
/// Method lets us transform data types into our internal representation.
291+
/// </summary>
292+
member inline this.Source(result: Choice<'ok, 'error>) : TaskResultOption<'ok, 'error> =
293+
this.Source(Result.ofChoice result)
294+
295+
/// <summary>
296+
/// Method lets us transform data types into our internal representation.
297+
/// </summary>
298+
member inline _.Source(option: 'ok option) : TaskResultOption<'ok, 'error> =
299+
TaskResultOption.ofOption option
300+
301+
[<AutoOpen>]
302+
module TaskResultOptionCEExtensionsMediumPriority =
303+
// Medium priority extensions - more specific than generic Task/Async
304+
type TaskResultOptionBuilder with
305+
306+
/// <summary>
307+
/// Method lets us transform data types into our internal representation.
308+
/// </summary>
309+
member inline _.Source
310+
(asyncResult: Async<Result<'ok, 'error>>)
311+
: TaskResultOption<'ok, 'error> =
312+
asyncResult
313+
|> Async.StartImmediateAsTask
314+
|> TaskResultOption.ofTaskResult
315+
316+
/// <summary>
317+
/// Method lets us transform data types into our internal representation.
318+
/// </summary>
319+
member inline _.Source(taskOption: Task<'ok option>) : TaskResultOption<'ok, 'error> =
320+
TaskResultOption.ofTaskOption taskOption
321+
322+
/// <summary>
323+
/// Method lets us transform data types into our internal representation.
324+
/// </summary>
325+
member inline _.Source
326+
(taskResult: Task<Result<'ok, 'error>>)
327+
: TaskResultOption<'ok, 'error> =
328+
TaskResultOption.ofTaskResult taskResult
329+
330+
[<AutoOpen>]
331+
module TaskResultOptionCEExtensionsHighPriority =
332+
// High priority extensions - identity for the self type
333+
type TaskResultOptionBuilder with
334+
335+
/// <summary>
336+
/// Method lets us transform data types into our internal representation. This is the identity method to recognize the self type.
337+
///
338+
/// See https://stackoverflow.com/questions/35286541/why-would-you-use-builder-source-in-a-custom-computation-expression-builder
339+
/// </summary>
340+
member inline _.Source
341+
(result: TaskResultOption<'ok, 'error>)
342+
: TaskResultOption<'ok, 'error> =
343+
result

tests/FsToolkit.ErrorHandling.Tests/TaskResultOption.fs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,121 @@ let ``TaskResultOptionCE inference checks`` =
259259
|> ignore
260260
]
261261

262+
let computationExpressionSourceTests =
263+
testList "taskResultOption CE Source Tests" [
264+
testCase "Bind Result.Ok"
265+
<| fun _ ->
266+
taskResultOption {
267+
let! value = Result.Ok "Foo"
268+
return value
269+
}
270+
|> Expect.hasTaskOkValueSync (Some "Foo")
271+
272+
testCase "Bind Result.Error"
273+
<| fun _ ->
274+
taskResultOption {
275+
let! value = Result.Error "error"
276+
return value
277+
}
278+
|> Expect.hasTaskErrorValueSync "error"
279+
280+
testCase "Bind Choice1Of2"
281+
<| fun _ ->
282+
taskResultOption {
283+
let! value = Choice1Of2 "Foo"
284+
return value
285+
}
286+
|> Expect.hasTaskOkValueSync (Some "Foo")
287+
288+
testCase "Bind Choice2Of2"
289+
<| fun _ ->
290+
taskResultOption {
291+
let! value = Choice2Of2 "error"
292+
return value
293+
}
294+
|> Expect.hasTaskErrorValueSync "error"
295+
296+
testCase "Bind Some"
297+
<| fun _ ->
298+
taskResultOption {
299+
let! value = Some "Foo"
300+
return value
301+
}
302+
|> Expect.hasTaskOkValueSync (Some "Foo")
303+
304+
testCase "Bind None"
305+
<| fun _ ->
306+
taskResultOption {
307+
let! value = (None: string option)
308+
return value
309+
}
310+
|> Expect.hasTaskOkValueSync None
311+
312+
testCase "Bind TaskResult Ok"
313+
<| fun _ ->
314+
taskResultOption {
315+
let! value = Task.FromResult(Ok "Foo")
316+
return value
317+
}
318+
|> Expect.hasTaskOkValueSync (Some "Foo")
319+
320+
testCase "Bind TaskResult Error"
321+
<| fun _ ->
322+
taskResultOption {
323+
let! value = Task.FromResult(Error "error")
324+
return value
325+
}
326+
|> Expect.hasTaskErrorValueSync "error"
327+
328+
testCase "Bind TaskOption Some"
329+
<| fun _ ->
330+
taskResultOption {
331+
let! value = Task.FromResult(Some "Foo")
332+
return value
333+
}
334+
|> Expect.hasTaskOkValueSync (Some "Foo")
335+
336+
testCase "Bind TaskOption None"
337+
<| fun _ ->
338+
taskResultOption {
339+
let! value = Task.FromResult(None: string option)
340+
return value
341+
}
342+
|> Expect.hasTaskOkValueSync None
343+
344+
testCase "Bind Task<'T>"
345+
<| fun _ ->
346+
taskResultOption {
347+
let! value = Task.FromResult "Foo"
348+
return value
349+
}
350+
|> Expect.hasTaskOkValueSync (Some "Foo")
351+
352+
testCase "Bind AsyncResult Ok"
353+
<| fun _ ->
354+
taskResultOption {
355+
let! value = async { return Ok "Foo" }
356+
return value
357+
}
358+
|> Expect.hasTaskOkValueSync (Some "Foo")
359+
360+
testCase "Bind AsyncResult Error"
361+
<| fun _ ->
362+
taskResultOption {
363+
let! value = async { return Error "error" }
364+
return value
365+
}
366+
|> Expect.hasTaskErrorValueSync "error"
367+
368+
testCase "Bind Async<'T>"
369+
<| fun _ ->
370+
taskResultOption {
371+
let! value = async { return "Foo" }
372+
return value
373+
}
374+
|> Expect.hasTaskOkValueSync (Some "Foo")
375+
]
376+
262377
let allTests =
263378
testList "TaskResultOption Tests" [
264379
mapTests
@@ -268,4 +383,5 @@ let allTests =
268383
computationExpressionTests
269384
operatorTests
270385
``TaskResultOptionCE inference checks``
386+
computationExpressionSourceTests
271387
]

0 commit comments

Comments
 (0)