Skip to content

Commit ccd3641

Browse files
fix: correct Async.bind signature and implementation
The previous signature was (Async<'T> -> Async<'U>) -> Async<'T> -> Async<'U>, which passed the entire Async<'T> computation to the binder without first awaiting it. This made the function essentially equivalent to plain function application, not a monadic bind. The correct signature is ('T -> Async<'U>) -> Async<'T> -> Async<'U>, matching Task.bind (which had the same bug fixed in commit 8486e1b) and standard monadic bind semantics. Updated CompatibilitySuppressions.xml to suppress the CP0002 baseline-breaking change diagnostic, since this is an intentional fix of an incorrect public API. Added Utils.Tests.fs with tests covering the corrected behavior of Async.bind, Task.bind, Async.map, and Task.map. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9984a8b commit ccd3641

File tree

6 files changed

+130
-2
lines changed

6 files changed

+130
-2
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
Unreleased
5+
- fixes: `Async.bind` signature corrected from `(Async<'T> -> Async<'U>)` to `('T -> Async<'U>)` to match standard monadic bind semantics (same as `Task.bind`); the previous signature made the function effectively equivalent to direct application
56

67
1.1.1
78
- perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
<Compile Include="TaskSeq.Using.Tests.fs" />
8181
<Compile Include="TaskSeq.CancellationToken.Tests.fs" />
8282
<Compile Include="TaskSeq.WithCancellation.Tests.fs" />
83+
<Compile Include="Utils.Tests.fs" />
8384
</ItemGroup>
8485

8586
<ItemGroup>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module TaskSeq.Tests.Utils
2+
3+
open System
4+
open System.Threading.Tasks
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
11+
module AsyncBind =
12+
[<Fact>]
13+
let ``Async.bind awaits the async and passes the value to the binder`` () =
14+
let result =
15+
async { return 21 }
16+
|> Async.bind (fun n -> async { return n * 2 })
17+
|> Async.RunSynchronously
18+
19+
result |> should equal 42
20+
21+
[<Fact>]
22+
let ``Async.bind propagates exceptions from the source async`` () =
23+
let run () =
24+
async { return raise (InvalidOperationException "source error") }
25+
|> Async.bind (fun (_: int) -> async { return 0 })
26+
|> Async.RunSynchronously
27+
28+
(fun () -> run () |> ignore)
29+
|> should throw typeof<InvalidOperationException>
30+
31+
[<Fact>]
32+
let ``Async.bind propagates exceptions from the binder`` () =
33+
let run () =
34+
async { return 1 }
35+
|> Async.bind (fun _ -> async { return raise (InvalidOperationException "binder error") })
36+
|> Async.RunSynchronously
37+
38+
(fun () -> run () |> ignore)
39+
|> should throw typeof<InvalidOperationException>
40+
41+
[<Fact>]
42+
let ``Async.bind chains correctly`` () =
43+
let result =
44+
async { return 1 }
45+
|> Async.bind (fun n -> async { return n + 10 })
46+
|> Async.bind (fun n -> async { return n + 100 })
47+
|> Async.RunSynchronously
48+
49+
result |> should equal 111
50+
51+
[<Fact>]
52+
let ``Async.bind passes the unwrapped value, not the Async wrapper`` () =
53+
// This test specifically verifies the bug fix: binder receives 'T, not Async<'T>
54+
let mutable receivedType = typeof<unit>
55+
56+
async { return 42 }
57+
|> Async.bind (fun (n: int) ->
58+
receivedType <- n.GetType()
59+
async { return () })
60+
|> Async.RunSynchronously
61+
62+
receivedType |> should equal typeof<int>
63+
64+
65+
module TaskBind =
66+
[<Fact>]
67+
let ``Task.bind awaits the task and passes the value to the binder`` () = task {
68+
let result =
69+
task { return 21 }
70+
|> Task.bind (fun n -> task { return n * 2 })
71+
72+
let! v = result
73+
v |> should equal 42
74+
}
75+
76+
[<Fact>]
77+
let ``Task.bind chains correctly`` () = task {
78+
let result =
79+
task { return 1 }
80+
|> Task.bind (fun n -> task { return n + 10 })
81+
|> Task.bind (fun n -> task { return n + 100 })
82+
83+
let! v = result
84+
v |> should equal 111
85+
}
86+
87+
88+
module AsyncMap =
89+
[<Fact>]
90+
let ``Async.map transforms the result`` () =
91+
let result =
92+
async { return 21 }
93+
|> Async.map (fun n -> n * 2)
94+
|> Async.RunSynchronously
95+
96+
result |> should equal 42
97+
98+
[<Fact>]
99+
let ``Async.map chains correctly`` () =
100+
let result =
101+
async { return 1 }
102+
|> Async.map (fun n -> n + 10)
103+
|> Async.map (fun n -> n + 100)
104+
|> Async.RunSynchronously
105+
106+
result |> should equal 111
107+
108+
109+
module TaskMap =
110+
[<Fact>]
111+
let ``Task.map transforms the result`` () = task {
112+
let result = task { return 21 } |> Task.map (fun n -> n * 2)
113+
114+
let! v = result
115+
v |> should equal 42
116+
}

src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
33
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
4+
<Suppression>
5+
<DiagnosticId>CP0002</DiagnosticId>
6+
<Target>M:FSharp.Control.Async.bind``2(Microsoft.FSharp.Core.FSharpFunc{Microsoft.FSharp.Control.FSharpAsync{``0},Microsoft.FSharp.Control.FSharpAsync{``1}},Microsoft.FSharp.Control.FSharpAsync{``0})</Target>
7+
<Left>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Left>
8+
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
9+
<IsBaselineSuppression>true</IsBaselineSuppression>
10+
</Suppression>
411
<Suppression>
512
<DiagnosticId>CP0002</DiagnosticId>
613
<Target>M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind``5(FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}})</Target>

src/FSharp.Control.TaskSeq/Utils.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,7 @@ module Async =
7272
return mapper result
7373
}
7474

75-
let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder async }
75+
let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async {
76+
let! result = async
77+
return! binder result
78+
}

src/FSharp.Control.TaskSeq/Utils.fsi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,4 @@ module Async =
103103
val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U>
104104

105105
/// Bind an Async<'T>
106-
val inline bind: binder: (Async<'T> -> Async<'U>) -> async: Async<'T> -> Async<'U>
106+
val inline bind: binder: ('T -> Async<'U>) -> async: Async<'T> -> Async<'U>

0 commit comments

Comments
 (0)