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.
In this gist I want to show my perspective and the solution that I am currently employing
by implementing the examples given in the post using PureScript and purescript-aff
.
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"
map
(aka <$>
) -- allows us to apply pure function to the (result of) Aff
action.
We have an Aff
action getRandomInt
and a pure function addFive
.
We can combine them, simply by using map
operator:
addFiveToRandomInt :: Aff _ Int
addFiveToRandomInt = map addFive getRandomInt
<<<
is just a function composition.
addTenToRandomInt :: Aff _ Int
addTenToRandomInt = map (addFive <<< addFive) getRandomInt
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
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 apply
(aka <*>
) 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 Par
helper:
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 areAff
actions 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
addFive
to the result ofgetRandomInt
, we have to explicitly usemap
combinator. 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 andAff
actions without falling into hell like:Promise.all([x, y, z]).then((ns) => Promise.resolve(ns[0] + ns[1] + ns[2])
Your code manipulates normal values and
Aff
actions (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-aff
is just a library. Nopurescript-aff
specific 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.