Replies: 32 comments 1 reply
-
Note: keeping this here rather than language-suggestions for now. I threw together a naiive version here: module Async =
let map f computation =
async.Bind(computation, f >> async.Return)
let zip c1 c2 =
async {
let! x = Async.StartChild c1
let! y = Async.StartChild c2
let! x' = x
let! y' = y
return x', y'
}
type AsyncBuilder with
member _.BindReturn(x: Async<'T>, f) = Async.map f x
member _.MergeSources(x: Async<'T>, y: Async<'U>) = Async.zip x y I'm sure there is a far better implementation possible though. |
Beta Was this translation helpful? Give feedback.
-
I would naively expect something more along the following lines: let zip c1 c2 =
[| c1 ; c2 |]
|> Async.Parallel
|> Async.map (fun a -> a.[0], a.[1]) except of course this would incur an unbox to do it in a type-safe way; there are a number of awful ways to fix this, e.g. the following monstrosity: let zip (x : 'a Async) (y : 'b Async) : Async<'a * 'b> =
let mutable r1 = Unchecked.defaultof<'a>
let mutable r2 = Unchecked.defaultof<'b>
[|
async {
let! x = x
r1 <- x
return ()
}
async {
let! y = y
r2 <- y
return ()
}
|]
|> Async.Parallel
|> Async.Ignore
|> Async.map (fun () -> (r1, r2)) |
Beta Was this translation helpful? Give feedback.
-
Luckily, FSharp.Core internals are the right place for that sort of thing :) |
Beta Was this translation helpful? Give feedback.
-
Adding this would lead to implicit parallelism. |
Beta Was this translation helpful? Give feedback.
-
Not sure I understand why it's not a good idea. Since you can't depend on the values returned from the computations there's no real drawbacks to having |
Beta Was this translation helpful? Give feedback.
-
Yes, but you are talking from a technical viewpoint. I mean, you could also apply this reasoning to operation with arrays, why not doing some of them in parallel ? Just because we can do something, doesn't mean it's a good idea. Maybe an alternative is to create a different builder for this, call it In some Haskell-like languages where they tend to group operations in types to take advantage of type-classes, they had the same issue, so I think they created a different type in such a way that the parallelism is not implicit. So, although the solution is different, the problem is the same: implicit parallelism is evil :) |
Beta Was this translation helpful? Give feedback.
-
To use the etymology of the word, "async" just means "not at the same time", which is orthogonal to parallelism, concurrency, sequential operations, etc. We now have a language-level construct that disallows depending on the result of a previous computation (thereby implying sequential execution), so it's worth thinking through what a another model with |
Beta Was this translation helpful? Give feedback.
-
It seems that you read only my last sentence. Maybe the text in your browser was clipped ;) |
Beta Was this translation helpful? Give feedback.
-
Nope. You haven't given a reason why not to do this aside from saying it's not done for other things. What is your reasoning based on async programming with F# today? Are there scenarios you have in mind? |
Beta Was this translation helpful? Give feedback.
-
I can understand why one might not want it to be parallel by default. However, one of the main objections might be "what about side effects", and in my opinion it's also definitely wrong for users to be able to rely on a known evaluation order of |
Beta Was this translation helpful? Give feedback.
-
I can think of many scenarios where this would surprise me, but I was trying to give a more generic reason why. This is just another instance of the typical problem I have a monad, cool then I have an applicative, hey but it turns out there is another applicative possible which is not consistent with my monad but as applicative is more convenient, great let's use that instead So, this happens also with the:
Now, the async one is a bit special in the sense that, as opposed to the the above mentioned it won't change the result in a pure computation, but it will create the parallel side effect, so if our function is not pure, surprise, results are not the same. The interesting fact is that even in pure languages they decided to avoid that implicit parallelism, so here we would be being more pure than the purists. The equivalence between monad and applicatives is:
yeah, I know I'm sounding too cryptic here and you might wonder who cares about that math, also it was recommended not to use operators for applicatives. But it turns out that when using CEs inlieu of operators, this equivalence becomes way more evident:
We use Now in applicative, as I said before, the difference is not appreciable in pure code. We can actually simplify this discussion and turn it into the discussion "Is implicit parallel execution considered just an optimization?". Do I really need to give you specific scenarios for this? |
Beta Was this translation helpful? Give feedback.
-
Honestly I think you've just persuaded me that it should be parallel. The fact that we use different syntax ( I think this holds for the applicative |
Beta Was this translation helpful? Give feedback.
-
Yes, and let's make it also pointwise for So
See? You seem to start confusing monadic with a specific applicative. Monadic behavior is not describing sequential behavior. I can propose instad to create a different CE, let's say Why looking for shortcuts which leads to inconsistency, when you could do it properly? Just to save a few keystrokes? Does it worth it? |
Beta Was this translation helpful? Give feedback.
-
The seq {
let! foo = Seq.empty
return foo
}
I certainly don't feel confused. Constructing the The only possible places where the syntax of the computation expression is ambiguous between the applicative and the monad are: let a =
async {
return 5
}
let b =
async {
let! thing = Async.lift 5
return 5
} And fortunately the semantics of the monad and the applicative coincide in those two cases anyway. I don't see an argument against Since we have a different and incompatible syntax for applicative CEs, I just think it would be a waste if we didn't use it to express the different and incompatible applicative! |
Beta Was this translation helpful? Give feedback.
-
@Smaug123
What is obvious for you, might not be obvious for others, or even for you at a different time ;)
Yes, but it wouldn't allow me to use a future optimization of the (sequential) applicative.
Which one? There are 2?
This part I really don't get it. How is it incompatible? Let's not forget that all this started as mechanism for optimizations: #7756 (comment) "for more efficient computation expressions" but now we're discussing about changing the semantics. Of course Computation Expressions are kind of free, in the sense you can roll your own, and it could be not even a monad, but if we're talking about the Namely, in the Computation Expression Zoo paper, there is a law that states the expectation of equivalence between functors and monads, in monadic CEs. I'm not convinced of saving a few keystrokes with implicit behavior, that was not the original spirit of F#, to me that fits better in languages like C# where you have lot of unrelated overloads and implicitness, which makes impossible to reason and promotes either learning everything by heart, or work with the reference open all the time. Having said that, nothing stops you from developing your own "convenience-key strokes-saving-abstraction breaking-surprising" library. In this case it's just a few lines, you can just add the extensions methods you need for async or (even better) create your own, by inheriting the Builder, but let's don't do this in Core. |
Beta Was this translation helpful? Give feedback.
-
I want to comment this separately as it's more technical, so it's a bit unrelated to the reasons above explained. Actually, there might be a technical limitation of doing this. It states that if the Now, here's where things start to go wrong, if we implement some methods with parallel while having This is just a consequence of mixing different semantics, again, if you go back to the RFC, it clearly states the motivation was to create more efficient CEs, not different semantics. The feature is designed in such a way that you can incrementally add optimizations. But one more time, if we consider Parallel an optimization, I would say we should focus in that discussion instead (to me it's clearly not). |
Beta Was this translation helpful? Give feedback.
-
But that's not what will happen. If there is no corresponding |
Beta Was this translation helpful? Give feedback.
-
Could you give an example of an optimisation you could add to Edit: I guess actually there's an advantage in constructing the |
Beta Was this translation helpful? Give feedback.
-
Even if it's an a common thing to do Async.Parallell, will hidden usage of parallell break existing code? How easy is it to use the feature in an unintended way? Isn't this a thing that you could write a lib for (so that when you want to have async behave in that way you can use that lib instead)? |
Beta Was this translation helpful? Give feedback.
-
My assertion is that |
Beta Was this translation helpful? Give feedback.
-
Not at the moment, would need to dig into async internals, but I'm sure it's possible. Anyways, I think we exchanged already many ideas about the pro/cons of this proposal, from the theoretical point of view. I'm still convinced it's not a good idea, but leaving that feeling aside, in order to be able to implement an async-parallel applicative we would need to solve the problem of the zipMany function, which current implementation doesn't allow to express. With @baronfel we did an experiment to see how can we upgrade the compiler to support these kind of constructs. The result is here: #9605 it allows 2 ways of doing it, run-time and compile-time. For the compile-time flavor, it relies on another PR related to tuple type equivalence which is not approved at the moment, but we don't need it for the run-time one. |
Beta Was this translation helpful? Give feedback.
-
Objection! Your honour, the parallelism would be explicit due to the different keyword! |
Beta Was this translation helpful? Give feedback.
-
I agree with this the most. I can appreciate that some people might expect |
Beta Was this translation helpful? Give feedback.
-
Since |
Beta Was this translation helpful? Give feedback.
-
@gusty is correct - no implementation of As one litmus test, introducing a do-nothing
should be equivalent to
That is, I believe the accurate implementation that meets litmus tests such as these is something like this: module Async =
let map f computation =
async.Bind(computation, f >> async.Return)
let zip c1 c2 =
async {
let! ct = Async.CancellationToken
let! x = Async.StartImmediateAsTask (c1, cancellationToken=ct)
let! y = Async.StartImmediateAsTask (c2, cancellationToken=ct)
let! x' = Async.AwaitTask x
let! y' = Async.AwaitTask y
return x', y'
}
type AsyncBuilder with
member _.BindReturn(x: Async<'T>, f) = Async.map f x
member _.MergeSources(x: Async<'T>, y: Async<'U>) = Async.zip x y With this, given the code let! x = async1
and! y = async2
E The effect of this is to start async1 and run it immediately on the current thread up to its first async release, then do the same for async2, and then wait for the two results. Note that it's my understanding that let f c1 =
async {
let! x = c1
return x'
} and let f c1 =
async {
let! ct = Async.CancellationToken
let! x = Async.StartImmediateAsTask (c1, cancellationToken=ct)
let! x' = Async.AwaitTask x
return x'
} are essentilly equivalent (up to trampolining, and perhaps exceptions?) - if they differ then we must adjust the above implementation. |
Beta Was this translation helpful? Give feedback.
-
Parallel await on the completion is all I was after, pardon for stating it poorly. Would be happy with Don's implementation. |
Beta Was this translation helpful? Give feedback.
-
@dsyme This would still mean implicit parallelism, just moved past initial yield, which as a redeeming quality for this implementation has the necessary consistency with the monadic semantics; a consistent initial order of execution which is very important. As your given implementation is completely in line with Task's promise-style semantics this is convenient for me as TPL library author, but is it the right one for Async as well? I do know, and have experienced first hand, a very common mistake beginner Scala programmers make is one where they construct a list of Futures (essentially Scala's Task) and suddenly have 100s of work items in flight. The given implementation could end up encouraging very similar behavior, albeit somewhat more opaquely due to the necessary involvement of an applicative operation. It could make this a new failure mode for Async - very similar to many Tasks starting all at once - and generalizing over any applicative functor as @gusty would like to do with FSharpPlus makes this a 'dangerous' implementation. For Task there is no choice than to align to this particular applicative semantics but it's worth it to think carefully about the least surprising default for Async. The essential question is: would you under any circumstance want to allow a large number of work items in flight for a single Async applicative operation. For reference, Scala's default for ZIO/Cats/Scalaz/Monix IO types is sequential applicatives and Haskell IO is similar, all ecosystems have exceptions like
|
Beta Was this translation helpful? Give feedback.
-
To add, I personally don't have a strong preference, considering .NET's uniform and very well behaved threadpool I could certainly go both ways. Exploring our options and some prior art I merely hope helps us pick one we won't regret :) |
Beta Was this translation helpful? Give feedback.
-
The problem I see with the implementation is that if task 2 faults without task 1 completing, we will never bubble up that exception.
|
Beta Was this translation helpful? Give feedback.
-
For posterity, I implemented a parAsync CE in IcedTasks with benchmarks, so anyone can try alternative implementations. Also having it being a dedicated CE makes it "opt in". |
Beta Was this translation helpful? Give feedback.
-
Is your feature request related to a problem? Please describe.
With applicatives support landing in F# 5 I assumed we'll see FSharp core implementing the support in existing CEs, specifically
async
.Describe the solution you'd like
Since it hasn't been implemented yet I'd like to propose that the implementation executes child asyncs in parallel when
and!
is used.Describe alternatives you've considered
Implement sequentially.
Pros: the same semantics as
let!
- no surprises.Cons: Seems like it would be a missed opportunity to hide the boilerplate currently required to exec 2 child asyncs (of different results type) in parallel.
Beta Was this translation helpful? Give feedback.
All reactions