4 ms·
> Yes - because it means existing code that expects a T cannot accept a Future<T> First of all, this is a programming language specific problem, not a general
by valenterry 6y ago
> Yes - because it means existing code that expects a T cannot accept a Future<T>
First of all, this is a programming language specific problem, not a general one. A good language allows you to write:
someFuture.map(someFunction)
Where "someFunction" accepts the plain value of type T that is stored within "someFuture".
Yes, you cannot write someFunction(someFuture), but that is a good thing, since asynchronous execution can very well change the behaviour of the program. Hence, I want the compiler (if there is one) to warn me if a variable that I pass to a function is now a Future instead of a plain value. I then map over the Future and I'm good. Or, maybe I want to wait for completion of the future and I'm good.
In both cases, there is no reason to write a second version of "someFunction".
Therefore...
> If you want to use the legacy code you have to force the future potentially before you really needed it.
... this is only true in incapable programming languages.
- chrisseaton 6y agoIsn't this the point I was making? You need support in the language to do this. If you're writing a library routine from scratch as Future<... with the compiler knowing nothing about how to handle it specially then you aren't getting a future as they were originally envisioned and you're missing out.
- valenterry 6y ago> Isn't this the point I was making? I don't think so - let me try to make it more clear, hold on. > You need support in the language to do this. If you're writing a library routine from scratch as Future<... with the compiler knowing nothing about how to handle it specially then you aren't getting a future as they were originally envisioned and you're missing out. You seem to assume that the programming language needs to know of the existence of Futures to help. But that is not the case and probably the reason for the misunderstanding. It is sufficient that the language supports generics/type-parameters as well as a lightweight syntax (such as in the example I gave) and optimally higher kinded types or a similar mechanism. If that is given, the language can be totally agnostic of Futures or async programming in general and it is easily possible to define a Future type as a library-type and use it in the way I described. Scala is a good example of such a language.
- chrisseaton 6y agoDo you mean the simple construct someFuture.map(someFunction) My understanding is that this only runs someFunction when the future has completed? This blocks the entire of someFunction until all the values are ready. You haven't adapted someFunction to work with futures at all - you're simply not running it until all the futures are gone and only normal values remain which you then pass into it! Imagine someFunction comes from a library so cannot be modified trivially, takes three values, and can run successfully with the first two completed values while the third completes. What would you write with map to do that?
- valenterry 6y agoYour 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.