Javascript FP design pattern: Binding functions
(a.k.a, Monads!)
Ok, ok, monads have been done. They’ve even been done by me before. But, functional programming is still popular, monads are still very useful for programming in an FP style, and people are entering the field every day. So here’s another attempt at an approachable introduction to usefully apply this pattern to everyday coding problems.
(Also I was dicking around a bit and thought I’d get some use out of my dumb experiments)
Functional Javascript Design Pattern: Binding functions
In OO, design patterns are an attempt to codify some good habits for object-oriented software architectures. Following design patterns (rather than re-inventing them) allows software to be more easily understood in terms of these patterns.
Most OO design patterns are obviated by programming in a functional style. But there are a few common patterns that most software written in FP style will follow. You may, for example, have heard of Railway Oriented Programming. ROP is in fact a specific case of a more general pattern, which I’ll be calling the binding function pattern.
What I’ll be describing is in essence a Monad pattern, but with a bit of extra wiggle room. Haskell enthusiasts [who write Javascript monad tutorials] usually insist on using some wrapper value, and I think this is a mistake. Javascript’s dynamism allows us cut some corners (so to speak). This lack of technical correctness is why I’ve decided to use a different naming convention.
The rules of binding functions are simple:
- Binding functions accept a wrapped value and a bindable function.
- Bindable functions accept a single value and return a wrapped value.
- Wrapped values can be just about any value with some semantic meaning. What consitutes a “wrapped value” depends on the binding function used.
By writing functions that follow these rules, we gain access to a reusable pattern that we can then apply to all sorts of situations.
Take, for example, the “Maybe” monad binding function:
const bindMaybe = (mx, f) => {
if(mx !== null && mx !== undefined) {
return f(mx)
}else{
return null
}
}
Here, the “wrapped” value (mx) is either some value or nothing (null or undefined). We can use this function to connect take any number of “bindable” functions – in this case, a function that accepts a non-null value and returns a nullable one – and safely combine them, “piping” a value through them without having to worry about null values.
Pipeline-oriented programming
The easiest way to make use of binding functions is to add a helper that allows us to compose a series of bindable functions into one:
const bindWith = (bindFn, fns) => (val) => fns.reduce(bindFn, val)
(Hint: This function, given a list of
functions fns = [fn1, fn2, fn3]
,
combines them as if you wrote bindFn(bindFn(bindFn(val, fn1), fn2), fn3)
)
Here, we’ve created a function that accepts a binding function and a list of unary (i.e. one-argument) functions to “thread” together via that binding function. It returns a function, which we can either assign a name to or call immediately. Here it is in action:
bindWith(bindMaybe, [
(x) => x + "1",
(y) => null,
(z) => z + "3"
])('0') // => null (not "null3")
Here, I’ve used some very short and pointless intermediary functions, but these could be any unary (one-argument) function that returns a nullable value.
Error Handling
If you’ve ever seen a monad tutorial before, you know that the Error
monad is up next. Here, I’ve again diverged from the Haskell standard
by simply treating anything that’s not an instance of Error
(a javascript
builtin) as an ok value, and also catching and wrapping any errors
that happen to be thrown while I run the function:
const bindError = (mx, f) => {
if (mx instanceof Error) {
return mx
}
try {
return f(mx)
} catch (e) {
if (e instanceof Error) {
return e
}
return Error(e)
}
}
bindWith(bindError, [
(x) => x + "1",
(y) => {
throw('Failed.')
},
(z) => z + "3"
])('0') // => Error('Failed.')
Note here that the return value is either the result, or an error – you’ll
want to run the instanceof Error
check before using it. (Or, if you don’t
need to do anything with the error, just put everything inside the
bindWith chain).
This, again, is the focus of Railway Oriented Programming, which I urge you to read.
Inserting asynchonity
Finally, we’ll try an Async monad, which is very nearly done for us with javascript promises:
const bindPromise = (mx, f) => mx.then(f)
This is good time to mention the other essential monad function, return
(sometimes called “unit”). Prior to this, our values have been simple javascript
values with some special handling for certain cases, but in this version
Promise.resolve
serves as a return that wraps its argument in a Promise.
We’ll redefine that to be clearer (note that if Promise.resolve was a regular
function this could just be returnPromise = Promise.resolve
)
const returnPromise = (x) => Promise.resolve(x)
Now we’ll use it. Remember the signature that our pipeline functions need to adhere to: (x) => M x. So, our binding functions must return Promises:
bindWith(bindPromise, [
(x) => returnPromise(x + "1"),
(y) => returnPromise(y + "2"),
(z) => returnPromise(z + "3")
])(returnPromise("0")) // => Promise("0123")
However, to make this less verbose, you can use async functions (which return promises) instead:
bindWith(bindPromise, [
async (x) => x + "1",
async (y) => y + "2",
async (z) => z + "3",
])(returnPromise("0")) // => Promise("0123")
Combining bindings
Since we’re working with functions, and functions compose, we should expect to be able to compose our binding functions somehow. And indeed we can!
const bindPromiseResult = (mx, f) => bindPromise((x) => bindError(f, x), mx)
bindWith(bindPromiseResult, [
(x) => returnPromise(x + "1"),
(y) => {
throw "Failed."
return returnPromise(y + "2")
},
(z) => {
console.log("I never run!")
return returnPromise(z + "3")
}
])(returnPromise("0")) // Promise(Error("Failed."))
(That said, you should probably just use the more standard Promise.catch for this)
Applying the binding pattern to data validation
So far we’ve just been re-implementing some classic monads, so here’s something you might actually use.
Suppose you have some data that you want to validate. Perhaps simply returning an error isn’t enough; perhaps you’d like to apply a series of validation functions and aggregate all the errors. You have a few options here, depending on how we prefer to write our validation functions, but here’s my implementation:
const bindValidator = (mx, f) => {
const {data, errors} = f(mx.data)
// Merge the errors
return {data, errors: {...mx.errors, ...errors}}
}
Here, our binding function expects an input with the shape:
{data: ..., errors: ...}
So, validators we write should accept the data only, but return a matching structure. I like this design because it allows us to both return keyed errors, and change the data without necessarily reporting an error.
Here’s a dumb validation suite that checks if an address is in Topeka, Illinois:
const validateCityIsTopeka = (data) => {
if (data.city === 'Topeka') {
return {data}
} else {
return {data, errors: {'city': 'Invalid city: ' + data.city}}
}
}
const validateStateIsIl = (data) => {
if (data.state === 'Illinois') {
return {data: {...data, state: 'IL'}}
} else if (data.state === 'IL') {
return {data}
} else {
return {data, errors: {'state': 'State not Illinois!'}}
}
}
const validateTopeka = bindWith(bindValidator, [
validateCityIsTopeka,
validateStateIsIl
])
Our validator can now be applied to (appropriately prepared) data, and is capable of returning many detailed errors:
validateTopeka({data: {city: 'Topeka', state: 'Illinois'}, errors: {}})
// {"data": {"city": "Topeka", "state": "IL"}, "errors": {}}
validateTopeka({data: {city: 'Chicago', state: 'IL'}})
// {"data":{"city":"Chicago","state":"IL"},
// "errors":{"city":"Invalid city: Chicago"}}
validateTopeka({data: {city: 'New York', state: 'New York'}})
// {"data": {"city": "New York", "state": "New York"},
// "errors": {"city": "Invalid city: New York", "state": "State not Illinois!"}}
For actual use you might want to add some extra validation to the bind function to help you track down validators which don’t conform to the spec, but really it’s robust enough to be production ready as-is – not despite being simple, but because of it.
That’s it for yet another monad tutorial. I hope you liked it! Feel free to snark about my crimes against category theory in the comments.