Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Aff vs. Promises vs. Eventual values.

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:

  1. "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).
  2. "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.

Pure functions

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

Aff actions

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"

Some Combinators

map

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
<<< (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 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

Parallelizing

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))

Do syntax

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)

Conclusion

  • Asynchronous actions (Aff) are explicit on the type level. It is statically known, which of your values are Aff 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 of getRandomInt, we have to explicitly use map 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 and Aff 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. No purescript-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment