This may seem a little strange, but althrough Javascript is a dynamic language, with very loose typing (automatic convertions, equals signs that only works on arrays/numbers/undefined/nil), lots of things that are “falsy” by default, with the new promise-based approach of Javascript, the language is borrowing some very interesting concepts from Haskell.
And yes, this is a great thing. And yes, this will probably change the way we program.
Let’s begin by talking about Javascript, and its new features. Old async-javascript code was probably like this:
some_io_function(function(result) { find_name_in_db(result.person_id, function(name) { console.log(name); }) });
Now, it’s like this:
some_io_function() .then((result) => find_name_in_db(result.person_id)) .then(console.log)
And, with new ES6 features:
async () => { var result = await some_io_function(); var name = await find_name_in_db(result.person_id); console.log(name) }
Now, what does this have to do with Haskell? Multiple things, but the most important: Functors!
The most simple explanation (and a little bit incorrect/incomplete) of a functor is some kind of “box” that “maps” values to another box. So, an Array is a functor: it is a “box” for “multiple values”, and by a function, we can “map” it’s contents to another “box”, or an Array. On Haskell, the most notable functor is IO
(that is also a monad, but this is a talk for another time). It’s kinda strange, but on Haskell, functions can have the IO
type: this means that if a function prints something, reads something, or in any way interacts with the outside world, it will have IO
type. Also, IO
is some of these “boxes”: it has a fmap
(Haskell’s equivalent of map
in other languages) that transforms an IO
of anything into another IO
of other thing. Let’s see it in practice: supose we want to upper-case a string. This means the code: fmap Data.Char.toUpper "some string"
. Now, we want the user to be able to read this string. So, we want a function that will map a “user interaction” to a upper-case string.
In Haskell, this is not possible: getLine
, the function that we use to read a string from the user, have type IO String
. It’s not possible to get rid of IO
in any way. What we can do is to, again, “map a box of something into another box of other thing”, so:
import Data.Char stringToUpper :: String -> String stringToUpper someString = fmap toUpper someString readUpperCase :: IO String readUpperCase = fmap stringToUpper getLine
Now, I’ve added type signatures so we can see which function will have each type. Now, what happened? getLine
will return a functor: an IO String
. We use fmap
, which has type Functor f => (a -> b) -> f a -> f b
, which means: “Given that f
is a Functor
, we expect a function that converts from some type a
into type b
. Then, when we pass a f a
, that is, a “box” of type f
containing a
data, we’ll return a box of the same type f
containing b
data. Now, onto concrete types, in our function, f
is a Functor, and it’s name is IO
. a
is a String. Our first parameter is a function that have type String -> String
, so our b
is a String
too. So, the “full” type of our fmap
, applied to our case, is Functor IO => (String -> String) -> IO String -> IO String
.
Now, this means that IO
functions will always be “separated” from the rest of our code: we can’t pass an IO String
when we expect just a String
, so we’ll need to use fmap
to extract it’s value and put it back. Now, a little detour:
Back to Javascript
Let’s re-implement our example with Javascript, using Node.JS:
const readline = require('readline'); // Some JS to read a line with promises const getLine = () => new Promise( resolve => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.resume(); rl.on('line', (line) => { rl.close(); resolve(line); }); }); const readUpperCase = () { getLine().then(e => e.toUpperCase()) }
Now, Javascript is not a statically typed language. But this doesn’t means we can’t give this function a type. I think all we agree that the type of this function is something like Promise String
. That means we expect no parameters, and we return a String, inside a Promise. As with Haskell, we never “exit” the Promise: it’s promisses all the way down, like in our IO
case.
Now, let’s make a code that will concatenate two strings, like a “given name” and a “surname”. In JS, we could do this:
const readline = require('readline'); const getLine = () => new Promise( resolve => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.resume(); rl.on('line', (line) => { rl.close(); resolve(line); }); }); console.log("Type your name") let givenName = getLine() console.log("Type your surname") let surname = getLine() givenName.then(gn => surname.then(sn => console.log(gn + " " + sn)))
But it would be wrong. First, because the getLine returns a promise. This means that the code is asynchronous. See what happens when I try to run this code on my machine:
Okay, so we need a way to synchronize our code. We can do it the old way:
console.log("Type your name") getLine().then(gn => { console.log("Type your surname") getLine().then(sn => { console.log(gn + " " + sn) }) })
Or, the new way:
(async () => { console.log("Type your name") let gn = await getLine() console.log("Type your surname") let sn = await getLine() console.log(gn + " " + sn) })()
Which is just a syntax sugar on the old way.
Diving into sugar syntax
The reason we need for this sugared syntax is not simply convenience: it is kinda difficult to synchronize all data that we need using callbacks (even considering that Javascript’s Promise
is flatten – there’s no way to return a promise inside another promise). By the way, Haskell does have a sugared syntax for its IO
(in truth, for all monad but this, again, is a subject for another time) operations too:
readName = do putStrLn "Digite seu nome" gn <- getLine putStrLn "Digite seu sobrenome" sn <- getLine putStrLn (gn ++ " " ++ sn)
That de-sugars to:
readNameNoSugar = putStrLn "Digite seu nome" >>= \_ -> getLine >>= \gn -> putStrLn "Digite seu sobrenome" >>= \_ -> getLine >>= \sn -> putStrLn (gn ++ " " ++ sn)
And I think that it is pretty cool.
2 Comments
Why I tend to avoid core.async? – Maurício Szabo · 2019-12-05 at 22:26
[…] Even if you use promises (await is only syntax sugar around .then(<callback>). More on this on another post), so things become really complicated really […]
There is NO SUCH THING as COLOR FUNCTIONS! – Maurício Szabo · 2024-07-31 at 21:39
[…] personal opinion is simple: Javascript is the first mainstream language to incorporate some Haskell ideas for people to use. This basically means, well, people are not aware that these different […]
Comments are closed.