I recently read this interesting post about eventual values. One thing that struck me and which I failed to understand is:
- Eventual Values can be interacted with like normal values.
- If an Eventual Value is part of a simple value operation, then that expression resolves to a new Eventual > Value which resolves when all its Eventual Values are resolved.
If I understood the author correctly, that is supposed to be solving several problems:
- "I don’t know if this is a Promise or not" (I don't know if it's the resolved result of the action, or the action itself).
- "I’d really like to write code that interacts with values, and not Promises, and leave the machinery to the computer to work out".
I don't see how (1) can be solved by "[making values that aren't yet resolved] mostly indistinguishable from a “normal” value". I don't think (2) is possible to resolve unambigiously, often there would be multiple ways to execute a sequence of actions and only few of them would be 'correct' (match application logic).
I hope I didn't take any quotes out of the context here. To see a full picture please refer to the original post.
Please meet these pure functions. They have nothing to do with async:
addFive :: Int -> Int addFive x = x + 5 addThreeInts :: Int -> Int -> Int -> Int addThreeInts x y z = x + y + z
That's an async action (indicated by
Aff in the type signature).
It would get a random integer (as a string) from random.org
Then it would parse that string into an actual integer, and if the parsing
fails it would return 42. The used combinators are explained further.
getRandomInt :: Aff _ Int getRandomInt = map (fromMaybe 42 <<< Int.fromString <<< _.response) (Ajax.get url) where url = "https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain"
<$>) -- allows us to apply pure function to the (result of)
We have an
getRandomInt and a pure function
We can combine them, simply by using
addFiveToRandomInt :: Aff _ Int addFiveToRandomInt = map addFive getRandomInt
<<< (backward function composition)
<<< is just a function composition.
addTenToRandomInt :: Aff _ Int addTenToRandomInt = map (addFive <<< addFive) getRandomInt
Same but with operators
Using operators instead of normal functions, makes the thing more terse, but once you get used it reads like a piece of cake:
getRandomInt = fromMaybe 42 <<< Int.fromString <<< _.response <$> Ajax.get url
Or if you don't like reading right-to-left:
getRandomInt = Ajax.get url <#> _.response >>> Int.fromString >>> fromMaybe 42
Applying a pure function to multiple arguments
You can apply a pure function to multiple actions using
liftN family of functions:
sumOfThreeRandomInts :: Aff _ Int sumOfThreeRandomInts = lift3 addThreeInts getRandomInt getRandomInt getRandomInt
There is also
<*>) combinator. Using it together with
<$> allows to do the same thing as with
liftN, but scales to arbitrary amount of arguments and gives quite a nice pattern.
sumOfThreeRandomInts :: Aff _ Int sumOfThreeRandomInts = addThreeInts <$> getRandomInt <*> getRandomInt <*> getRandomInt
Note that even though that code is asynchronous (e.g.
Ajax.get won't block
the main thread),
Aff actions are sequential by default.
sumOfThreeRandomInts would perform three requests sequntially even though
they could be performed in parallel.
In order to parallelize those requests we need to use the
sumOfThreeRandomIntsPar :: Aff _ Int sumOfThreeRandomIntsPar = runPar (lift3 addThreeInts (Par getRandomInt) (Par getRandomInt) (Par getRandomInt))
There is also a handy
do syntax. For this simple example it would be
redundant since none of our actions depend on the result of the previous
actions. Just to show it off:
sumOfThreeRandomIntsUsingDo :: Aff _ Int sumOfThreeRandomIntsUsingDo = do x <- getRandomInt y <- getRandomInt z <- getRandomInt pure (addThreeInts x y z)
Asynchronous actions (
Aff) are explicit on the type level. It is statically known, which of your values are
Affactions and which are just normal pure values.
The problem of "I don’t know if this is a Promise or not" simply doesn't exist.
For example in order to
addFiveto the result of
getRandomInt, we have to explicitly use
mapcombinator. If we don't the compiler would complain:
-- Add 5 to the result of `getRandomInt` map addFive getRandomInt -- Add 5 to the `getRandomInt` action. Doesn't make any sense. addFive getRandomInt
Lots of well-defined combinators and
do-notation allows you to combine your pure values and
Affactions without falling into hell like:
Promise.all([x, y, z]).then((ns) => Promise.resolve(ns + ns + ns)
Your code manipulates normal values and
Affactions (which are just a special type of values). At the boundaries you have to explicitly tell how to interleave them together. Computer can't unambigiously work that out, but it can check whether your usage of machinery makes sense. The idea of working that out automatically sounds a bit like lazy evaluation, and experience of using lazy IO in Haskell makes me a bit skeptical about that.
purescript-affis just a library. No
purescript-affspecific magic is present in the compiler.
I would prefer things like that not to be a part of the language and I expect the language to provide the features that allow implementing such things in userland.