diff --git a/release-notes.txt b/release-notes.txt index 20ee8fb..49639e6 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: Unreleased + - 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 1.1.1 - perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 4fcf36c..0284354 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -80,6 +80,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs new file mode 100644 index 0000000..c89765c --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs @@ -0,0 +1,116 @@ +module TaskSeq.Tests.Utils + +open System +open System.Threading.Tasks +open Xunit +open FsUnit.Xunit + +open FSharp.Control + + +module AsyncBind = + [] + let ``Async.bind awaits the async and passes the value to the binder`` () = + let result = + async { return 21 } + |> Async.bind (fun n -> async { return n * 2 }) + |> Async.RunSynchronously + + result |> should equal 42 + + [] + let ``Async.bind propagates exceptions from the source async`` () = + let run () = + async { return raise (InvalidOperationException "source error") } + |> Async.bind (fun (_: int) -> async { return 0 }) + |> Async.RunSynchronously + + (fun () -> run () |> ignore) + |> should throw typeof + + [] + let ``Async.bind propagates exceptions from the binder`` () = + let run () = + async { return 1 } + |> Async.bind (fun _ -> async { return raise (InvalidOperationException "binder error") }) + |> Async.RunSynchronously + + (fun () -> run () |> ignore) + |> should throw typeof + + [] + let ``Async.bind chains correctly`` () = + let result = + async { return 1 } + |> Async.bind (fun n -> async { return n + 10 }) + |> Async.bind (fun n -> async { return n + 100 }) + |> Async.RunSynchronously + + result |> should equal 111 + + [] + let ``Async.bind passes the unwrapped value, not the Async wrapper`` () = + // This test specifically verifies the bug fix: binder receives 'T, not Async<'T> + let mutable receivedType = typeof + + async { return 42 } + |> Async.bind (fun (n: int) -> + receivedType <- n.GetType() + async { return () }) + |> Async.RunSynchronously + + receivedType |> should equal typeof + + +module TaskBind = + [] + let ``Task.bind awaits the task and passes the value to the binder`` () = task { + let result = + task { return 21 } + |> Task.bind (fun n -> task { return n * 2 }) + + let! v = result + v |> should equal 42 + } + + [] + let ``Task.bind chains correctly`` () = task { + let result = + task { return 1 } + |> Task.bind (fun n -> task { return n + 10 }) + |> Task.bind (fun n -> task { return n + 100 }) + + let! v = result + v |> should equal 111 + } + + +module AsyncMap = + [] + let ``Async.map transforms the result`` () = + let result = + async { return 21 } + |> Async.map (fun n -> n * 2) + |> Async.RunSynchronously + + result |> should equal 42 + + [] + let ``Async.map chains correctly`` () = + let result = + async { return 1 } + |> Async.map (fun n -> n + 10) + |> Async.map (fun n -> n + 100) + |> Async.RunSynchronously + + result |> should equal 111 + + +module TaskMap = + [] + let ``Task.map transforms the result`` () = task { + let result = task { return 21 } |> Task.map (fun n -> n * 2) + + let! v = result + v |> should equal 42 + } diff --git a/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml index 488c87a..c0e8a35 100644 --- a/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml +++ b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml @@ -1,6 +1,13 @@  + + CP0002 + 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}) + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + true + CP0002 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}}) diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index ec076f2..147734b 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -72,4 +72,7 @@ module Async = return mapper result } - let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder async } + let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { + let! result = async + return! binder result + } diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi index b171720..d252d45 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fsi +++ b/src/FSharp.Control.TaskSeq/Utils.fsi @@ -103,4 +103,4 @@ module Async = val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> /// Bind an Async<'T> - val inline bind: binder: (Async<'T> -> Async<'U>) -> async: Async<'T> -> Async<'U> + val inline bind: binder: ('T -> Async<'U>) -> async: Async<'T> -> Async<'U>