A Function-oriented Dialect of JavaScript
In imperative programming, sequences of operations are generally constructed by operating on a state of some kind, which is read from and updated at each step. In OOP, this state would take the form of an object, and the available operations on the state would be the object's public methods.
In functional programming sequences of operations take the form of function composition - that is, passing the output of one function to the input of the next. Function composition is the bread-and-butter of functional programming.
/* This function composes two other functions.
We name it "o" instead of "compose" to imitate mathematical
notation, so the composition "g following f" is "g `o` f" */
o f g = \ ->
g # f.apply undefined arguments
One downside of this style is the frequent need to perform some repetitive task each time a function in the sequence returns. Suppose, for example, that we wish to allow our functions to log messages. Since we are avoiding side-effects, a cumulative log message will have to be passed to each function and then returned again, possibly with additions.
zip list1 list2 =
zipped = []
for index i in list1
zipped[i] = [list1[i], list2[i]]
zipped
zipLog list1 list2 msg =
[zipped, msg + 'zipped two lists']
/* ... other such pairs of functions ... */
bigOp input1 input2 input3 =
msg = ''
[listA, msg] = getListALog input1 input2 msg
[listB, msg] = getListBLog input2 input3 msg
[zipped, msg] = zipLog listA listB msg
/* etc. */
This is obviously bad style. We should not be requiring or depending on the user to implement log-appending behavior for each and every function in this chain. Here is a better design, if more complicated than what we need in this case. The utility of the additional complexity will become clear when we look at further applications of it.
// We require that our chained functions return a Logger object
Logger x msg =
this.x := x // the "return" value
this.msg := msg // message to append to the log
/* Chaining function.
'm' is a Logger
'f' is a function from m.x to Logger */
Logger.into m f ->
m2 = f m.x
new Logger m2.x (m.msg + m2.msg)
// wraps a value in a Logger
Logger.result x = new Logger x ''
// returns all the data we want from the Logger
Logger.run m = [m.x, m.msg]
zipLog list1 list2 = new Logger (zip list1 list2) 'zipped two lists'
/* ... */
bigOp input1 input2 input3 =
m1 = getListALog input1 input2
m2 = getListBLog input2 input3
zipped =
listA <- m1.into
listB <- m2.into
zipLog listA listB
/* ... */
Now the log-related logic is separated from the "normal" flow,
running parallel to it instead of being interspersed. Notice that
into
does not know what value is being returned - the value is
passed transparently to f
, just as in normal function
composition. Also notice that the chained functions do not know
what the cumulative log message is, or how the chaining is
implemented. The "return value" never depends on the log, or even
on the fact that the function is part of a Logger chain.
For comparison, here is vanilla function composition implemented with the same pattern.
Identity x =
this.x := x
Identity.into m f -> f m.x
// wraps a value in a Logger
Logger.result x = new Identity x
// returns all the data we want from the Logger
Logger.run m = m.x
If we have functions that do not need to do any logging, we can easily transform them into compatible functions.
loggerfy f = Logger.result `o` f
// Same as zipLog, but adds nothing to the log message
zipNoLog = loggerfy zip
We could do the same thing a different way, using a map-like
function, which we call here fmap
:
fmap m f = m.into (m.result `o` f)
This general pattern of customized function composition is
reusable. It is usually discussed using the awful, abstruse term
"monads." The pattern itself would, I guess, be called "monadic
programming," and a specific kind of composition is called
a "monad," singular. For example, Logger
is what is
traditionally called "the Writer monad." The word "monad" comes
from math; I don't know what it actually means, and it doesn't
matter what. Many an innocent programmer has wasted hours in the
vain hope of figuring out just what the hell it means. Don't
bother. It's customized function chaining, except in math, where
it's something else.
The important features of a monad are:
There is a small set of monads that see repeated use. We'll look
at a few here. First, we'll define some useful, reusable
functions for monads. We use the "trait" model here: the Monad
function adds the Monad trait to a constructor.
Monad = typeClass
{ result: undefined
, into: undefined
, run: undefined
/* "then" is for when you want the "effect" of m1 (the log
* message, for instance), but not the "value"
*/
, then: \type ->
\m1 m2 -> type.into m1 \_ -> type.result m1 m2
, fmap: \type ->
\m f -> type.into m # type.result `o` f
}
{ into: \m f -> m.into f
, run: \m arg -> m.run arg
, then: \m1 m2 -> m1.then m2
, fmap: \m f -> m.fmap f
}
{ into: \f -> f this @
, run: \f -> f this @
, then: \f -> f this @
, fmap: \f -> f this @
}
There are many other useful, generic functions on monads, but for now we'll keep it simple.
Notice that there is no Monad.result
function. This is because
there is no way to know which monad you want your data to be put
into, so you have to specify.
The List Monad covers functions that may return multiple values, each of which serves as an input for the next function. This might be used in, for example, a game, where many moves are possible on each turn, and we want to compute all possible games.
List xs =
this.xs := xs
Monad List
{ result: \x -> new List [x]
, run: \m -> m.xs
, into: \m f ->
out = []
for index i:v in m.xs
out = out.concat (f v).xs
new List out
}
// Adds all possible rolls of one die to a running total
addDie total =
newTotals = []
for i = 1; i <= 6; i++
newTotals.push (total + i)
new List newTotals
// Show the sum of every possible roll of two dice
console.log # List.result 0 .into addDie .into addDie .run()
// Show every name made of these possibilities
firsts = new List ['Fred', 'Mary', 'Stu']
middles = new List ['A.', 'Z.']
lasts = new List ['Rogers', 'Lynn', 'Baker']
names =
first <- firsts.into
middle <- middles.into
last <- lasts.into
List.result (first + ' ' + middle + ' ' + last)
console.log names.run()
Exceptions are a particularly hard-to-eliminate side effect. To
manage them in a functional-friendly manner, we must continually
surround calls with try
blocks. As an alternative, we can
replace error-throwing functions with error-returning ones, and
use the Error monad to handle bubbling and catching. First, we
define the Either
class. An Either object is a container that
either contains a valid value or an error message, and knows
which kind it has. We use this class as the basis for our Error
monad.
var left, right
(->
Either a b isRight =
this.a := a
this.b := b
this.isRight := isRight
left a := new Either undefined a false
right b := new Either undefined b true
)()
Monad Either
{ result: right
// if there's an error, skip the next function in the chain
, into: \m f -> ?? m.isRight : f m.b : m
, run: \m errHandler ->
if m.isRight
m.b
else
errHandler m.a
}
// f must return an Either
// 'a.recover f .into b' allows b to still run, even if a raises an error
Either.recover m f = ?? m.isRight : m : f m.a
Either.prototype.recover = Either.recover this @
safelyReadFile path =
try
right # fs.readFileSync path
catch ex
left ex
safelyWriteFile path content =
try
right # fs.writeFileSync path content
catch ex
left ex
safeBackup path =
safelyReadFile path .into \content ->
safelyWriteFile (path + '.bak') content
(I find the State monad difficult to understand, so I'm going to explain it in a way that may seem pedantic. Sorry.)
We can use the monad pattern to emulate statefulness in a functional way. This may seem silly, since the whole point of function-oriented programming is to avoid using state. It may even seem impossible by definition. Be assured that the State Monad use pure functions throughout, you'll see that it does so in a way that enables better control and testing of state-based operations.
In the monads we've looked at so far, bound functions are called
as soon as they are bound (i.e., as soon as we pass them to the
into()
function.) However, there is nothing about the monad
pattern that requires us to do things this way. Until run()
is
called, the data inside the monad objects is invisible, so we can
do things at any time. We make critical use of this fact in the
State monad.
We are calling the monadic class Stateful
here. A Stateful
object is a wrapper around a state function - a function which
takes the current state (an object) as input, and returns some
value, together with the new state. This state will then get fed
into the next state function in the chain. The tricky part is how
these state functions get threaded together. I tried drawing
a diagram, but found that to be even more confusing than just
showing the code, so here it is:
// f :: state -> [a, state]
Stateful f =
this.x := f
Monad Stateful
{
/* creates a state function that returns 'val'
* and leaves the state unchanged
*/
result: \val -> new Stateful \s -> [val, s]
, into: \m f -> new Stateful \s ->
// feed the current state into m
[val, s2] = m.x s
// feed the "value" of m into f, which returns a new Stateful object
m2 = f val
// feed the new state to the new Stateful object
m2.x s2
// run 'm' with 'state' as the initial state
, run: \m state -> m.x state
}
We now define our basic functions for getting and setting state variables.
// leaves the state unchanged, and passes the value for the given key
getVar k = new Stateful \s -> [s[k], s]
// creates a new state with the given key/value pair, and passes the value
setVar k v = new Stateful \s -> [v, set s k v]
Finally, here is an (admittedly contrived) example. Functions
ending in 'M' return monad objects. Note that setInitials
is
itself a monad object, not a function, though in a sense it acts
like one.
setNameM first last =
setFirst = setVar :first_name first
setLast = setVar :last_name last
setFirst.then setLast
setInitials =
first <- getVar :first_name .into
last <- getVar :last_name .into
setVar :initials (first[0] + last[0])
setAllM first last =
setNameM first last .then setInitials
foobar = setAllM 'Foo' 'Bar'
[_, finalState] = foobar.run {}
console.log finalState.initials // outputs "FB"
Using the State monad, we can make different pieces of state information available to different parts of our code, without having to expose it to the rest of the application, and without having to define "protected" members, or do any other such complicated things. And by keeping state information exclusively in an object, we make it easy - even trivial - to test that our state information is correct,
It is possible to create pathological monads that don't act like function composition.
CountChain x count =
this.x := x
this.count := count
Monad CountChain
{ result: \x -> new CountChain x 0
, into: \m f ->
m2 = f m.x
new CountChain m2.x (m.count + m2.count + 1)
, run: \m -> [m.x, m.count]
}
This code does something that, admittedly, no programmer would
ever need to do: it counts the number of times into
has been
called in this chain. There's a problem, though: when chaining
functions, chaining a function that does nothing should, well, do
nothing.
upcase = \word ->
CountChain.result word.toUpperCase()
chain1 = CountChain.result 'fwiw' .into upcase
chain2 = chain1.into Chaining.result // chaining function that does nothing
chain1.run() // == ['FWIW', 1]
chain2.run() // == ['FWIW', 2] uh oh!
CountChain fails to act like composition because is not a monad. We require monads to obey these "monad laws":
(result x).into f == f x
m.into result == m
(m.into \x -> f x).into g == m.into \x -> (f x).into g
The first two laws say that chaining a function that does nothing
should do nothing. The third law says it doesn't matter whether
you nest your chained functions or not. It's the monadic version
of (f `compose` g) `compose` h == f `compose` (g `compose` h)
The Continuation monad is a brain-buster. It chains functions via
continuation-passing style. One of its benefits is it doesn't
actually call any of the actual continuation-passing functions
until you call .run
. For simplicity, we're ignoring errors
for now.
Cont = \x ->
this.x := x
Monad Cont
{ result: \val ->
new Cont \g -> g val
, into: \m f ->
new Cont \g ->
m.x \val ->
(f val).x g
, run: \m g ->
m.x \val -> g val
}
readFile file = new Cont \g ->
(require 'fs').readFile file 'utf8' \err data ->
g data
writeFile file content = new Cont \g ->
(require 'fs').writeFile file content \err ->
g()
// open the file, then open the file whose path you find in that file
redirectedRead file = readFile file .into readFile
concatFiles x y z =
contents_x <- readFile x .into
contents_y <- readFile y .into
writeFile z (contents_x + contents_y)
actions = concatFiles 'a.txt' 'b.txt' 'a_and_b.txt'
.then redirectedRead '/path/to/file.txt'
actions.run \text -> console.log text
Continuation-passing style provides a way of dealing with the side effects of I/O. The strategy goes like this: all I/O functions are replaced (wrapped, actually) by ones that pass control to a user-provided callback when they finish. If we do things this way, and assuming we are otherwise side-effect-free, each callback is effectively like a new program, and the effects of the I/O are just the environment the "program" finds itself running in. The Cont monad can be used to make this continuation-passing style much more user-friendly.
Nesting functions indefinitely deep is a bad idea in most
languages, including JavaScript. Fortunately, we can avoid this
problem by using setTimeout
, which runs the given function in
a new call stack.
Monad Cont
{ result: \val ->
new Cont \g -> setTimeout (-> g val)
, into: \m f ->
new Cont \g ->
setTimeout \ -> m.x \val ->
setTimeout \ -> (f val).x g
, run: \m g ->
m.x \val -> setTimeout (-> g val)
}
As I'll explain in the next update, monads can be combined either by using "monad transformers," or by just incorporating the functionality of both into a single mega-monad. The curious can take a look at nodam, which combines the Cont, State, and Error monads for doing I/O in node.js. Thanks for reading!
Project maintained by Rob Rosenbaum Theme by mattgraham, modified by Rob Rosenbaum