Do not read the following text. Read this instead
IMPORTANT: these are my notes of "Mostly Adequate". So please go read the original content and not the following. The true author is a genius.
4 Paradigms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imperative | Declarative
(C) | (SQL)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Object Oriented | Functional
(Java, Python) | (Haskell, Clojure)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Imperative: instruction by instruction alter the global state
- Object Oriented: instruction by instruction alter self contained block states called objects
- Declarative: tell what you want instead of knowing how what you want is fetched
- Functional: ?
First Class Functions
First class functions refers to the fact that a function is like any other datatype, with nothing particularly special about them - they may be stored in arrays, passed around as function parameters assigned to variables and what have you.
Why favor first class
As we saw in the getServerStuff and BlogController examples, it is very easy to add layers of indirection that provide no added value and only increase the amount of redundant code to maintain and search through.
In addition, if such a needlessly wrapped function must be changed, we must also need to change our wrapper function as well.o
httpGet('/post/2', function (json) {
return renderPost(json);
});
If httpGet
were to change to send a possible err
, we would need to go back and change the "glue":
// go back to every httpGet call in the application and explicitly pass err along.
http('post/2', function (json, err) {
return renderPost(json, err);
});
had we written it as a first class function, much less would need to change:
// renderPost is called from within httpGet with however many arguments it wants
httpGet('post/2', renderPost);
besides the removal of unnecessary functions, we must name and reference arguments. Names are a bit of an issue, you see. We have potential misnomers - especially as the codebase ages and requirements change.
Having multiple names for the same concept is a common source of confusion. There is also the issue of generic code. For instance two functions do exactly the same thing but one feels infinitely more general and reusable:
// specific to our current blog
const validArticles = function (articles) {
return articles.filter(function (article) {
return article !== null && article !== undefined;
});
};
// vastly more relevant for future projects
var compact = function (xs) {
return xs.filter(function (x) {
return x !== null && x !== undefined
});
};
By using specific naming, we've seemingly tied ourselves to specific data (in this case articles
). This happens quite a bit and is a source of much reinvention.
Important: just like with OOP, you must be aware of this
coming to bite you in the jugular. If an underlying function uses this
and we call it first class, we are subject to this leaky abstraction's wrath:
import fs from 'fs';
// scary
fs.readFile('freaky_friday.txt', Db.save);
// less so
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
Having been bound to itself, the Db
is free to access its prototypical garbage code.
Important: avoid using this
like a dirty nappy. There is really no need when writing functional code. However when interfacing with other libraries, you might have to acquiesce to the mad world around us.
Node: some will argue that this
is necessary for optimizing speed. If you are the micro-optimization sort, please close this book. If you cannot get your money back, perhaps you can exchange it for something more fiddly.
Pure Functions
A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.
const xs = [1, 2, 3, 4, 5,]
// pure
xs.slice(0, 3); // [1, 2, 3, ]
xs.slice(0, 3); // [1, 2, 3, ]
xs.slice(0, 3); // [1, 2, 3, ]
// impure
xs.splice(0, 3); // [1, 2, 3, ]
xs.splice(0, 3); // [4, 5, ]
xs.splice(0, 3); // []
In functional programming we dislike unwieldy functions like splice
that mutate data. This will never do as we are striving for reliable functions that return the same result every time, not functions that leave a mess in their wake like splice
.
Here's another example
// impure
let minimum = 21;
// ...
// minimum = ...
// ...
const checkAge = age => age >= minimum;
// pure
const checkAge = function (age) {
const minimum = 21;
return age >= minimum;
};
In the impure portion, checkAge
depends on the mutable variable minimum
to determine the result. In other words, it depends on the system state which is disappointing because it increases the cognitive load by introducing an external environnement.
Important It might not seem like a lot in this example, but this reliance upon state is one of the largest contributors to system complexity.
Note: the impure checkAge
may return different results depending on the factors external to input, which not only disqualifies it from being pure, but also puts our minds through the ringer each time we're reasoning about the software.
The pure form is completely self sufficient. We can even make minimum
immutable, which preserves purity as the state will never change. To do this, we must create an object to freeze.
const immutableState = Object.freeze({
minimum: 21,
});
Side effects may include
What is the meaning of side effect mentioned in the definition of a pure function?
Effect: anything that occurs in our computation other than the calculation of a result.
There is nothing intrinsically bad about effects and we'll be using them all over the place in the chapters to come. It is the side part that bears the negative connotation.
A side effect is a change of system state or observable interaction with the outside world that occurs during the calculation of a result.
Side Effects may include, but are not limited to:
- changing the file system
- inserting a record into the database
- making an http call
- mutations
- printing to the screen / logging
- obtaining user input
- querying the DOM
- accessing system state
And the list goes on and on. Any interaction with the world outside of a function is a side effect, which is a fact that may prompt you to suspect the practicality of programming without them. The philosophy of functional programming postulates that side effects are a primary cause of incorrect behavior.
It is not that we're forbidden to use them, rather we want to contain them and run them in a contained way.
8th grade math
A function in math has the requirement that each input is mapped to one output. Though the output doesn't necessarily have to be unique per input.
In contrast, a mapping between an input to many outputs is not a function.
So valid functions are those that can be described as pairs with the position (input, output): [(1, 2), (3, 6), (5, 10)]
. (It appears the function doubles its input). Or expressed as a table, or as a graph etc. There is no need for implementation details when the input dictates the output. One could simply jot down object literals and run them with []
instead of {}
.
const toLowerCase = {
'A': 'a',
'B': 'b',
'C': 'c',
'D': 'd',
'E': 'e',
'F': 'f',
}
toLowerCase['C']; // 'c'
const isPrime = {
1: true,
2: true,
3: true,
4: false,
5: true,
6: false,
7: true,
8: false,
};
isPrime[3]; // true
Of course you might want to calculate instead of hand writing things out, but this illustrates a different way to think about functions. (You may be thinking what about functions with multiple arguments? Indeed, that presents a bit of an inconvenience when thinking in terms of mathematics. For now, now we can bundle them up in an array or just think of the arguments
object as the input. When we learn about currying, we'll se how we can directly model the mathematical definition of a function.)
Important pure functions are mathematical functions and they're what functional programming is all about. Programming with these little angels can provide huge benefits. Let's look a some reasons why we're willing to go to great lengths to preserve purity.
The case of purity
Cacheable
For starters pure functions can always be cached by input. This is typically done using a technique called memoization.
const squareNumber = memoize(function (x) {
return x * x;
});
squareNumber(4); // 16
// a new call with same input returns the cached result
squareNumber(4); // 16
squareNumber(5); // 25
// a new call with same input returns the cached result
squareNumber(5); // 25
Here is a simplified implementation of memoize
const memoize = function (f) {
const cache = {};
return function () {
const argStr = JSON.stringify(arguments);
cache[argStr] = cache[argStr] || f.apply(f, arguments);
return cache[argStr];
};
};
Important you can transform an impure function into pure ones by delaying evaluation:
const pureHttpCall = memoize(function (url, params) {
return function () {
return $.getJSON(url, params);
};
});
The interesting thing here is that we don't actually make the http call. We just return the function that will make it once called. So we don't cache the results, but rather the function that will query them. This is not very useful now but we'll soon make it so after we learn some tricks.
Portable / Self-Documenting
Pure functions are completely self contained. Everything the function needs is handed to it on a silver platter. Ponder this for a moment... How might this be beneficial? For starters a function's dependencies are explicit and therefore easier to see and understand.
// impure
const saveUser = function (attrs) {
const user = Db.save(attrs)
// ...
};
const welcomeUser = function (user) {
Email(user, ...);
};
const signUp = function (attrs) {
const user = saveUser(attrs);
welcomeUser(user);
// ...
};
// pure
const saveUser = function (Db, attrs) {
// ...
};
const welcomeUser = function (Email, user) {
// ...
};
const signUp = function (Db, Email, attrs) {
return function () {
const user = saveUser(Db, attrs);
welcomeUser(Email, user);
}
};
The example above demonstrates that the pure function must be honest about its dependencies and, as such, tell us exactly what it's up to.
We'll learn how to make functions like this pure without merely deferring evaluation, but the point must be made that the pure form is much more informative than the sneaky impure counterpart.
Something else to notice is that we are forced to inject our dependencies, or pass them as arguments, which makes our app much more flexible because we've parametrized our database or mail client, or whatever you. Should we choose to use a different Db we need only to call our function with it.
In a JavaScript setting, portability could mean serializing and sending functions over a socket. It could mean running all our app code in web workers. Portability is a powerful trait.
When was the last time you copied a method into a new app? A favorite quote about OOP from Joe Armstrong the creator of Erlang:
The problem with OO languages is they've got all this implicit environment that they carry around with them. YOu wanted a banana, but what you got was a gorilla holding the banana... and the jungle.
Testable
Pure functions are much more testable. We do not have to mock a real payment gateway or setup and assert the state of the world after each test. We simply give the function input and assert output.
In fact we find the functional community pioneering new test tools that can blast our functions with general input and assert that properties hold on the output. Search for Quickcheck
Reasonable
Many believe the biggest win when working with pure functions is referential transparency. A spot of code is referentially transparent when it can be substituted for its evaluated value without changing the behavior of the program.
import Immutable from 'immutable';
const decrementHP = function (player) {
return player.set('hp', player.get('hp') -1);
};
const isSameTeam = function (player, player2) {
return player.get('team') === player2.get('team');
};
const punch = function (player, target) {
return isSameTeam(player, target)
? target
: decrementHP(target);
};
const john = Immutable.Map({
name: 'John',
hp: 20,
team: 'red',
});
const edward = Immutable.Map({
name: 'Edward',
hp: 20,
team: 'blue',
});
punch(john, edward);
decrementHP
, isSameTeam
, punch
are all pure functions. Mainly thanks to package Immutable
which provides methods to players such that set('propName', value)
will return a different object, without changing the original.
Since they are pure, every statement can be replaced by its deeper equivalent:
Step 1:
const punch = function (player, target) {
return player.get('team') === target.get('team')
? target
: target.set('hp', target.get('hp') -1);
};
Step 2:
const punch = function (player, target) {
return 'red' === 'blue'
? target
: target.set('hp', target.get('hp') -1);
};
Step 3:
const punch = function (player, target) {
return target.set('hp', target.get('hp') -1);
};
This ability to reason about code is terrific for refactoring and understanding code in general. We'll use referential transparency a lot.
Parallel Code
Here's the "coup de grace", we can run any pure function in parallel, since it does not need access to shared memory and it cannot, by definition have a race condition due to some side effect.
This is very much possible in a server side js environment with threads as well as in the browser with web workers. Though the current culture seems to avoid it due to complexity when dealing with impure functions.
Currying
Normal multi parameter function to curried function:
// multi parameter function
const multiParamAdd = function (x, y) {
return x + y;
};
// curried function
const curriedAdd = function (x) {
return function (y) {
return x + y;
};
};
const sumRes = multiParamAdd(10, 4); // 14
const sumRes2 = multiParamAdd(10, 2); // 12
const sumRes3 = curriedAdd(10)(2); // 12
const sum10To = curriedAdd(10); // function (y) { return 10 + y; }
sum10To(4); // 14
sum10To(2); // 12
Here are a few curried functions :
import curry from 'lodash/curry';
const match = curry(function (what, str) {
return str.match(what);
});
const whiteSpaces = match(/ /g);
const replace = curry(function (what, replacement, str) {
return str.replace(what, replacement);
});
const filter = curry(function (f, arr) {
return arr.filter(f);
});
const map = curry(function (f, arr) {
return arr.map(f);
});
Here is a possible implementation of curry
:
// f(x) => f(x)
// f(x, y) => (f(x))(y)
// f(x, y, z) => ((f(x))(y))(z)
function curry(f) {
const arity = f.length; // get the initial f args length
return function innerCurry(...args) { // here we get the args of when it is called. It will be called explicitly by the user using single args or more and each of these args is `accumulated` through the `bind(null, ...args)` which is called every innerCurry is called by the user with an additional arg. Since we are returning the innerCurry bound to null and the additional args, when it is called again, the previous args are passed as heading arguments.
if (args.length < arity) { // if called with less args length as original
return innerCurry.bind(null, ...args); // add the passed args as initial arguments
}
// Therefore when called enough times such that the arity is equalized, we simply do the actual call with all the arguments, that are implicitly passed to the bound function.
return f.call(null, ...args);
};
};
If you do not have curry
you can also do explicit curry implementations with arrow functions, with the slight difference that they will only accept a predefined sequence of single arguments.
const match = what => str => str.match(what);
const matchAlNum = match(/[a-zA-Z0-9]/g);
matchAlNum("Hey this is a text that will get matched");
Partial Application
Currying is useful for many things, we can make new functions by passing single arguments like above: matchAlNum
.
We can also transform any function that works on single elements into a function that works on arrays simply by wrapping it with our curried map
function:
const map = curry(function (f, arr) {
return arr.map(f);
});
const getChildren = x => x.child;
const allTheChildren = map(getChildren);
Partial application through curried calls, removes a lot of boilerplate. A standard implementation without all the help of the curried map
would be:
import standardMap from 'lodash/map'
const map = standardMap;
const getChildren = x => x.child;
const allTheChildren = elements => map(elements, getChildren);
Which is much more verbose.
NOTE: Typically we do not define functions that work on arrays. We can simply call map
, sort
, filter
, and other higher order functions inline.
NOTE: When we talked about pure function, we said they take 1 input to 1 output, and currying does exactly that: each single argument returns a new function expecting the remaining arguments. No matter if the output is another function - it qualifies as pure. We do allow more than one argument at a time, but this is seen as merely removing the extra ()
, as in: match(/r/g, 'heyr')
is equivalent to match(/r/g)('heyr')
.
Higher Order Functions
Higher order functions are functions that take a function as an input or return a function as an output.
Composition
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
// or old style
// This is not the correct solution, but we will improve it to come up with the right function
const composeStep1 = function (...fns) {
return function (...args) {
return fns.reduceRight(function (res, fn) {
return fn.call(null, res);
}, args); // args is the initial value which is passed as _res_ to the first call to the reduce right callback
};
}
// since _args_ is an array, and it is passed as _res_, we need to unpack it
const composeStep2 = function (...fns) {
return function (...args) {
return fns.reduceRight(function (res, fn) {
return fn.call(null, ...res); // unpack _res_
}, args);
};
}
// since _...res_ is now expected to be an iterable, and on the second callback call of reduceRight, it will contain the return of the first callback call, we need to make sure to return an iterable
const composeStep3 = function (...fns) {
return function (...args) {
return fns.reduceRight(function (res, fn) {
return [fn.call(null, ...res)]; // make sure every return value is an array since it will be unpacked in _res_
}, args);
};
}
// since we are wrapping _res_ in an array, and _res_ is what will ultimately be returned, we need to remove the wrapping from the last return of reduceRight
const compose = function (...fns) {
return function (...args) {
return fns.reduceRight(function (res, fn) {
return [fn.call(null, ...res)]; //wrapped res
}, args)[0];// unwrapping res from return
};
}
Functional Husbandry
Instead of thinking about the general version of compose
which can take any amount of functions and compose them, let's consider a variadic implementation.
const compose2 = (f, g) => x => f(g(x));
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose2(exclaim, toUpperCase);
shout("This is amazing"); // THIS IS AMAZING!
The execution is from left to right like the following
const shout = x => exclaim(toUpperCase(x));
So order of execution matters, see for example
const reverse = x => x.reduceRight((res, curr) => res.concat([curr]), []);
// or from left to right
const reverse = x => x.reduce((res, curr) => [curr].concat(res), []);
const head = x => x[0];
const last = compose(head, reverse);
last([1,2,3,4]); // 4
const lastBAD = compose(reverse, head);
lastBAD([1, 2, 3, 4]); // Uncaught TypeError: x.reduceRight is not a function
Associativity works for composition
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);
// same result
compose(toUpperCase, compose(head, reverse))
compose(compose(toUpperCase, head), reverse)
Point-Free Style
// with points
const snakeCase = word => word.toLowerCase().replace(/s+/ig, '_');
// pointfree
const snakeCase = compose(replace(/\s+/ig, '_', toLowerCase);
See how we partially applied replace
? What we are doing is piping our data through each function of 1 argument. Currying allows us to prepare each function to just take its data, operate on it, and pass it along. Something else to notice is how we don't need the data to construct our function in the pointfree version, whereas in the pointful one, we must have our word available before anything else.
Remove argument passthrough. Not completely, but confine it's usage to very well tested functions like not
:
Here is another example:
// with points
const initials = name => name.split(' ').map(compose(toUpperCase, head).join('. '));
// pointfree
const initials = compose(intercalate('. '), map(compose(toUppercase, head)), split(' '));
initials('hunter stockton thompson'); // 'H. S. T'
Pointfree code can help us remove needless names and keep us concise and generic. Pointfree lets us know we've got small functions that take input to output. One can't compose a while loop, for instance.
Warning: pointfree is double-edged sword and can sometimes obfuscate intention. Node: not all functional code is pointfree and that is ok. We'll shoot for it when we can and stick with normal functions otherwise.
const not = function (fn) {
return function (...args) {
return !fn(...args);
};
};
So instead of writing
const isOdd = function (n) {
return n % 2 === 1;
}
const isEven = function (n) { // notice the argument passthrough
return !isOdd(n);
}
Write
const isEven = not(isOdd);
The not
function uses a point style, but it allows removing it from other functions, so it is a gain in point-free style code overall. And the not
is such an obvious and tested method that it's makes sense to allow it. So Point-Free style coding does not mandate to completely remove arguments passthroughs, but it pushes you to only use them in meaningful places, where they truly bring an added value like in not
.
Category Theory
Category theory is an abstract branch of mathematics that can formalize concepts from several different branches, such as set theory, type theory, groupe theory and more.
It primarily deals with objects, morphisms, and transformations, which mirrors programming quite closely.
In category theory, weh have something called a category. It is defined as a collection with tht following components:
- A collection of objects
- A collection of morphisms
- A notion of composition on the morphisms
- A distinguished morphism called identity
Category theory is abstract enough to model many things, but let's apply this to types and functions.
A Collection of Objects; the objects will be the data types. For instance: String
, Boolean
, Number
, Object
, etc. We often view data types as sets of all the possible values. One could look at Boolean
as the set of [true, false]
and Number
as the set of all possible numeric values. Treating types as sets is useful because we ca use set theory to work with them.
A Collection of Morphisms; the morphisms will be our standard every day pure functions.
A notion of Composition on the morphisms; this, as you may have guessed, is our brand new toy - compose
. We've discussed that our compose
function is associative which is no coincidence as it is a property that must hold for any composition in category theory.
Here is how composition works on collections of objects using morphisms:
g: String -> Int
f: Int -> Boolean
And the composition works like so:
g o f : String -> Boolean
Here is a concrete example
const g = x => x.length;
const f = x => x === 4;
const gof = compose(g, f);
A distinguished morphism called identity; this function simply takes some input and returns it as output.
const id = x => x;
So what the hell is identity useful for?? Well, think of it as a function that can stand in for our value - a function masquerading every day data.
id
must play nicely with compose. Here's a property that always holds for every unary (unary: one argument function f):
compose(id, f) === compose(f, id) = f;
Soon we will be using id
all over the place. A function that acts as a stand in for a given value. It will be quite useful when writing pointfree code.
So there we have it, a category of types and functions. As of now, what you need to retain is that it provides us with some wisdom regarding composition - namely, associativity and identity properties.
What are other categories? Well, we can define one for:
- directed graphs with: nodes being objects, edges being morphisms and composition just being path concatenation
- or : numbers as objects, and
>=
as morphisms (actually any partial or total order can be a category).
There are heaps of categories, but we will only look at the one defined previously.
Note: Composition connects our functions together like a series of pipes. Data will flow through our application as it must - pure functions are input to output after all, so breaking this chain would disregard output, rendering our software useless.
IMPORTANT: we hold Composition as a design principle above all others. This is because it keeps our app simple and reasonable. Category they will play a big part in app architecture, modeling side effects and ensuring correctness.
Examples of refactoring
For the following Car shape:
{
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}
Note that all "magic" functions are considered to be defined previously.
Ex 1
// isLastInStock :: [Car] -> Boolean
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};
Refactored:
const isLastInStock = compose(prop('in_stock'), last);
Ex 2
// averageDollarValue :: [Car] -> Int
const averageDollarValue = (cars) => {
const dollarValues = map(c => c.dollar_value, cars);
return average(dollarValues);
};
Refactored:
const averageDollarValue = compose(average, map(prop('dollar_value')));
Ex 3
// fastestCar :: [Car] -> String
const fastestCar = (cars) => {
const sorted = sortBy(car => car.horsepower, cars);
const fastest = last(sorted);
return concat(fastest.name, ' is the fastest');
};
const fastestCar = compose(
append(' is the fastest'),
prop('name'),
last,
sortBy(prop('horsepower'))
);
Declarative Coding
We'll switch our mindset. From here on out, we'll stop telling the computer how to do its job, and instead write a specification of what we'd like as a result. Which is much less stressful than trying to micromanage everything all the time.
Declarative, as opposed to imperative, means that we will write expressions, as opposed to step by step instructions. It's what we do in SQL. When the engine is optimized and updated, our queries still work.
Here is an example of the difference between imperative and declarative:
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
makes.push(cars[i].make);
}
// declarative
const makes = cars.map(car => car.make);
Se how the declarative version specifies what and not how. Now, the map function can be optimized and our code will work properly.
// imperative
const authenticate = (form) => {
const user = toUser(form);
return logIn(user);
};
// declarative
const authenticate = compose(logIn, toUser);
Thought there is nothing necessarily wrong with the imperative version there is still an encoded step-by-step evaluation baked in. The compose
expression simply states a fact: Authentication is the composition of toUser
and logIn
. This leaves room for support code changes and results in our application code being high level specification.
In the case above the order of execution matters: toUser
needs to be called before logIn
, but there are many scenarios where the order does not matter. And this is easily specified with declarative coding (more on this later).
Becaus we don't have to encode the order of evaluation, declarative coding tends itself to parallel computing. This coupled with pure functions is why FP is a good option for the parallel future - we don't really need to do anything special to achieve parallel/concurrent systems.
A Flicker of Functional Programming
See actual code in /workspace/js/tests/ch6/index.html
Map Composition rule
Look at the code below and see how we are calling map more times than we should: we are calling it to extract the media urls and then again to generate an img tag for each.
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);
Let's line up the code to make it more clear what we are saying:
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), compose(map(mediaUrl), prop('items')));
First we can use composition associativity:
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));
And then this map composition law:
compose(map(f), map(g)) === map(compose(f, g));
Which gives us:
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));
This way we only iterate once.
Hindley-Milner
// reduce :: ((b, a) -> b) -> b -> [a] -> b
const reduce = curry((f, x, xs) => xs.reduce(f, x));
See the notation of the function type above? That is what we call Hindley-Milner.
An important note here is that when we use variable types like a
and b
, we cannot make any assumption about the actual types. Therefore we need to treat those as if they could be any
type. In the reduce above, we only know that the 3rd curry param is an Array
of any
.
// reverse :: [a] -> [a]
What can reverse possibly do above? Can it sort the contents of the array? No, it does not have enough information on the type of a
to know how to sort it. Can it re-arrange? Yes, it can, but it has to do so in exactly the same predictable way every time. It may also decide to remove or duplicate an element. The point is, the possible behavior is massively narrowed by its polymorphic type.
Important: this narrowing possibility allows us to use type signature engines like Hoogle to find a function we are after.
Free as in Theorem
Besides deducing implementation possibilities, this sort of reasoning gains us free theorems. Here are a few random ones:
// head :: [a] -> a
compose(f, head) === compose(head, map(f));
// aka, map is redundant if we are going to get the head later, we might as well skip it
// filter :: (a -> Bool) -> [a] -> [a]
compose(map(f), filter(compose(p, f))) === compose(filter(p), map(f));
You don't need any code to get these theorems, they follow directly from the types. The first one says that if we get the head
of our array, then run some function f
it is the same and incidentally much faster than map(f)
and then take the head
.
The filter
theorem is similar. It says that if we compose f
and p
to check which should be filtered, then actually apply the f
on the filtered via map
(remember filter
will not transform the elements, its signature does not allow to touch a
), it will always be equivalent to mapping our f
the filtering the result with the p
predicate.
Important: these are just two examples, but you can apply this reasoning to any polymorphic type signature and it will always hold. In javascript, there are some tools available to declare rewrite rules. One might also do this via the compose
function itself. The fruit is low hanging and the possibilities are endless.
Constraints
We can also constrain types to an interface:
// sort :: Ord a => [a] => [a]
To the left side of a
we state that a
must be an Ord
or implement the interface Ord
. This restricts the set of possible types a
, so we call it a type constraint.
// assertEqual :: (Eq a, Show a) => a -> a -> Assertion
Here we have two constraints: Eq
and Show
, Those will ensure that we can check equality of our a
and print the difference if they are not equal.
Container
A container's purpose is to hold a value, and that value should not be accessed. It could through a.$value
but that defeats the purpose.
class Container {
constructor(x) {
this.$value = x;
}
static of(x) {
return new Container(x);
}
}
Container.of(3);
Container.of(Container.of({something: [1,2,3]}))
Functors
Once our value, whatever it may be, is in the container, we'll need a way to run a function on it.
// (a -> b) -> Container a -> Container b
Container.prototype.map = function(f) {
return Container.of(f(this.$value));
};
It's like the Array.prototype.map
but on Container
s. So:
Container.of(2).map(two => two + 2);
// Container(4)
Container.of('flamethrowers').map(s => s.toUpperCase());
// Container('FLAMETHROWERS')
Container.of('bombs').map(append(' away')).map(prop('length'))
// Container(10)
We can work our value without ever having to leave a Container
. Our value in the Container
is handed to the map
function so we can fuss with it and afterward, returned to its Container
for safe keeping. As a result of never leaving the Container
, we can continue to map
away, running functions as we please. We can even change the type as we go along (like in the last example).
Important: if we keep calling map
, it appears to be some sort of composition! That's what we call Functors:
A Functor is a type that implements
map
and obeys some laws
A Functor is an interface with a contract. We could just as easily named it Mappable, but now, where's the fun in that? Functors come from Category theory.
Important: what is the point of bottling up our value and using map
to get at it? The answer comes with a better question: what do we gain from asking our container to apply functions for us? Well, abstraction of function application. When we map
a function, we ask the container type to run it for us.
Schrodinger's Maybe
Container
is boring and usually called Identity
(it has about the same impact as our id
function). There is a mathematical connection we'll look at when the time is right.
There are other functors, that is, container-like types that have a proper map
function, which can provide useful behavior whilst mapping:
class Maybe {
static of(x) {
return new Maybe(x);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
constructor(x) {
this.$value = x;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
}
(Maybe.of('Malkovich Malkovich').map(match(/a/ig))).inspect();
// Just(True)
(Maybe.of(null).map(match(/a/ig))).inspect();
// Nothing
(Maybe.of({ name: 'Boris', }).map(prop('age')).map(add(10))a).inspect();
// Nothing
(Maybe.of({ name: 'Dinah', age: 34}).map(prop('age')).map(add(10))).inspect();
// Just(44)
Now Maybe
looks like the Container
with one minor change: it will first check to see if it has a value before calling the supplied function. This will have the effect of side stepping those pesky null
s as we map
(This is a simplified implementation).
Important: notice how our app does not explode with errors as we map functions over our null values.
Important: this dot syntax is perfectly fine and functional, but for reasons mentioned in Part 1, we'd like to maintain our pointfree style. As it happens, map
is fully equipped to delegate to whatever function it receives:
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f))
This is delightful as we can carry on with composition per usual and map
will work as expected. This is the case with ramda's map
as well. We'll use dot notation when it's instructive and the pointfree version when it's convenient.
Note: see the extra notation Functor f =>
instructs that f
must be a Functor.
Use Cases
In the wild we'll typically use Maybe
in functions which might fail to return a result.
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);
// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));
streetName({ addresses: [], });
// Maybe(undefined).inspect();
// Nothing
streetName({ addresses: [{ street: 'Shady in.', number: 4201 }], })
// Maybe('Shade in.').inspect();
// Nothing
Important: notice how it is safeHead
that introduces the Maybe.of()
in the streetName
composed chain. Prior to that, prop('addresses')
deals with the direct object. But then it forces us to use map
in order to get prop('street')
because the object is canned in a Maybe
. Without the map
we would get an undefined
since there is no Maybe.street
prop.
Important ...continued: by wrapping with a Maybe
we safeguard against null
but if we do not use the map
, we still get null
but from a different source... So how do you deal with this?
Sometimes a function might return a Nothing
explicitly to signal a failure:
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) => (
Maybe.of(balance >= amount
? { balance: balance - amount }
: null
)
));
// This function is hypothetical, not implemented here... nor anywhere else
// updateLedger :: Account -> Account
const updateLedger = account => account
// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;
// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);
// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20))
getTwenty({ balance: 200.00 });
// Just('Your balance is $180')
getTwenty({ balance: 19.00 });
// Nothing
withdraw
will return Nothing
if we are short on cash. This function also communicates its fickleness and leaves us no choice, but to map
everything afterwards. The difference is that null
was intentional here. Instead of a Just('..')
, we get a Nothing
back to signal failure and our application effectively halts in its tracks. This is important to note: if the withdraw
fails, then map
will sever the rest of our computation since it doesn't ever run the mapped functions, namely FinishTransaction
. This is precisely the intended behavior as we'd prefer not to update our ledger or show a new balance if we hadn't successfully withdrawn funds.
Releasing the Value
"If a program has no observable effect, does it ever run?". Does it run correctly for its own satisfaction? I suspect it merely burns some cycles and goes back to sleep...
Our application's job is to: retrieve, transform and carry that data along until it's time to say goodbye and the function which does so may be mapped, thus the value needn't leave the warm womb of its container.
Indeed, a common error is to try to remove the value from our Maybe
one way or another as if the possible value inside will suddenly materialize and all will be forgiven. We must understand it may be a branch of code where our value is not around to live up to its destiny. Much like Schrodinger's cat, is in two states at once and should maintain that fact until the final function. This gives our code a linear flow despite the logical branching.
There is however, an escape hatch. If we would rather return a custom value and continue on, we can use a little helper called maybe
:
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
if (m.isNothing) {
return v;
}
return f(m.$value);
});
// getTwenty :: Account -> String
const getTwenty = compose(maybe("You're broke!", finishTransaction), withdraw(20));
getTwenty({ balance: 200.00 })
// "You're balance is $180.00"
getTwenty({ balance: 10.00 })
// "You're broke!"
We will now either return a static value (of the same type that finishTransaction
returns in this call return f(m.$value)
) or continue on with the first argument to maybe
(here "You're broke!"
).
The the imperative equivalences of maybe
and map
are :
maybe
:if/else
statementmap
:if (x !== null) { return f(x) }
The introduction of Maybe
can cause some initial discomfort. When pushed to deal with null
checks all the time (and there are times we know with absolute certainty the value exists), most people can't help but feel it's a tad laborious. But you want to use it because writing unsafe software is like building a retirement home with materials warned against by the three little pigs.
The "real" implementation of Maybe
into two types:
- One for something:
Some(x)
orJust(x)
- another for nothing:
None
orNothing
... instead of a single Maybe
that does null
checks on its value.
Pure Error Handling
throw/catch
is not very pure. Instead of returning a value, we sound alarms!
Our new friend Either
, can do better than declare a war on input, it can respond with a polite message:
class Either {
static of(x) {
return new Right(x);
}
constructor(x) {
this.$value = x;
}
}
class Left extends Either {
map(f) {
return this;
}
inspect() {
return `Left(${inspect(this.$value)})`;
}
}
class Right extends Either {
map(f) {
return Either.of(f(this.$value));
}
inspect() {
return `Right(${inspect(this.$value)})`;
}
}
Right
and Left
are two subclasses of an abstract type we call Either
. We've skipped the ceremony of creating the Either
superclass because we will never use it, but it's good to be aware. There is nothing new here besides the two types.
And here's a method to specifically create an instance of Left
:
const left = x => new Left(x);
Now let's use them, and note that Either.of
will create a new instance of Right
:
Either.of('rain').map(str => `b${str}`);
// Right('brain')
left('rain').map(str => `It's gonna ${str}`);
// Left('rain')
Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')
left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')
What Left
does for us, is that it will ignore our request to map
and return itself instead. But the power will come from the ability to embed an error message within the Left
.
const moment = require('moment');
// getAge :: Date -> User -> Either(String, Number);
const getAge = curry((now, user) => {
const birthDate = moment(user.birthDate, 'YYYY-MM-DD');
return birthDate.isValid()
? Either.of(now.diff(birthDate, 'years'))
: left('Birth date could not be parsed');
});
getAge(moment(), { birthDate: '2011-01-02' });
// Right(9)
getAge(moment(), { birthDate: 'July 4, 2001'});
// Left('Birth date could not be parsed')
Now, just like Nothing
, we are short-circuiting our app when we return a Left
. The difference, is now we have a clue as to why our program has derailed.
Note: something to notice is that we return Either(String, Number)
, which holds a String
as its left value and a Number
as its right one. This type signature is a bit informal as we haven't taken the time to define an actual Either
superclass, however we learn a lot from the type: it informs us that we're either getting an error message or the age back.
// fortune :: Number -> String
const fortune = compose(append('If you survive, you will be '), toString, add(1));
// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()))
zoltar({ birthDAte: '2005-12-12' });
// 'If you survive you will be 15'
// Right(undefined)
zoltar({ birthDAte: 'ballons!' });
// Left('Birth date could not be parsed');
When our program fails, we are handed a Left('..') with an error message instead of really throwing an error and screaming like a child when something went wrong.
In this example we are logically branching our control flow depending on the validity of the birth date, yet it reads as a one linear motion from right to left rather than climbing through the curly braces of a conditional statement. Usually, we'd move the console.log
out of the zoltar
function and map
it at the time of calling, but it's helpful to see how the Right
branch differs. We use _
in the right branch's type signature to indicate it's a value that should be ignored (In some browsers you have to use console.log.bind(console)
to use it first class).
Lifting
Let's point out something we may have missed: fortune
, despite its use with Either
in the example, is completely ignorant of any functors milling about. This was also the case with finishTransaction
in the previous example. At the time of calling, a function can be surrounded by map
, which transforms it from a non-functory function to a functory one, in informal terms. This process is called lifting. Here is map
as a reminder:
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f))
Functions tend to be better off working with normal data types rather than container types. Then lifted into the right container as deemed necessary. This leads to simpler, more reusable functions that can be altered to work with any functor on demand.
Either
is great for:
- casual errors like validation
- more serious ones like stop the show errors: missing files or broken sockets
We've done Either
a disservice by introducing it as merely a container for error messages. It captures logical disjunction (a.k.a ||
) in a type. It also encodes the idea of a Coproduct from category theory, which won't be touched on this book, but is well worth reading up on as there's properties to be exploited. It is the canonical sum type (or disjoint union of sets) because its amount of possible inhabitants is the sum of the two contained types. There are many things Either
can be, but as a functor, it is used for its error handling.
Just like with Maybe
, we have little either
, which behaves similarly, but takes two functions instead of one and a static value. Each function should return the same type:
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
let result;
switch (e.constructor) {
case Left:
result = f(e.$value);
break;
case Right:
result = g(e.$value);
break;
// no default
}
return result;
});
// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));
zoltar({ birthDate: '2005-10-12'});
// 'If you survive, you will be 10'
// undefined
zoltar({ birthDate: 'balloons!'});
// 'Birth date could not be parsed'
// undefined
Finally a use of the mysterious id
function. It simply parrots back the value in the Left
to pass the error message to console.log
.
We've made our fortune telling app more robust by enforcing error handling from within getAge
. We either show a truth to the user, or carry on with our process.
Old McDonald Had Effects
We are ready to move on to an entirely different type of functor.
In the chapter with purity we saw a peculiar example of a pure function. This function contained a side-effect, but we dubbed it pure by wrapping its action in another function. Here is another example of this:
// getFromStorage :: String -> _ -> String
const getFromStorage = key => _ => localStorage[key];
Had we not surrounded it with another function, getFromStorage
would vary its output depending on external circumstances. With the wrapping though, we always return a function that when called, will retrieve a particular item from localStorage
.
But this does not seem very useful, it's like a toy in its original packaging, we can not actually play with it.
If only there were a way to reach inside the container and get at its contents...
Enter IO
:
class IO {
static of(x) {
return new IO(_ => x);
}
constructor(fn) {
this.$value = fn;
}
map(fn) {
return new IO(compose(fn, this.$value))
}
inspect() {
return `IO(${inspect(this.$value)})`;
}
}
IO
differs from the previous functors in that the $value
is always a function. We don't think of its $value
as a function, however - that is an implementation detail and we best ignore it. What is happening is exactly what we saw with the getFromStorage
example: IO
delays the impure action by capturing the return value of the wrapped action and not the wrapper itself. This is apparent in the of
function: we have an IO(x)
, the IO(_ => x)
is just necessary to avoid evaluation.
Note to simplify reading, we'll show the hypothetical value contained in the IO
as result; however in practice, you can't tell what this value is unitll you've actually unleashed the effects!
Let's see it in use:
// ioWindow :: IO Window
const ioWindow = new IO(_ => window);
ioWindow.map(win => win.innerWidth);
// IO(1430) - remember in reality we have not executed the function yet, just showing the actual result of calling for practicality but in reality it is { $value: [Function] }
ioWindow
.map(prop('location'))
.map(prop('href'))
.map(prop('/'))
// IO(['http:', '', 'localhost:8000', 'blog', 'posts']) - same as above, we actually have the function not the value
// $ :: String -> IO [DOM]
const $ = selector => new IO(_ => document.querySelectorAll(selector));
$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')
Here, ioWindow
is an actual IO
that we can map
over straight away, whereas $
is a function that returns an IO
after it's called. I've written out the conceptual return values to better express the IO
, though in reality it will always be { $value: [Function] }
.
When we map over our IO
, we stick that function at the end of a composition which, in turn, becomes the new $value
and so on. Our mapped functions do not run, they get tacked on the end of a computation we're building up, function by function, like carefully placing dominoes that we don't dare tip over.
Note the result is reminiscent of Gang of Four's command pattern or a queue.
Now, we've caged the beast, but we'll still have to set it free at some point. Mapping over our IO
has built up a mighty impure computation and running it is surely going to disturb the peace.
So where and when can we pull the trigger?
Is it still even possible to run our IO
and still wear white at our wedding?
The answer is yes, if we put the onus on the calling code.
Our pure code, despite the nefarious plotting and scheming, maintains its innocence and it's the caller who gets burdened with the responsibility of actually running the effects. Let's see an example of this:
// url :: IO String
const url = new IO(_ => window.location.href);
// toPairs :: String -> [[String]]
const toPairs = compose(map(split('=')), map(split('&')));
// params :: String -> [[String]]
const params = compose(toPairs, last, split('?'));
// findParam :: String -> IO Maybe [String]
const findParam = key => map(
compose(
Maybe.of,
find(compose(eq(key), head)),
params
),
url
);
// -- Impure calling code -----------------------------
// run it by calling $value();
findParam('searchTerm').$value();
// Just(['searchTerm', 'wafflehouse'])
For sake of clarity we should rename $value
to something more explicit about the potential catastrophic effects, like: unsafePerformIO()
Asynchronous Tasks
Callbacks are the narrowing spiral staircase to hell. A nicer way to deal with them is using the Task
class. Here is an example implementation.
class Task {
constructor(fork) {
this.fork = fork;
}
// ----- Pointed (Task a)
static of(x) {
return new Task((_, resolve) => resolve(x));
}
// ----- Functor (Task a)
map(fn) {
return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn)));
}
}
We will use: Folktale
's Data.Task
(previously Data.Future
) here's an example:
const fs = require('fs');
// readFile :: String -> Task Error String
const readFile = filename => new Task((reject, result) => {
fs.readFile(filename, (err, data) => (err
? reject(err)
: result(data)
));
});
readFile('metamorphosis').map(split('\n')).map(head);
// Task('One morning, ...')
// -- jQuery getJSON example ------------------------
// getJson :: Sting -> {} -> Task Error JSON
const getJSON = curry((url, params) => (
new Task((reject, result) => {
// no return
$.getJSON(url, params, result).fail(reject);
});
);
We are passing a callback with failure and success callbacks as parameters to task in new Task((reject, result) => {...})
. The result
callback is passed to $.getJSON
for on success handling, and on failure we pass the reject
callback to be called.
Notice how in the readFile
call there is no await
but see how we can immediately map
? This is because map
works a bit like a then
.
We can put normal non futuristic values inside as well
Task.of(3).map(three => three + 1);
// Task(4)
Like IO
, Task
will patiently wait for us to give the green light before running. Because it waits for our command, IO
is effectively subsumed by Task
for all asynchronous stuff: readFile
and getJSON
don't require an extra IO
container to be pure. Task
works in a similar fashion when we map
over it; we are placing instructions for the future, like a chore chart in a time capsule.
IMPORTANT to run our Task
, we must call the method fork
. This works like unsafePerformIO
. But as the name suggests, it will fork our process and evaluation continues on without blocking our thread. This can be implemented in numerous ways: with threads and such, but here it acts as a normal async call would, and the big wheel of the event loop keeps on turning:
IMPORTANT if you are familiar with promises, the fork
is sort of like a then
and catch
combined. Meaning that you will need to pass the catch
callback as the first param to then, and the then
callback as a second one.
// -- Pure application ----------------------------
// blogPage :: Posts -> HTML
const blogPage = Handlebars.compile(blogTemplate);
// renderPage :: Posts -> HTML
const renderPage = compose(blogPage, sortBy(prop('date')));
// blog :: Params -> Task Error HTML
const blog = compose(map(renderPage), getJSON('/posts'));
// render :: String -> String -> jQuery
const render = curry((where, what) => $(where).html(what));
// renderError :: String -> jQuery
const renderError = compose(render('#error'), prop('message'));
// renderPage :: String -> jQuery
const renderPage = render('#main');
const requestParams = {};
// -- Impure calling code -------------------------
blog(requestParams).fork(renderError, renderPage);
// called while posts are being fetched
$('#spinner').show();
Upon calling fork
, the Task
hurries off to find some posts and render the page. Meanwhile we show a spinner since fork
does not wait for a response. Finally we will either display an error or render the page onto the screen depending if the getJSON
succeeded or not.
IMPORTANT Task
has also swallowed up Either
! It must do so in order to handle futuristic failures since our normal control flow does not apply in the async world. This is all well as it provides sufficient and pure error handling out of the box.
Even with Task
, our IO
and Either
functors are not out of a job. Here is a quick example that leans toward the mor complex and hypothetical side, but it is useful for illustrative purposes:
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String
// -- Pure application ---------------------------
// dbUrl :: Config -> Either Error Url
const dbUrl = () => {
if (uname && pass && host && db) {
return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
}
return left(Error('Invalid config!'));
};
// connectDb :: Config -> Either Error (IO DbConnection)
const connectDb = compose(map(Postgres.connect), dbUrl);
// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile)
// -- Impure calling code ------------------------
getConfig('db.json').fork(
logErr("couldn't read file"),
either(console.log, map(runQuery))
);
In the example above, we still make use of Either
and IO
from within the success branch of readFile
. Task
takes care of the impurities of reading a file asynchronously, but we still deal with validating the config with Either
and wrangling the db connection with IO
. So you see, we're still in business for all things synchronous. All this thanks to the simple usage of map
.
Note in practice, you'll likely have multiple asynchronous tasks in one workflow, and we haven't yet acquired the full container apis to tackle this scenario. Not to worry, we will look at monads and such soon, but first, we must examine the maths that make this all possible.
Theory on Category Theory
Identity: map id law
// --- identity --- --- --- --- --- --- --- ---
map(id) = id;
// example
id(Container.of(2)); // Container.of(2)
map(id)(Container.of(2)); // Container.of(2)
Composition: compose map law
// --- composition --- --- --- --- --- --- ---
compose(map(f), map(g)) === map(compose(f, g))
// example
compose(map(append(' world')), map(append(' cruel')))(Container.of('Goodbye')); //
map(compose(append(' world'), append(' cruel')))(Container.of('Goodbye')); //
Category definition
A network of objects with morphisms that connect them. So a functor would map the one category to the other without breaking the network.
If an object a
is in our source category C
, when we map it to category D
with functor F
, we refer to the result as F a
For example Maybe
maps our category of types and functions to a category where each object may not exist and each morphism has a null
check.
We can visualize the mapping of a morphism and its corresponding objects with this diagram:
a ------f----> b
| |
F.of F.of
| |
v v
F a ---map(f)--> F b
Here is an example:
// topRoute :: String -> Maybe String
const topRoute = compose(Maybe.of, reverse);
// bottomRoute :: String -> Maybe String
const bottomRoute = compose(map(reverse), Maybe.of);
topRoute('hi'); // Just('ih')
bottomRoute('hi'); // Just('ih')
Graphically:
"hi" ---------reverse------> "ih"
| |
Maybe.of Maybe.of
| |
v v
Maybe("hi") ---map(reverse)--> Maybe("ih")
We can instantly see and refactor code based on properties held by all functors.
Functors can stack:
const nested = Task.of(
[
Either.of('pillows'),
left('no sleep for you'),
]
);
// map
map(map(map(toUpperCase)), nested);
// equivalently
map(map(map(toUpperCase)))(nested);
// Task([Rigt('PILLOWS'), left('no sleep for you')])
// ------------------------------------------
// This is how it would resolve (approx) pseudo code
// Task
nested.fork = (_, resolve) => resolve(
[
Right.$value = 'pillows',
Left.$value = 'no sleep'
]
);
nested.map(map(map(map(toUpperCase))))
// ->
task2.fork == (rej, res) => compose(res, map(map(map(toUpperCase))))([
Right.$value = 'pillows',
Left.$value = 'no sleep'
])
task2.fork(logError, console.log);
console.log(map(map(map(toUpperCase)))([
Right.$value = 'pillows',
Left.$value = 'no sleep'
]));
console.log(map(map(map(toUpperCase)))(asdf));
[ Right.$value = 'pillows',
Left.$value = 'no sleep', ].map(map(map(toUpperCase)));
[ map(map(toUpperCase))(Right.$value = 'pillows'),
map(map(toUpperCase))(Left.$value = 'no sleep'), ];
[ (Right.$value = 'pillows').map(map(toUpperCase)),
(Left.$value = 'no sleep').map(map(toUpperCase)), ];
[ (Either.of(map(toUpperCase)('pillows')),
(Left.$value = 'no sleep'), ];
[ (Right.$value(map(toUpperCase)('pillows')),
(Left.$value = 'no sleep'), ];
[ (Right.$value(toUpperCase('pillows')),
(Left.$value = 'no sleep'), ];
[ (Right.$value('PILLOWS')),
(Left.$value = 'no sleep'), ];
// That is what we end up with after calling fork
What we have here with nested
is a future array of elements that might be errors. We map
to peel back each layer and run our function on the elements. We see no callbacks, if/else
's or for
loops; just an explicit context.
NOTE but we do however have to map(map(map(f)))
. We can instead compose functors:
class Compose {
constructor(fgx) {
this.getCompose = fgx;
}
static of(fgx) {
return new Compose(fgx);
}
map(fn) {
return new Compose(map(map(fn)), this.getCompose))
}
}
const tmd = Task.of(Maybe.of('Rock over London'))
// new Task(_, resolve) => resolve(maybe))
// task.fork = (_, resolve) => resolve(maybe)
//maybe.$value = 'Rock...')
const ctmd = Compose.of(tmd);
// new Compose(task)
// ctmd.getCompose = task
// task.fork = (_, resolve) => resolve(maybe)
// maybe.$value = 'Rock...'
const ctmd2 = map(append(', rock on, Chicago'), ctmd);
// Remember: append(a, b) == flip(concat(a, b)) == b.concat(a)
// Remember: map(a, b) == map(a)(b) == b.map(a)
// --------------------------------------------------
// Resolution in pseudo code
ctmd2 = ctmd.map(x => x.concat(', rock...'))
new Compose(map(map(append(', rock...')), task))
new Compose(task.map(map(append(', rock...'))))
new Compose(new Task((rej, res) => ((_, resolve) => resolve(maybe))(rej, compose(res, map(append(', rock...'))))))
new Compose(task2)
task2.fork = (rej, res) => compose(res, map(append(', rock...')))(maybe) //maybe.$value = 'Rock...'
ctmd2.getCompose = task2
// if we call fork, we get
ctmd2.getCompose.fork('some error', console.log);
// console.log(maybe.map(append(', rock...')))
// console.log(Maybe.of(append(', rock...')('Rock...')))
// console.log(Maybe.of(append(', rock on Chicago')('Rock over London')))
// console.log(Maybe.of('Rock over London'.concat(', rock on, Chicago')))
// console.log(Maybe.of('Rock over London, rock on, Chicago'))
// Note that Maybe could be implemented in a similar fashion as we did for Either
// with two child classes Just and Nothing
Note from g notice how we the reject
param in (reject, resolve) => ...
is ignored with (_, resolve) => resolve(x)
in Task.of(x)
? Then how come in map
we are calling (reject, resolve) => this.fork(reject, etc.)
? Isn't the reject branch or param ignored ?
See how, now, thanks to Compose.of(/*Nested Functors*/)
we are able to call a single map
:
const nestedFunctors = Task.of(Maybe.of('Rock over London'));
const composedFunctors = Compose.of(nestedFunctors);
map(append(', rock on, Chicago'), composedFunctors);
// Maybe.of('Rock over London, rock on, Chicago');
Functor composition is associative and earlier, we defined Container
, which is actually called the Identity
functor. If we have identity and associative composition: we have a category. This particular category has categories as objects and functors as morphisms, which is enough to make one's brain perspire.
Example
// showWelcome :: User -> String
const showWelcome = compose(append('Welcome '), prop('name'));
// validateUser :: (User -> Either String ()) -> User -> Either String User
const validateUser = curry((validate, user) => validate(user).map(_ => user));
// save :: User -> IO User
const save = user => new IO(_ => ({ ...user, saved: true }));
const validateName = { name } => (
name.length > 3
? Either.of(null)
: left('Name must be more than 3 chars long')
);
const validateUser = curry((validate, user) => validate(user).map(_ => user));
// see how it would unfold
validateUser(validateName)(user)
validateName(user).map(_ => user)
Either.of(null).map(_ => user)
Either.of((_ => user)(null))
Either.of(user)
Right(user)
left('Name must be more than 3 chars long').map(_ => user);
Left.x = 'Name must be more than 3 chars long'
const saveAndWelcome = compose(map(showWelcome), save);
// see how it would unfold
either(IO.of, saveAndWelcome)(Right(user));
either(IO.of, compose(map(showWelcome), save))(Right(user));
either(x => new IO(_ => x), compose(map(showWelcome), save))(Right(user));
compose(map(showWelcome), save)(user)
map(showWelcome)(save(user))
map(showWelcome)(new IO(_ => ({ ...user, saved: true })))
map(showWelcome)(io) // io.unsafePerformIO = _ => ({ ...user, saved: true }))
io.map(showWelcome)
new IO(compose(showWelcome, this.unsafePerformIO))
new IO(compose(compose(append('Welcome'), prop('name')), _ => ({ ...user, saved: true })))
new IO(compose(append('Welcome'), prop('name'), _ => ({ ...user, saved: true })))
new IO(compose(append('Welcome'), prop('name'), _ => ({ ...user, saved: true })))
// unsafePerformIO :: a -> User -> String -> String
io2 // io2.unsafePerformIO = compose(append('Welcome'), prop('name'), _ => ({ ...user, saved: true }))
either(x => new IO(_ => x), compose(map(showWelcome), save))(Left('Name should be 3 chars or longer'));
new IO(_ => 'Name should be 3 chars or longer');
// a -> String
io3 // io3.unsafePerformIO = _ => 'Name should be 3 chars or longer'
const register = compose(
either(IO.of, saveAndWelcome),
validateUser(validateName),
);
Monadic Onions
The of
method in our functors is used to place values in what is called a default minimal context.
The of
is part of an important interface called: pointed functor
A Pointed Functor is a functor with an
of
method
What's important is being able to drop any value in our type and start mapping away:
IO.of('tetris').map(concat(' master')); // IO('tetris master')
Mavbe.of(1336).map(add(1)); // Maybe(1337)
Task.of([{ id: 2 }, { id: 3 }]).map(map(prop('id'))); // Task([2, 3])
Either.of('The past, present and future walk into a bar...').map(concat('it was tense.')); // Right('The past, present and future walk into a bar... it was tense')
Remember: IO
and Task
's constructors expected a function as their argument, but Maybe
and Either
do not. The motivation for this interface is a common, consistent way to place a value into our functor without the complexities and specific demands of constructors.
default minimal context: we'd like to lift any value in our type and map
away per usual with the expected behavior of whichever functor.
Note: Left.of
doesn't make any sense. Each functor must have one way to place a value inside it and with Either
, that's new Right(x)
. We define of
using Right
because if our type can map
, it should map
. Looking at examples above we have a feeling of how of
works, but Left
breaks that mold.
Other names for of
are : pure
, point
, unit
and return
.
of
will become important when we start using monads because, as we will see, it's our responsibility to place values back into the type manually.
Important to avoid using the new
keyword there are JavaScript tricks and libraries so let's use them and use of
like a responsible adult from here on out.
Space Burritos
Monads are like onions
const fs = require('fs');
// readFile :: String -> IO String
const readFile = filename => new IO(_ => fs.readFileSync(filename, 'utf-8'));
// print :: String -> IO String
const print = x => new IO(_ => {
console.log(x);
return x;
});
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
cat('.git/config');
//how it would unfold
compose(map(print)(filename => new IO(_ => fs.readFileSync(filename, 'utf-8'))('.git/config')));
compose(map(print)(new IO(_ => fs.readFileSync('.git/config', 'utf-8'))));
compose(map(print))(io);
map(print)(io); // io.unsafePerformIO = _ => fs.readFileSing('.git/config', 'utf-8')
io.map(print);
new IO(compose(print, _ => fs.readFileSing('.git/config', 'utf-8')));
new IO(print(_ => fs.readFileSing('.git/config', 'utf-8')));
new IO((x => new IO(_ => { console.log(x); return x; }))(_ => fs.readFileSing('.git/config', 'utf-8')))
new IO(new IO(_ => { console.log(_ => fs.readFileSing('.git/config', 'utf-8')); return _ => fs.readFileSing('.git/config', 'utf-8'); }))
new IO(new IO(_ => { console.log(_ => fs.readFileSing('.git/config', 'utf-8')); return _ => fs.readFileSing('.git/config', 'utf-8'); }))
io2
io2.unsafePerformIO = new IO(_ => { console.log(_ => fs.readFileSing('.git/config', 'utf-8')); return _ => fs.readFileSing('.git/config', 'utf-8'); })
io2.unsafePerformIO = io3
io3.unsafePerformIO = _ => { console.log(_ => fs.readFileSing('.git/config', 'utf-8')); return _ => fs.readFileSing('.git/config', 'utf-8'); }
io2.unsafePerformIO.unsafePerformIO()()
// above must be wrong, not the same as in book
// IO(IO('[core]\nrepositoryformatview = 0\n'))
We've got an IO
trapped inside another IO
because print
introduced a second IO
during our map
. To continue working with our string we must map(map(f))
and to observe the effect we must unsafePerform().unsafePerform()
// cat :: String => IO (IO String)
const cat = compose(map(print), readFile);
// catFirstChar :: String -> IO (IO String)
const catFirstChar = compose(map(map(head)), cat);
catFirstChar('.geti/config');
// IO(IO('['))
compose(
map(map(safeProp('street'))),
map(safeProp(0)),
safeProp('addresses'),
);
safeProp('addresses')({ addresses: [{ street: { name: 'mulburry' }, number: 909 }]})
// Maybe([{ street: { name: 'Mulburry' }, number: 909 }])
map(safeProp(0))(Maybe([{ street: { name: 'Mulburry' }, number: 909 }]))
// maybe.map(safeProp(0))
// new Maybe(safeProp(0)(this.$value))
// new Maybe(safeProp(0)([{ street: { name: 'Mulburry' }, number: 909 }])
// new Maybe({ street: { name: 'Mulburry' }, number: 909 })
map(map(safeProp('street')))(Maybe({ street: { name: 'Mulburry' }, number: 909 }))
// maybe.map(map(safeProp('street')))
// new Maybe(map(safeProp('street'))(this.$value))
// new Maybe(map(safeProp('street'))({ street: { name: 'Mulburry' }, number: 909 }))
// new Maybe(({ street: { name: 'Mulburry' }, number: 909 }).map(safeProp('street'))) !!!!! EEEEEEERRRRRRROOOOOOOORRRRRRRRRRR TypeError fuck you!!!!!
// !!! I have a fucking map too many!! Why?????
// Actual answer
// Maybe(Maybe(Maybe({ name: 'Mulburry' })))
It is neat to see there are 3 possible failures in our function, but it's a little presumptuous to expect a caller to map
three times to get at the value. This pattern will arise time and time and it's where we'll need to pull out our monads.
Monads are like onions because tears well up as we peel back each layer of the nested functor with map
to get at the inner value. Use the method called join
:
const mmo = Maybe.of(Maybe.of('nunchucks'));
// Maybe(Maybe('nunchucks'))
mmo.join();
// Maybe('nunchucks');
const ioio = IO.of(IO.of('pizza'));
ioio.join();
// IO('pizza')
const ttt = Task.of(Task.of(Task.of('sewers')));
// Task(Task(Task('sewers')))
ttt.join('sewers');
// Task(Task('sewers'))
When we have two layers of the same type, we can join
them together. This ability to join together, is what makes a monad a monad.
Monad is a pointed functor that can flatten. Aka, any functor which has a join
and an of
method and obeys a few laws is a monad.
Maybe.prototype.join = function join() {
return this.isNothing() ? Maybe.of(null) : this.$value;
};
What this does is to just pull out the value out of the container. Which, when the value is a functor, has the effect of returning that functor; reducing the number of functors.
Here is the previous example using join
s:
const join = mma => mma.join();
// firstAddressStreet :: User -> Maybe Street
const firstAddressStreet = compose(
join,
map(safeProp('street')),
join,
map(safeHead),
safeProp('addresses')
);
firstAddressStreet({ addresses: [ street: { name: 'mulburry' }, number: 909 ]});
// Maybe({ name: 'Mulburry' })
Here's another example:
// log :: a -> IO a
const log = x => new IO(() => {
console.log(x);
return x;
});
// setStyle :: Selector -> CSSProps -> IO DOM
const setStyle =
curry((sel, props) => new IO(() => jQuery(sel).css(props)));
// getItem :: String -> IO String
const getItem = key => new IO(() => localStorage.getItem(key));
// applyPreferences :: String -> IO DOM
const applyPreferences = compose(
join,
map(setStyle('#main')),
join,
map(log),
map(JSON.parse),
getItem,
);
applyPreferences('preferences').unsafePerformIO();
// Object {backgroundColor: "green"}
// <div style="background-color: 'green'"/>
We often end up calling join
after a map
call, so let's abstract this into a function called chain
:
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = curry((f, m) => m.map(f).join());
// or
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = f => compose(join, map(f));
chain
may also be called >>==
(pronounced bind) or flatMap
which are all aliases for the same concept. The most accurate name is probably flatMap
but chain
is the most widely used in JS.
So let's refactor previous example:
const fistAddressStreet = compose(
chain(safeProp('street')),
chain(safeHead),
safeProp('addresses'),
);
const applyPreferences = compose(
chain(setStyle('#main')),
chain(log),
map(JSON.parse),
getItem,
);
We've swapped out any map/join
with chain
for cleanliness, but there is more to chain
. chain
effortlessly nests effects, we can capture both sequence and variable assignment in a purely functional way.
// getJSON :: Url -> Params -> Task JSON
getJSON('/authenticate', { username: 'stale', password: 'crackers' })
.chain(user => getJSON('friends', { user_id: user.id }));
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}])
// querySelector :: Selector -> IO DOM
querySelector('input.username')
.chain(
({ value: uname }) => querySelector('input.email').chain(({ value: email }) => IO.of(`Welcome ${uname} prepare for spam at ${email}`))
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');
Monads Theory
Associativity
compose(join, map(join)) === compose(join, join);
Or as a graph:
M(M(M a)) ---map(join)---> M(M a)
| |
join join
| |
v v
M(M a) --------join-----> M a
Above, if we go from top left M(M(M a))
to down left M(M a)
, we can join
the outer two M(M(...))
first and then join
M(M a)
to get M a
.
Alternatively, we can flatten the inner two ..(M(M a)..
s using map(join)
so we end up with the outer and inner M
as M(M a)
, then we can again use join
to get at M a
.
Important, regardless of whether we join the outer two or the inner two first, we end up with the same M a
, that's what this law tells us.
Identity
Another similar law is:
compose(join, of) === compose(join, map(of)) === id;
M a ---of---> M(M a) <-map(of)- M a
\ | /
id join id
\ | /
\ | /
v v v
M a
If we start at the top left heading right, we can see that of does indeed drop our M a in another M container. Then if we move downward and join it, we get the same as if we just called id in the first place. Moving right to left, we see that if we sneak under the covers with map and call of of the plain a, we'll still end up with M (M a) and joining will bring us back to square one.
These identity and associativity laws are found in a category, but to fully be a category, we need a composition function to complete the definition:
const mcompose = (f, g) => compose(chain(f), g);
// left identity
mcompose(M, f) === f;
// right identity
mcompose(f, M) === f;
// associativity
mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h));
They are category laws after all. Specifically, monads form a category called Kleisli category where all objects are monads and morphisms are chained functions.
Summary on Monads: monads let us drill downward into nested computations. We can assign variables, run sequential effects, perform asynchronous tasks, all without laying one brick in a pyramid of doom. They come to rescue when a value finds itself jailed in multiple layers of the same type. With the help of Pointed interface, monads are able to lend us an unboxed value and know we'll be able to place it back in when we are done.
Current limitations (addressed later): monads are very powerful but we still find ourselves needing some extra container functions. For instance:
- what if we want to run a list of API calls at once, then gather the results?
- We can accomplish this with monads, but we'd have to wait for each one to finish before calling the next.
- what about combining several validations?
- We'd like to continue validating to gather the list of errors, but monads would stop the show after the first
Left
entered the picture.
- We'd like to continue validating to gather the list of errors, but monads would stop the show after the first
Applicative functors come to rescue.
Applicative Functors
An applicative functor is a pointed functor with an
ap
method
Container.prototype.ap = function (otherContainer) {
return otherContainer.map(this.$value); // this.$value should be a function
};
Container.of(add(2)).ap(Container.of(3));
// Container(5)
Container.of(2).map(add).ap(Container.of(3));
// Container(5)
Here's a nice property
F.of(x).map(f) === F.of(f).ap(F.of(x))
Which basically translates to this:
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)
Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)
Important see how we are chaining two calls to ap
above? This is because add
is a curried function that takes two parameters. So the first call will partially apply the add
which will return a partially applied add
function, that will be stored in the returned container's $value
, thus allowing us to call ap
again. But a 3rd call would not work, because the return value of the second ap
call, is a container with a $value
of 5
and not a function.
Important of/ap
is equivalent to map
Task.of(2).map(add(3))
Task.of(add(3)).ap(Task.of(2))
Important part of ap
's appeal is the ability to run things concurrently so defining it via chain
is missing out on that optimization.
Important Why not just use monads and be done with it, you ask? It's good practice to work with the level of power you need, no more, no less. This keeps cognitive load to a minimum by ruling out possible functionality. For this reason, it's good to favor applicatives over monads. Also applicatives do not change the container types on us, so another reason to favor them over monads
Important Monads have the unique ability to sequence computation, assign variables, and halt further execution all thanks to the downward nesting structure. When one sees applicatives in use, they needn't concern themselves with any of that business.
// Travel Site
// Http.get :: String -> Task Error HTML
const renderPage = curry((destination, events) => { /* render page */ });
Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('events'));
// Task('<div>some page with dest and events</div>')
Cheat sheet
Functor: is a type that implements map
Our value in the Container
is handed to the map
function so we can fuss with it afterward and returned to its Container
for safe keeping.
// (a -> b) -> Container a -> Container b
Container.prototype.map = function(f) {
return Container.of(f(this.$value));
};
It's like the famous Array.prototype.map
but on Container
s. So we have Container a
instead of [a]
and it works the same. Container.of(2).map(x => x+1)
map
is used to tell the Container
to run a function on the value for us. It gives us abstraction of function application. We'll use dot notation when it's instructive and the pointfree version when it's convenient.
When we surround a function with map
, it is transformed from non-functory to a functory one.
Lifting: at the time of calling, a function can be surrounded by map
, which transforms it from a non-functory function to a functory one. Functions tend to be better off working with normal data types rather than container types, then lifted into the right container as deemed necessarily.
Maybe is another functor whose map
function's role is to only apply the function if the $value
is not Nothing
(aka not null
or undefined
). If it is, then it will simply return itself and not apply any function. You'll often see types like: Just(x) / Nothing
or Some(x) / None
instead of Maybe
.
Either: is a functor that allows branching of code. Thanks to Either
we can do better than declare war on input, we can respond with a polite message. Left, Right are two subclasses of Either
- Left
is used to ignore the request to map
and Right
will work just like the Container
(a.k.a identity). The power comes from the ability to embed an error message with the Left
. Either
captures also logical disjunction ||
as well as encoding the idea of a Coproduct from category theory.
either
is just like maybe
except it takes two functions instead of one and a static value. Each function should return the same type. When either
is applied, if it receives a Left
then the first argument function is called on the e.$value
and the unwrapped value is returned, if it's a Right
the second argument function is called and the unwrapped value returned. Since it is unwrapped, it makes sense to put either at the end of the composition chain.
IO delays the impure action by capturing it in a function wrapper _ => x
. We think of IO
as containing the return value of the wrapped action an not the wrapper itself; even though IO
surrounds a function with another function to make it pure (see of(x)
and the IO(_ => x)
): aka always return the same thing; a function that when called does some side effects. It differs from the previous functors in that the $value
is always a function. We do not think of its $value
as a function, however - that is an implementation detail and we best ignore it. When we map
over our IO
we stick that function at the end of a composition which, in turn, becomes the new $value
and so on. Our mapped functions do not run, the get tacked on the end of a computation we're building up, function by function. Like carefully placing dominoes that we do not dare to tip over. It allows us to play with impure values without sacrificing our purity.
IO.prototype.map = function (fn) {
return new IO(compose(fn, this.$value));
};
Notice how mapping functions on an IO
will just compose the function contained in $value
(be it: _ => x
wrapper, or the result of a previous map
). This will just create a queue pattern delaying execution.
So when do we execute? It is the caller who has the burden of actually pulling the trigger by calling .$value()
at the end. We can rename $value
to unsafePerformIO
to better communicate the possible catastrophic effects.
Task will patiently wait for us to give it the green light before running. In fact, because it waits for our command, IO
is effectively subsumed by Task
for all things asynchronous.
To run our Task
, we must call the method fork
. This works like unsafePerformIO
but as the name suggests, it will fork our process and evaluation continues without blocking our thread.
fork
is a method of Task
and takes two parameters: onError
, onSuccess
. It will trigger the actual execution of a Task
, without blocking. Meaning that code below it will probably run before it has ended the asynchronous stuff and dived into the onSuccess
callback code.
of
is there to allow placing values in a default minimal context and start mapping away. A functor with of
is called a pointed functor.
Libraries: folktale
, ramda
or fantasy-land
functor instances provide the correct of
method as well as nice constructors that do not rely on new
.
Monad: pointed functors that can flatten
Vocabulary
-
pointed functor is a functor with a
of
method -
monad is a pointed functor with a an
join
method -
applicative functor is a pointed functor with an
ap
method -
map
pop the container value, apply the function passed to map, and put the result back into a container -
join
pop the container value. Used to remove outer layers. -
chain
do amap
and then ajoin
. So apply function on contained value, put back into a container, then get the value out of the container. -
safeProp
takes in the prop name, then the object, and returns that prop value wrapped in aMaybe
-
ap
is called on a functor containing a function, and takes a functor as parameter the value of which the function will be mapped// safeProp :: String -> Object -> Maybe a
-
IO
is a functor that delays execution of its contents by wrapping it in a parameterless arrow function. The arrow function returning the value is stored inunsafePerformIO
-
pureLog
takes in the object to log, then wraps aconsole.log
of that object into anIO
. Deferring execution to a laterunsafePerformIO()
call.const getFile = IO.of('/home/ch09.md'); // === new IO(_ => '/home/ch09.md') // equivalently const getFile = new IO(_ => '/home/ch09.md'); // === new IO(_ => '/home/ch09.md') // but when passing a function that needs to be called by io.unsafePerformIO() we need to const pureLog = str => new IO(_ => console.log(str)); // or using IO.of we need bind which will delay execution of console.log to unsafePerformIO() const pureLog = str => IO.of(console.log.bind(console, str)); // so pureLog('Hello').unsafePerformIO(); // "Hello" // otherwise if we did not use bind // BAD const badPureLog = str => IO.of(console.log(str)); // badPureLog('Hello') immediately executes the console log instead of waiting for unsafePerformIO() badPureLog('Hello'); // === new IO(_ => undefined) // "Hello"
const user = { address: { street: { name: "Mulburry" } } };
// getStreetName :: User -> Maybe String
const getStreetName = compose(chain(safeProp('name')), chain(safeProp('street')), safeProp('address'));
getStreetName(user);
// Maybe.of("Mulburry")
Above we pass the user
object to safeProp
which returns a Maybe.of({ street: { name: "Mulburry" } })
, so now it is caged, we need to get at street
property. For that we can use chain(safeProp("street"))
. Remember: safeProp
takes the unwrapped object. Since we have it wrapped, chain
will: m.map(safeProp("street")).join()
. Because map
pops the value out of the cage before applying safeProp("street")
, the correct unwrapped object will be passed, and caged back into a Maybe
by safeProp("street")
, and then caged back again by map
so we get: Maybe.of(Maybe.of({ name: "Mulburry" }))
. That's when the second part of chain
is applied: namely join
, which peels the outer Maybe
wrapping off. On the return of chain(safeProp("street"))
get Maybe.of({ name: "Mulburry" })
. Then the last chain(safeProp("name"))
is applied and we end up by the same mechanism with: Maybe.of("Mulburry")
. And that's it!
// getFile :: IO String
const getFileIO = IO.of('/home/mostly-adequate/ch09.md');
// pureLog :: String -> IO ()
const pureLog = str => new IO(() => console.log(str));
// getFilename :: String -> String
const basename = compose(last, split('/'));
// logFilenameIO :: IO String -> IO ()
const logBasename = compose(chain(pureLog), map(basename));
// logFilename :: IO ()
const logFilename = logBasename(getFileIO);
logBasename(new IO(_ => '/asdf/asdf/asdf.md'));
compose(chain(pureLog), map(basename))(new IO(_ => '/asdf/asdf/asdf.md'));
compose(chain(pureLog), map(basename))(io); // io.uPIO == _ => '/asdf/asdf/asdf.md'
compose(chain(pureLog))(map(basename)(io));
compose(chain(pureLog))(io.map(basename));
compose(chain(pureLog))(new IO(compose(basename, io.unsafePerformIO))); // io.uPIO == _ => '/asdf/asdf/asdf.md'
chain(pureLog)(new IO(compose(basename, io.unsafePerformIO))); // io.uPIO == _ => '/asdf/asdf/asdf.md'
chain(pureLog)(io2); // io2.uPIO == compose(basename, _ => '/asdf/asdf/asdf.md')
io2.chain(pureLog);
io2.map(pureLog).join();
(new IO(compose(pureLog, io2.unsafePerformIO))).join();
io3.join(); // io3.uPIO === compose(purelog, compose(basename, _ => '/asdf/asdf/asdf.md'))
io3.join(); // io3.uPIO === compose(purelog, basename, _ => '/asdf/asdf/asdf.md')
new IO(_ => io3.unsafePerformIO().unsafePerformIO());
io4 // this is what the initial call resolves to, we still need to call unsafePerformIO(), see below
// applying would
io4.unsafePerformIO(); // actual call made by us, then below is the resolution
// resolution of above call
io3.unsafePerformIO().unsafePerformIO();
compose(purelog, basename, _ => '/asdf/asdf/asdf.md')().unsafePerformIO(); // NOTE1 Read below
compose(purelog, basename)('/asdf/asdf/asdf.md').unsafePerformIO();
compose(purelog)(basename('/asdf/asdf/asdf.md')).unsafePerformIO();
pureLog(basename('/asdf/asdf/asdf.md')).unsafePerformIO();
pureLog('asdf.md').unsafePerformIO();
(str => new IO(_ => console.log(str)))('asdf.md').unsafePerformIO();
(new IO(_ => console.log('asdf.md'))).unsafePerformIO();
(io5).unsafePerformIO(); // io5.uPIO === _ => console.log('asdf.md')
(_ => console.log('asdf.md')();
console.log('asdf.md');
undefined
NOTE1 note the passage:
//from
io3.unsafePerformIO().unsafePerformIO();
// to
compose(pureLog, basename, _ => '/asdf/asdf/asdf.md')().unsafePerformIO(); // NOTE1 Read below
Notice that compose()()
, where we are replacing unsafePerformIO()
call with a compose()()
"double" call. Why is that? Remember that the compose
was stored in io3
while we were map
ping. For example at the beginning io.map(basename)
becomes new IO(compose(basename, io.unsafePerformIO))
which as we know, ends up stored as is in io2.unsafePerformIO = compose(basename, io.unsafePerformIO)
without any wrapping lambda. So a call to io2.unsafePerformIO()
is an actual call to compose(basename, io.unsafePerformIO)
, namely: compose(basename, io.unsafePerformIO)()
. The same logic applies to io3
.
To finish nailing the coffin: why is it that we can call unsafePerformIO
as second time in io3.unsafePerformIO().unsafePerformIO()
? Because at the end of the compose
chain, there is a call to pureLog
and the signature tells us that it takes a String
but returns an IO
, hence the second call which will call the pureLog
s returned IO
unsafePerformIO
.
Important when IO
constructor is directly used (instead of the IO.of
), no lambda wrapper is added. We end up storing the constructor param as is into unsafePerformIO
.
Important IO.map
is a special kind of map
. Remember that the usual map
meant: pull the value out of the container, apply the mapped function onto the value and finally wrap it again into a container. For IO
map
does not apply the mapped function. The big difference between IO
and let's say Maybe
is that IO
's goal is to postpone execution of any function to the final unsafePerformIO()
. Keeping it pure as long as possible. So when calling map(someFunction)
on an IO
it will not actually call that someFunction
, rather it will compose
someFunction
with the IO
's current unsafePerformIO
prop, and finally wrap that composition into another wrap. So for IO
, map
is pull out, compose
passed function with existing, and wrap again into a new IO
using the constructor to avoid lambda wrapping. So when we call unsafePerformIO()
we do the actual call to the compose
d function.
Important
const address = { street: "Mulburry" };
// getStreetName :: User -> Maybe String
const getStreetName = compose(safeProp("name"), join, safeProp("street"), join, safeProp("address"));
const getStreetName = compose(safeProp("name"), join, join, map(safeProp("street")), safeProp("address"));
const getStreetName = compose(chain(safeProp("name")), chain(safeProp("street")), safeProp("address"));
// Let's unfold a part of one of the solutions, see the difference between this:
compose(join, join, map(safeProp("street")))(address);
compose(join, join)(map(safeProp("street"))(address));
compose(join, join)(address.map(safeProp("street")));
compose(join, join)(Maybe.of(safeProp("street")(address)));
compose(join, join)(Maybe.of(Maybe.of(address["street"])));
compose(join)(join(Maybe.of(Maybe.of("Mulburry")));
compose(join)(Maybe.of(Maybe.of("Mulburry")).join());
compose(join)(Maybe.of("Mulburry"));
join(Maybe.of("Mulburry"));
Maybe.of("Mulburry").join();
"Mulburry";
// and this (REMEMBER join accepts a single parameter, which should be the Monad):
join(join(map(safeProp("street"))))(address); // calling outer join with the wrong parameter type: join(map(safeProp("street"))))
// ISOLATION: let's isolate the inner join which is the one that will be called first, because js needs to figure out the value of the left part before calling it with (address)
join(map(safeProp("street")))); // ISOLATED from above
join(m => m.map(safeProp("street"))); // ISOLATED resolving
join(lambda); // ISOLATED resolving, call the above lambda lambda
lambda.join(); // Uncaught TypeError: lambda.join is not a function
The problem with the second version above, is that when we call join(join(map(safeProp("street"))))(address)
JavaScript needs to resolve the left part's value, in order to call the whole thing with the parameter address
. So before we are even calling the whole thing, map
will be partially applied wrapping safeProp("street")
in a lambda. Then we call join
on a lambda expression which makes no sense. Hence the TypeError
.
Quick Tips
In a compose
suite call:
map(someFunc)
when piped a monad, and want to apply/composesomeFunc
join
when piped a monad, and you want to extract the contentschain(someFunc)
when piped a monad, want to apply/composesomeFunc
and extract the contentssafeProp("street")
when piped an Object, and you want to return aMaybe
with the inner prop as its contentsmap(safeProp("street"))
when piped a monad, and you want to apply/composesafeProp("street")
and add an additional layer ofMaybe
with the previous monad and inner prop as its contentsap
is a function that can apply the function contents of one functor to the value contents of another.