-
Notifications
You must be signed in to change notification settings - Fork 815
and!
support in TaskBulder
#18451
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
and!
support in TaskBulder
#18451
Conversation
❗ Release notes required
|
I checked those extensions in the empty console app, and thought that just inserting them in random place without doing full build locally might work, however it's not the case :) Will try again tomorrow |
I've pushed the fix for build, but the behavior I described is much easier to see in the empty console app: open System
open System.Runtime.CompilerServices
open System.Threading.Tasks
type TaskBuilder with
member inline this.MergeSources(task1: Task<^TResult1>, task2: Task<^TResult2>) =
this.Run(
this.Bind(task1, fun (result1: ^TResult1) ->
this.Bind(task2, fun (result2: ^TResult2) ->
this.Return(result1, result2)
)
)
)
// member inline this.MergeSources3(task1: Task<^TResult1>, task2: Task<^TResult2>, task3: Task<^TResult3>) =
// Console.WriteLine("Middle")
// this.Run(
// this.Bind(task1, fun (result1: ^TResult1) ->
// this.Bind(task2, fun (result2: ^TResult2) ->
// this.Bind(task3, fun (result3: ^TResult3) ->
// this.Return(result1, result2, result3)
// )
// )
// )
// )
//
// member inline this.MergeSources4(task1: Task<^TResult1>, task2: Task<^TResult2>, task3: Task<^TResult3>, task4: Task<^TResult4>) =
// Console.WriteLine("Bottom")
// this.Run(
// this.Bind(task1, fun (result1: ^TResult1) ->
// this.Bind(task2, fun (result2: ^TResult2) ->
// this.Bind(task3, fun (result3: ^TResult3) ->
// this.Bind(task4, fun (result4: ^TResult4) ->
// this.Return(result1, result2, result3, result4)
// )
// )
// )
// )
// )
// This is required for type inference in mixed cases to work correctly
member inline this.MergeSources< ^TaskLike2, ^TResult1, ^TResult2, ^Awaiter2
when ^TaskLike2 : (member GetAwaiter: unit -> ^Awaiter2)
and ^Awaiter2 :> ICriticalNotifyCompletion
and ^Awaiter2: (member get_IsCompleted: unit -> bool)
and ^Awaiter2: (member GetResult: unit -> 'TResult2)>
(task1: Task<^TResult1>, task2: ^TaskLike2)
: Task<^TResult1 * ^TResult2> =
this.Run(
this.Bind(task1, fun (result1: ^TResult1) ->
this.Bind(task2, fun (result2: ^TResult2) ->
this.Return(result1, result2)
)
)
)
// This is required for type inference in mixed cases to work correctly
member inline this.MergeSources< ^TaskLike1, ^TResult1, ^TResult2, ^Awaiter1
when ^TaskLike1 : (member GetAwaiter: unit -> ^Awaiter1)
and ^Awaiter1 :> ICriticalNotifyCompletion
and ^Awaiter1: (member get_IsCompleted: unit -> bool)
and ^Awaiter1: (member GetResult: unit -> 'TResult1)>
(task1: ^TaskLike1, task2: Task<^TResult2>)
: Task<^TResult1 * ^TResult2> =
this.Run(
this.Bind(task1, fun (result1: ^TResult1) ->
this.Bind(task2, fun (result2: ^TResult2) ->
this.Return(result1, result2)
)
)
)
member inline this.MergeSources< ^TaskLike1, ^TaskLike2, ^TResult1, ^TResult2, ^Awaiter1, ^Awaiter2
when ^TaskLike1: (member GetAwaiter: unit -> ^Awaiter1)
and ^TaskLike2 : (member GetAwaiter: unit -> ^Awaiter2)
and ^Awaiter1 :> ICriticalNotifyCompletion
and ^Awaiter2 :> ICriticalNotifyCompletion
and ^Awaiter1: (member get_IsCompleted: unit -> bool)
and ^Awaiter1: (member GetResult: unit -> ^TResult1)
and ^Awaiter2: (member get_IsCompleted: unit -> bool)
and ^Awaiter2: (member GetResult: unit -> ^TResult2)>
(task1: ^TaskLike1, task2: ^TaskLike2)
: Task<^TResult1 * ^TResult2> =
this.Run(
this.Bind(task1, fun (result1: ^TResult1) ->
this.Bind(task2, fun (result2: ^TResult2) ->
this.Return(result1, result2)
)
)
)
let test() =
task {
let! x = Task.FromResult 1
and! y = Task.FromResult 2
return x + y
}
test().GetAwaiter().GetResult() |> printfn "%A" |
tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/Tasks.fs
Outdated
Show resolved
Hide resolved
This needs ilgen tests as well, to make sure it codegens resumable code as we expect |
I'll refrain from any additional work until we have an agreement on the issue with MergeSourcesN (which is the main goal of this draft PR). Right now adding MergeSources3 with just tasks breaks compilation for the case Task+Task+ValueTask, so all possible combinations of overloads are required. And this is even worse with MergeSources4. Ideally we only have one SRTP-based overload for each number of arguments, but this doesn't work with tasks, and I wonder if there is any clever hack to fix it (so Task.GetAwaiter() always chooses generic version over non-generic in case of contention). Another option to leave it as is and face additional allocations for 3+ cases. |
This would be a bigger task (no pun intended) since it would involve changing SRTP overload semantics. |
@TheAngryByrd You are absolutely right, it looks like I'm hitting #8794 bug, because this line and ^Awaiter1: (member GetResult: unit -> ^TResult1) really should prevent compiler from trying to infer |
I think I'll leave it as is, only add some some documentation pieces. Once/if the aforementioned bug is fixed we can add @vzarytovskii I'm not following this comment
Can't find any ilgen tests for the core library and tasks, I'm not sure they are required at all. |
They should be for the new feature if you want to make sure resumable code is getting generated. You can skip if you don't care of course. |
I don't know where any existing ones might be, either, but if there is no existing place, this seems like as good a spot as any. Either that, or you could add a new folder following a similar structure. |
I don't want them to be more resumable than |
As I said, if you don't care and team agrees on no codegen tests, no point in writing them. |
1bb0016
to
a0f9631
Compare
I don't think we need exact IL verification tests. What would be good though would be a one-time evidence of the allocation footprint for the newly created variants (e.g. for 2,3 and 10 sources) - can be just a benchmarking result attached here to this PR, IMO it does not need to keep living in the codebase after that. |
[<MemoryDiagnoser>]
type Merge() =
let allTasks = [|
Task.FromResult(1)
Task.FromResult(2)
Task.FromResult(3)
Task.FromResult(4)
Task.FromResult(5)
Task.FromResult(6)
Task.FromResult(7)
Task.FromResult(8)
|]
[<Benchmark>]
member this.Baseline2() =
task {
let! a = allTasks[0]
let! b = allTasks[1]
return a + b
}
[<Benchmark>]
member this.Applicatives2() =
task {
let! a = allTasks[0]
and! b = allTasks[1]
return a + b
}
[<Benchmark>]
member this.Baseline8() =
task {
let! a = allTasks[0]
let! b = allTasks[1]
let! c = allTasks[2]
let! d = allTasks[3]
let! e = allTasks[4]
let! f = allTasks[5]
let! g = allTasks[6]
let! h = allTasks[7]
return a + b + c + d + e + f + g + h
}
[<Benchmark>]
member this.Applicatives8() =
task {
let! a = allTasks[0]
and! b = allTasks[1]
and! c = allTasks[2]
and! d = allTasks[3]
and! e = allTasks[4]
and! f = allTasks[5]
and! g = allTasks[6]
and! h = allTasks[7]
return a + b + c + d + e + f + g + h
} |
CI seems to be failing with error |
is there any difference if you use a |
@TheAngryByrd Wow, I didn't know it was possible! CE docs didn't mention that you can do that. However allocations gets improved just a bit, probably because task is allocated on each
|
This should help: https://github.com/dotnet/fsharp/blob/main/DEVGUIDE.md#updating-fcs-surface-area-baselines |
As for reducing allocations, I've found that the best option is to introduce BindN method, for example member inline this.Bind3(task1: Task<^TResult1>, task2: Task<^TResult2>, task3: Task<^TResult3>, continuation) =
this.Bind(task1, fun (result1: ^TResult1) ->
this.Bind(task2, fun (result2: ^TResult2) ->
this.Bind(task3, fun (result3: ^TResult3) ->
continuation struct(result1, result2, result3)
)
)
) But introducing it breaks compilation for three |
I've run this locally, however only one file
|
Nevermind, updated those files manually, since the suggested script didn't work |
Ah, compiler should've been built in both configs probably. Devguide should be updated. |
@Lanayx : Is this ready for review now? Given the SRTP limitation, the allocations should be accepted and implementation reworked later once the compiler can do a general mechanism to support any combination of Task and ValueTask for N=3,4. |
The other option to support |
@T-Gro Yes, this is ready for review. One thing to note, when/if SRTP bug is fixed, it will be possible to change current implementation to @TheAngryByrd implementing |
Co-authored-by: Jimmy Byrd <[email protected]>
This looks good. Could anyone please file new examples going into https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/task-expressions (as NET10 addition) ? |
* Consolidated two `SynExpr.LetOrUseBang` patterns with `isUse = true` (#18472) * `and!` support in TaskBulder (#18451) * Update package Category (#18479) * Update dependencies from https://github.com/dotnet/source-build-reference-packages build 20250423.3 (#18494) Microsoft.SourceBuild.Intermediate.source-build-reference-packages From Version 9.0.0-alpha.1.25209.1 -> To Version 9.0.0-alpha.1.25223.3 Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> --------- Co-authored-by: Edgar Gonzalez <[email protected]> Co-authored-by: Vladimir Shchur <[email protected]> Co-authored-by: Matt Mitchell <[email protected]> Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Description
Implementation for a suggestion
Checklist
Test cases added
Performance benchmarks added in case of performance changes
Release notes entry updated:
Comments
This could be a pretty straightforward implementation with just one method, but there is an issue with type inference of
Task<>
cases. If I remove overloads withTask<>
argument, then I face an error in the simple case:Current overloads do help, but this is a hack for 2 arguments cases. For 3 and 4 arguments I need to create all possible permutations of arguments with tasks in different positions, which obviously is not desired. In theory, current PR can be used as is, but without MergeSources3 and MergeSources4 overloads we'll face additional allocations in those cases. I wonder, is it possible to fight type inference error without introducing combinatorial explosion giving up on reducing allocations in 3+ cases?
CC @TheAngryByrd @T-Gro