Skip to content
Draft
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
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<Compile Include="TaskSeq.Using.Tests.fs" />
<Compile Include="TaskSeq.CancellationToken.Tests.fs" />
<Compile Include="TaskSeq.WithCancellation.Tests.fs" />
<Compile Include="Utils.Tests.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
116 changes: 116 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
module TaskSeq.Tests.Utils

open System
open System.Threading.Tasks
open Xunit
open FsUnit.Xunit

open FSharp.Control


module AsyncBind =
[<Fact>]
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

[<Fact>]
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<InvalidOperationException>

[<Fact>]
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<InvalidOperationException>

[<Fact>]
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

[<Fact>]
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<unit>

async { return 42 }
|> Async.bind (fun (n: int) ->
receivedType <- n.GetType()
async { return () })
|> Async.RunSynchronously

receivedType |> should equal typeof<int>


module TaskBind =
[<Fact>]
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
}

[<Fact>]
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 =
[<Fact>]
let ``Async.map transforms the result`` () =
let result =
async { return 21 }
|> Async.map (fun n -> n * 2)
|> Async.RunSynchronously

result |> should equal 42

[<Fact>]
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 =
[<Fact>]
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
}
7 changes: 7 additions & 0 deletions src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
ο»Ώ<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<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>
<Left>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Left>
<Right>lib/netstandard2.1/FSharp.Control.TaskSeq.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<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>
Expand Down
5 changes: 4 additions & 1 deletion src/FSharp.Control.TaskSeq/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion src/FSharp.Control.TaskSeq/Utils.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading