Skip to content
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

Possible incorporation into FSHarp.Core #27

Open
dsyme opened this issue Apr 25, 2019 · 9 comments
Open

Possible incorporation into FSHarp.Core #27

dsyme opened this issue Apr 25, 2019 · 9 comments

Comments

@dsyme
Copy link

dsyme commented Apr 25, 2019

I'm looking at an incorporation into FSHarp.Core, see dotnet/fsharp#6811

Please let me know what you think, please follow that thread and please look at the design carefully. At the moment it follows the same general pattern as TaskBuilder.fs and the implementation is based on it.

Ply is also relevant, https://github.com/crowded/ply/, and we need to do state machine generation as well

@dsyme
Copy link
Author

dsyme commented May 21, 2021

Just to say this work is now ready for review, I'd be really grateful if people experienced with using TaskBuilder.fs could read the RFCs and review the implementation dotnet/fsharp#6811

For example, should FSharp.Core come with things like Task.Ignore or other such APIs? Right now it's task { ... } and backgroundTask { ... }

@dsyme
Copy link
Author

dsyme commented May 21, 2021

Also to mention that I removed the use of complex SRTP successfully replaced by a set of method overloads, see https://github.com/dotnet/fsharp/blob/feature/tasks/src/fsharp/FSharp.Core/tasks.fsi

Also see the RFC section https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1097-task-builder.md#bind-returnfrom-support

@rspeele Do you recall if there was a specific reason to use the more complex form of SRTP, with multi-parameter witnesses, priorities etc.? Beyond the need to resolve method overloading order?

@rspeele
Copy link
Owner

rspeele commented May 21, 2021

Hey, I think how you have it now, using extension methods in modules opened highest-priority-last, is how TaskBuilder used to work back when I last significantly contributed to it. Something like this version.

TaskBuilder in this form was, unbeknownst to me, dependent on a subtle change to type inference introduced in F# 4.1. When others tried to use it with the F# 4.0 compiler they got type inference errors and ambiguity over which Bind or and ReturnFrom should be selected. @gusty swooped in to the rescue (thank you!) and added the SRTP magic to make it work as it does now, which plays nicely with both old and new versions of the compiler.

For purposes of a new addition to F#, compatibility with the old compiler might not be so relevant. As long as the type inference behavior stays consistent and keeps this simple module-priority extension method technique working, I personally think it's simpler and easier to understand.

This old issue and the ones linked from it is a good reference.

@gusty
Copy link
Contributor

gusty commented May 21, 2021

@dsyme Nice job !

I like explicit things, like Task.Ignore and I think would be great to be explicit when converting to/from async, as it is more aligned with the F# way of doing things, and less with C# ad-hoc overloads.

@dsyme
Copy link
Author

dsyme commented May 21, 2021

@rspeele Thank you so much for the information

@gusty What would be your design principles for a Task module/type in F#? Should it follow the naming and design of Async? Or just be a couple of methods like Ignore?

@gusty
Copy link
Contributor

gusty commented May 22, 2021

@dsyme that's a good question.

I understand the value of aligning with Async, but at the same time I think Async type is a bit special in the sense that all methods are Pascal case, because of the overloads, which require method instead of functions.

My personal preference is not to follow that design and align instead with the rest of the FSharp.Core modules. When there are cases which requires overloads we can define different function names. An alternative to that would be use overloads but with camel case, you'll be breaking the naming conventions, but actually this reveals a pain point in the naming conventions in that the casing expose what I consider implementation details: module function or type member.

And as I said before, I'm not a big fan of ad-hoc overloading as their reasoning/understanding relies more on tooling and there are situations where they don't compose well.

I saw your job with Async2 maybe that will be a good opportunity to align the Async module.

Here's F#+ Task module, it's really a tiny set of functions, not saying that it's complete, more functions can be added: https://github.com/fsprojects/FSharpPlus/blob/master/src/FSharpPlus/Extensions/Task.fs

Finally I insist in the importance of being explicit when switching from Async and Task, for different task-like awaitables it is ok as we can consider them a set of the same abstraction.

@dsyme
Copy link
Author

dsyme commented May 22, 2021

It's certainly an important design point to consider.

We never wrote out a rationale for PascalCasing of Async. Here are my recollections for the record

  1. Overloading was needed for FromBeginEnd and friends

  2. Many of the constructs related to things that were present in .NET and had .NET naming (AwaitWaitHandle).

  3. At the time, F# Async was seen as a compositional multi-threaded, "here be dragons" imperative programming mechanism that we wanted held at arms length from the "core" of data-oriented functional programming. (IEnumerable/Seq can also be be seen as an imperative programming mechanism but most commonly was used as a form of functional collection programming, and C# positioned it as such with LINQ).

  4. We wanted the majority of async code to be written using async { ... } for clarity and debuggability so there was no desire to add Async.map etc.

  5. There was an element of the historical: there was general pressure at the time around F#'s lowerCase naming violating .NET design conventions. These included formal requirements to "review" and Microsoft-signed .NET DLLs with the high-priests at Microsoft, whose approval was needed. This led to CompiledName, unfortunate in retrospect though not catastrophically so.

Of these points 1, 2, 3 and 4 still hold to some extent and also apply to task.

We did add IEvent.map and IObservable.map and so on, which is inconsistent with some of the points above, especially (3). However there were existing "sources" of events and observables and the programming we expected for these was intended to be very transient/ephemeral - unlike async which we saw as a replacement for multi-threaded programming.

It would be lovely if we could somehow move all of this out of FSharp.Core, or split/refactor FSharp.Core.

I notice FSharpPlus Async.fs as an alternative viewpoint, though there's not a lot there.

I'd imagine we won't act on this as part of RFC FS-1097, but we should open a language suggestion for future Task/Async modules or classes.

@dsyme
Copy link
Author

dsyme commented May 22, 2021

Finally I insist in the importance of being explicit when switching from Async and Task, for different task-like awaitables it is ok as we can consider them a set of the same abstraction.

For me I think it's pretty much ok to implicitly bind an Async in a task. It's essentially foregoing the multi-start and cancellation token passing of the Async

The other way around should be explicit with AwaitTask since the cancellation token is lost

@gusty
Copy link
Contributor

gusty commented May 22, 2021

@dsyme thanks for sharing your thoughts, very interesting.

We should certainly open a suggestion for re-organizing F# core.
In F#+ Async module has only some most-wanted functions, it is by no means a complete module at the moment. There are also Async extensions, which follows the Pascal case naming because they are methods. But I'm not convinced that's the best way to go.

Even if we manage to move async to a module and use camel case names, we should review F# design guidelines, as there it states that methods should be Pascal case, as I said sometimes it is just an implementation detail.

The other way around should be explicit with AwaitTask since the cancellation token is lost

That would be my preferred way. I am OK with overloads as long as they represent the same abstraction, but I consider Task-likes and Async two different abstractions as although they address the same problem, the models differ a lot.

Also think about this if task accepts implicitly async, the expectation is that the opposite should also be true and it's not. So, better to be consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants