3 ms·
Your understanding is correct! It is inevitable that someFunction can only be called once someFuture has completed successfully. No language or technology can c
by valenterry 6y ago
Your understanding is correct! It is inevitable that someFunction can only be called once someFuture has completed successfully. No language or technology can change that, but it is as much of a problem as the fact that in most languages lines are executed in sequence: pretty much no one thinks that this is a problem and I think we both agree on that.
So then...
> Imagine someFunction comes from a library takes three values, and can run successfully with the first two completed values while the third completes. What would you write in someFuture.map(...) to do that?
That is a good example actually. I believe that the user (at the callsite) should both _know_ if the library operates in an async way and/or runs code in parallel _and_ the user should be in control of choosing his desired behaviour.
Hence, with a library function that just expects 3 values, this is not possible and I would say the design isn't great.
Here is how I would write it in scala-ish pseudocode:
// library - two separate functions instead of one
def calcX(a, b): X
def calcY(x, c): Y
// One "combined" synchronous function can be created easily if desired:
combinedCalc = calcX.andThen(calcY)
// all given values are (lazy) futures
aFuture = future(...)
aFuture = future(...)
aFuture = future(...)
// callsite - async but _no parallel execution_
xFuture = (aFuture, bFuture).mapN(calcX) // This is actually valid Scala syntax. Returns a future
yFuture = (xFuture, cFuture).mapN(calcY)
// alternative Scala syntax
yFuture = for {
a <- aFuture
b <- bFuture
x <- calcX(a, b)
c <- cFuture
} yield calcY(x, c)
As you can see, I could also change the order:
yFuture = for {
a <- aFuture
c <- cFuture
b <- bFuture
x <- calcX(a, b)
} yield calcY(x, c)
This is relevant because I might want to control when things get executed - but I can b sure they run one after each other and nothing runs while something else has already failed or was already started.
How to run it in parallel?
xFuture = (aFuture, bFuture).parMapN(calcX)
yFuture = (xFuture, cFuture).parMapN(calcY)
Now everything runs in Parallel.
Sorry the wall of code, but it shows that the code can look rather short even without future or async specific syntax support from the language.
The important part here is that it allows me to have fine-grained control over the execution of my code. There are a lot of situations where I probably don't care and it might be nice to have a way to say "Hey compiler, I don't care, just run that code in the way you think it's best", similar to what an SQL planner does with my queries.
But often I want control. For instance, in your example: what happens if the library decides to always evaluate "c" eagerly even though "a" and "b" often fail and the whole calculation is aborted? That would be a waste of CPU. But worse: the library might change its behaviour any time and keep the same function signature.
While that might be not so bad for mere calculations, what if we start to think about actions? Such as database or file or network IO - or even in-memory transactions that should be atomic. How about _streams_ of these actions, such as running an action every second and then combining this with other streams of actions? Losing control over the fain grained execution will result in terrible hard to debug and fix bugs due to concurrency.
- chrisseaton 6y ago> It is inevitable that someFunction can only be called once someFuture has completed successfully. No language or technology can change that This is just not a true statement - and that was the point of the original thread - the original paper on futures had them implemented transparently and able to be passed down into library code not expecting futures. It's also how Haskell's par works today. So it demonstrably can be implemented. > I believe that the user (at the callsite) should both _know_ Ok but this is just a 'don't do that' argument. If a user tells us they have a good reason to do it, I'm interested in how we can enable them to do that. > with a library function that just expects 3 values, this is not possible But it is in Haskell using par, and other languages with more traditional futures. > Here is how I would write it Great... but you've rewritten the library code with 'two separate functions instead of one.' My original point was 'because it means existing code that expects a T cannot accept a Future<T>' and you're proving that back to me so hard that you've accepted you need to completely rewrite the code to do it! Languages with traditional futures would let you just call the original, unmodified library code, with future values instead of actual values, and it would just work. If you had library code like this, you could pass in future values of T for a, b, and c, and it would execute the expensive_operation on a and b as soon as it could, while c completes. def foo(T a, T b, T c) expensive_operation(a, b) + expensive_operation(b, c) end > For instance, in your example: what happens if the library decides to always evaluate "c" eagerly even though "a" and "b" often fail and the whole calculation is aborted? Yes this is a problem with transparent futures, as is the ordering issue you raised. But I wasn't arguing they didn't have their own issues. You're saying it's impossible and that is demonstrably false, because languages do do it. (I did half of my PhD on these topics, and the other half on Ruby.)
- valenterry 6y ago> This is just not a true statement - and that was the point of the original thread - the original paper on futures had them implemented transparently and able to be passed down into library code not expecting futures. It's also how Haskell's par works today. So it demonstrably can be implemented. Okay, technically you are right - however it changes the semantics of the program. I admit that I didn't mention this constraint though. > Great... but you've rewritten the library code with 'two separate functions instead of one.' My original point was 'because it means existing code that expects a T cannot accept a Future<T>' and you're proving that back to me so hard that you've accepted you need to completely rewrite the code to do it! > Yes this is a problem with transparent futures, as is the ordering issue you raised. But I wasn't arguing they didn't have their own issues. I don't disagree with any of that. However, what you originally said: > > Are you saying that generics on futures are bad? > Yes - because it means existing code that expects a T cannot accept a Future<T>, which is the colouring problem being discussed. And you imply that transparent futures are better than the ones with generics. So where we disagree is on what is worse. I am convinced that the problems (and there are certainly a lot of unknowns) you introduce with transparent futures is much worse than the performance you might gain in certain cases and the easier syntax in many cases.