[RFC FS-1087] Discussion thread for resumable-code RFC #455
Replies: 29 comments 8 replies
-
I don't see mention of this in the PR and the implementation is rather large. Are the well-known intrinsics public? I think for something this large they should stay internal only for a while to account for any churn. I'm skeptical that we'll "get it right" on the first go and won't have a desire to adjust them afterwards. |
Beta Was this translation helpful? Give feedback.
-
Yes they are. They really need to be for testing purposes - it's very difficult if they aren't. We should get the design right before it goes out of preview. What's our policy for FSHarp.Core surface area changes for preview-only features (assume the compiler intrinsics are marked "Experimental" or "only use in preview" or whatever we can do now). I'm ok with the whole thing being in preview a fair time (for example I'm as concerned about the use of SRTP in Tasks.fs as I am about the intrinsics - and that's also FSharp.Core surface area). I could sort of see us taking the "task" support out of preview first, though I'm not sure that gains us that much, as if the inlined code coming from FSharp.Core must conform to the resumable code spec. It just means that if there are corner cases in the resumable code implementation not exposed by Tasks.fs then we could adjust that at a later point. As an aside, I would like to use this for:
The first two cases give 4x speedup or more for some basic list/array constructions, see https://github.com/dotnet/fsharp/blob/92415de183daf8c6874a52b695d1da7ae6bb595e/BenchmarkDotNet.Artifacts/results/TaskPerf.ArrayBuilder-report-github.md for example I may also look if there's a way to reimplement a 100% compatible |
Beta Was this translation helpful? Give feedback.
-
This would require the following then, yes: [<Experimental("Experimental library feature, requires '--langversion:preview'")>] Every new construct in FSharp.Core that is a part of a preview language feature needs to be decorated with this (unless custom work is done in the compiler itself to only light it up recognition of that construct and error out appropriately). This allows us to make a breaking change if we need, since previews are subject to breaking changes. Once that attribute goes away and we ship something, it cannot change. |
Beta Was this translation helpful? Give feedback.
-
Regarding the faster The difference between But the F# computation expression list Consider
Then no |
Beta Was this translation helpful? Give feedback.
-
I would like to have this available, maybe in an "Unsafe Primitives" module. |
Beta Was this translation helpful? Give feedback.
-
Yes, it would be reasonable to add this to an Unsafe or Unchecked module. Indeed all the resumable state machine primitives could be similarly named. |
Beta Was this translation helpful? Give feedback.
-
With the caveat that it's protected with |
Beta Was this translation helpful? Give feedback.
-
@cartermp Yup, but aspects of the mechanism is basically unchecked, e.g. the |
Beta Was this translation helpful? Give feedback.
-
Regarding "Limitation - No asynchronous tailcalls", is emitting a warning considered? this is akin to another gotcha quoted from http://tomasp.net/blog/csharp-async-gotchas.aspx/
It would be great to have warnings for those. |
Beta Was this translation helpful? Give feedback.
-
Seeing "resumable Tasks" makes me think of the old Workflow Foundation. Would it be expected that the machine state could be pickled and later restored? |
Beta Was this translation helpful? Give feedback.
-
Semantically, I think there's nothing you can do with resumable tasks that you can't already do with existing workflows. Existing workflow states can be pickled with some care (you need to to private reflection over the workflow state and captured values, or else make the state explicit in monadic actions) It might in theory help to have the state organised in a way that more closely matches the structure of the code (one state machine object per "foo { ... }") but this can also make things tricker, e.g. there can be multiple not-yet-active fields for locals in the state machine. For these reasons I wouldn't recommend thinking of this as a semantic feature at all, but rather just a performance feature. I'll add that to the RFC. |
Beta Was this translation helpful? Give feedback.
-
BTW the best worked out framework for persisted workflows that I know of is the workflow part of the Angara framework, open sourced here: https://github.com/microsoft/Angara.Flow/blob/master/docs/content/Execution.md It's not under active development and the docs are unfortunately not properly posted, but perhaps we can assess, learn and maybe even fork and resurrect. |
Beta Was this translation helpful? Give feedback.
-
Good point on all of these. I'll make a note of them in the RFC |
Beta Was this translation helpful? Give feedback.
-
I have a design question. Is there any reason magic naming (e.g. |
Beta Was this translation helpful? Give feedback.
-
This is all still open for discussion. I'll add it to the RFC Basically, attributes aren't allowed on expression-locals in F# today, and I was looking for something sufficiently weird to highlight that this is a here-be-dragons compiler optimization feature. |
Beta Was this translation helpful? Give feedback.
-
What would the ideal representation of these features in your view be @dsyme? I could see some value is a section around 'considered-but-rejected', 'ideal-but-unimplementable', and 'compromise-current-state' syntax if there were language-restricted reasons you landed on this particular syntax. That might point towards future language features that could be fleshed out? |
Beta Was this translation helpful? Give feedback.
-
@dsyme There's a design suggestion open: fsharp/fslang-suggestions#848 (allow attributes on locals), which, if supported and implemented, may allow this new feature to be triggered with attributes as opposed to magical names. |
Beta Was this translation helpful? Give feedback.
-
I would far prefer attributes over bizarre keywords, mostly since there's already a design precedent around them being used to augment things (byref structs, readonly structs, struct DUs, struct records, etc.) |
Beta Was this translation helpful? Give feedback.
-
I don't even know if the CLR would allow attributes on expression locals. Allowing attributes on local functions isn't quite the same thing, since methods are acceptable targets for attributes today. Here is the closest C# language suggestion I could find: dotnet/csharplang#2478 . I think there might be incompatibility with the AttributeUsageAttribute which wouldn't be able to cover the expression local I do wonder if instead of using special names, some combination of attributes (on things that can have attributes) and types as annotation could be used. For example, in the case of Also there are some cases I was wondering if functions can be used to restrict usage rather than asking the programmer to adhere to a particular pattern. The case I'm thinking of here is if __useResumableStateMachines then
__resumableStateMachine
{ new SomeStateMachineType() with
member __.Step () =
<resumable code>
}
else
<dynamic-implementation> Is there any reason a function that itself takes two functions (one for the compilation case, the other for the dynamic case) can't be used to enforce this pattern? Or an abstract class/interface that has two methods, one for each case? To be fair, I have no idea what the implementation cost is to do it in that way vs the way it currently is. |
Beta Was this translation helpful? Give feedback.
-
Any of these would be ok-ish, they are all problematic. I think the if/then/else lends itself easily to
This is correct, I believe it's not allowed in the .NET metadata format which is why we don't support it for expression locals in F#. We could make an exception for these particular attributes in F# code but that feels as hacky as anything else. The current approach is definitely hacky though - I'm not denying that - and am open to suggestions. It has some advantages (as noted in the RFC) - I don't think we even want this feature to feel "part of the language", even less so than It's possible we could continue to decompose it into micro features like C# did with byrefs, span etc. But I think that would lead naturally to an explosion of complexity for little gain. For example, we could allow something like this as a first class language feature:
To be effective this construct would need to imply a local type definition |
Beta Was this translation helpful? Give feedback.
-
Will |
Beta Was this translation helpful? Give feedback.
-
@TIHan and I spent today talking through how to add checking for resumable code at IDE-time, and not just in the late phase of state machine generation. In particular this means we could reliably give a warning if code will not compile to a state machine, in the IDE, even in debug mode, and the emit of this warning won't depend on debug/release settings Here are our notes. Take a sample set of functions or methods which covers the cases we care about (the names are indicative, and each method can take other non-Resumable code parameters). [<return: ResumableCode>]
let Leaf(x: int) = ...
[<return: ResumableCode>]
let Composition([<ResumableCode>] __expand_code1, [<ResumableCode>] __expand_code2) = ...
// note no 'ResumableCode' on return
let Consume([<ResumableCode>] __expand_code1) = ...
No warning is given for an state machine which doesn't produce any useful resumable code: let f code =
{ new StateMachine() with
[<ResumableCode>]
member x.M() = code() }
} For example (in fragments) [<return: ResumableCode>]
member inline _.Zero() : TaskSeqCode<'T> =
(fun _sm -> TaskSeqStatus.DONE)
[<return: ResumableCode>]
member inline _.Combine([<ResumableCode>] __expand_task1: TaskSeqCode<'T>, [<ResumableCode>] __expand_task2: TaskSeqCode<'T>) : TaskSeqCode<'T> = ...
member inline _.Run([<ResumableCode>] __expand_code : TaskSeqCode<'T>) : IAsyncEnumerable<'T> =
let sm =
{ new TaskSeqStateMachine<'T>(-1) with
[<ResumableCode>]
member sm.Step () =
if __useResumableStateMachines then
//-- RESUMABLE CODE START
__resumeAt sm.ResumptionPoint
__expand_code sm
//-- RESUMABLE CODE END
else
let code = sm.ResumptionFunc sm
match code with
| TaskSeqStatus.AWAIT ->
sm.Awaiter.UnsafeOnCompleted(Action(fun () -> sm.MoveNext()))
| _ -> ()
code
}
if not __useResumableStateMachines then
sm.ResumptionFunc <- __expand_code
sm.Start()
[<return: ResumableCode>]
member inline _.Yield (v: 'T) : TaskSeqCode<'T> =
(fun sm ->
if __useResumableStateMachines then
match __resumableEntry() with
| Some contID ->
sm.ResumptionPoint <- contID
sm.Current <- ValueSome v
TaskSeqStatus.YIELD
| None ->
TaskSeqStatus.DONE
else
sm.ResumptionFunc <- (fun sm -> TaskSeqStatus.DONE)
sm.Current <- ValueSome v
TaskSeqStatus.YIELD) Given this
|
Beta Was this translation helpful? Give feedback.
-
The RFCs for resumable code and tasks have been separated out into two RFCs, though the implementation remains one PR. The implementation of both now satisfies all requirement
|
Beta Was this translation helpful? Give feedback.
-
I've added a significant unresolved question to the RFC about the potential for people to start to over-use resumable code for other non-resumable optimized code purposes, see https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1087-resumable-code.md#potential-for-over-use I'm experimenting with solutions to this now which tease apart the mechanisms a little further. |
Beta Was this translation helpful? Give feedback.
-
As mentioned above, I'm concerned that people will complicated resumable code to get high-performance synchronous code - indeed I've been doing that already with the I really want to get to to the heart of this problem. As a result I've done the following:
This is a much simpler mechanism to use and has many, many applications across FSharp.Core and elsewhere. @TIHan also pointed out that Kotlin makes this the default for function arguments on inlined functions (though we can't really do that by default because it can cause code explosion). Here are the builders implemented in the different ways:
The results are good, see table below. Here I will now validate the same for It's an open question if
|
Beta Was this translation helpful? Give feedback.
-
I've made a major set of updates to the RFC for resumable code https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1087-resumable-code.md. Please review The implementation of coroutines (essentially, Task without any async I/O) using resumable code has been particularly instructive, and will probably be the running example in the RFC. I'll tidy up the example further. https://github.com/dotnet/fsharp/pull/6811/files#diff-98653d141f298053882d239a60805d78b485fad19c14c830fe47cba0d8f755b4R1 |
Beta Was this translation helpful? Give feedback.
-
I did a trial re-implementation of F# async (imperfectly and only a subset of the API) using resumable code. You can take a look at the subset that's implemented by looking in the signature file
Recall how async differs from tasks:
Anyway the approximate reimplementation appears to run as fast as TaskBuilder for sync cases, and as fast as tasks for async cases. That makes it like 10-20x faster than the current F# async implementation. Stack traces etc. would be greatly improved to. However it's not a perfect reimplementation - there are no tailcalls nor cancellation checks yet - and perfect compat is probably impossible sadly, there are lots of subtleties. For example That said it should be good enough to allow an FSharp.Control.Async2 package that is a drop-in replacement for F# async for 99.9% compat. (The |
Beta Was this translation helpful? Give feedback.
-
There's a discussion going on about whether to have "affine tasks" (aka "context insenstive tasks"), or else have a The idea of |
Beta Was this translation helpful? Give feedback.
-
The link to FS-1087-resumable-code.md is dead, where did it go? |
Beta Was this translation helpful? Give feedback.
-
Discussion thread for F# RFC FS-1087 - Resumable code
Beta Was this translation helpful? Give feedback.
All reactions