I wanted to take some time to talk about the humble map function. If you have ever used libraries like underscore, lodash, or ramda, you are sure to encounter more than a few curious functions. The unassuming map function is a good starting point on our journey to functional nirvana.
Making a map
Taking a step back, what is map? Just like a paper map, the function map is a guide to get from one place to another.
For example, in your notebook you have a list of numbers: 1, 2, and 3. Next to that list you write 2, 3, and 4. How did this happen? Simply enough, we went over each number in the list and added one to it. In other words, we mapped over the list of numbers and added one to them.
Let’s take those numbers from our notebook and put them in an array. We are going to implement map and use it to transform our array from [1, 2, 3] to [2, 3, 4].
Again, we are going to visit each element in our array and guide the value to its next value. Normally we would just use a for-loop.
var numbers = [1, 2, 3];
var result = [];
for (var i = 0; i < numbers.length; i++) {
result.push(numbers[i] + 1)
;}
複製程式碼
Simple enough! We are visiting each element in the array, adding one to it, and then adding it to a new result array. Now, what happens if we want to add three instead of one? What if we want to multiply? Do you see the pattern forming? There are two things going on here which we can abstract:
- We iterate over our list
- We apply a transformation to each item in the list
//JakeSHEN(@沈烽甲)註釋:先化坤道為容器,再加乾道為變化。複製程式碼
Let’s re-write this first to get our transformation out of the for-loop.
function addOne(x) { return x + 1;}
var numbers = [1, 2, 3];
var result = [];
for (var i = 0; i < numbers.length; i++) {
result.push( addOne( numbers[i] ));
}
複製程式碼
Simple right? We can now pass any function and any array into map. For example.
map(function(x) { return x + 1;}, [1, 2, 3]);
// [2, 3, 4]
//JakeSHEN(@沈烽甲)註釋: 執行console.log( [1,2,3].map(x=>x+1) ); 顯示結果,作者的map是概念
function multiplyTwo(x) { return x * 2;}
map(multiplyTwo, [1, 2, 3]);
// [2, 4, 6]
複製程式碼
As it turns out, javascript arrays already have this method. Pretty handy right?
[1, 2, 3].map(function(x) { return x + 1;});
複製程式碼
Other Data Types
Now forget about arrays and remember that map is just a means to get from one place to another. We do this by giving map a transformation function and some data to work on. How could we implement this on other data types?
Linked Lists
Keeping things array like, we will start by using linked lists.
function listNode(value, next) {
return { value: value,
next: next };}
var three = listNode(3);
var two = listNode(2, three);
var one = listNode(1, two);
// Or more conciselyvar one = listNode(1, listNode(2, listNode(3)));
//JakeSHEN(@沈烽甲)註釋:巢狀資料,類似二叉樹結構,個人在函式式上不是很喜歡這類結構。複製程式碼
How do we iterate over this list, our for-loop wont work! Simple enough, we can call our transformation on the value property of the listNode and then repeat the process recursively for the next value in the sequence. Instead of doing this as a function, we are going to make it a method on our list.
function listNode(value, next) {
return {
value: value,
next: next,
map: function(transformation) {
var nextValue = transformation(value);
var nextLink;
if (next) nextLink = next.map(transformation);
return listNode(nextValue, nextLink);
}
};
}
複製程式碼
Well now that is mind bending! What is going on in that map function? We accept one parameter which is our transformation function.
Just like with our array version, we supply the current value to the transformation and move along to the next element. We know how to get to the next element in the list by just following the next reference. Maps all the way down!.
Ok, next why do we return a listNode? We want to return the same type of thing from map. For arrays we return an array and from a list we return a list. This makes map much more predictable. Every time we call map we are guaranteed to have the same shape of object coming back. The result? We can map over our linked list! Using it is as simple as can be.
var linkedList = listNode(1, listNode( 2, listNode( 3) ));
linkedList.map(function(x) {
return x + 1;
});
// listNode(2, listNode(3, listNode( 4)));
複製程式碼
Trees
Are you starting to see the beauty of map? One more example perhaps! Thus far we have worked with array like structures, what about non arrays, like trees? The principle behind map stays the same, we have a function that takes some data on a journey from one value to another. It does this by running a transformation over its internals. In a tree’s case, we want to go over each node in the tree and apply our transformation to it.
function treeNode(value, left, right) {
return {
value: value,
left: left,
right: right };}
var myTree = treeNode(1, treeNode(2), treeNode(3));
複製程式碼
How can we implement map for this binary tree?
function treeNode(value, left, right) {
return {
value: value,
left: left,
right: right,
map: function(transformation) {
var nextValue = transformation(nextValue);
var nextLeft;
if (left) nextLeft = left.map(transformation);
var nextRight;
if (right) nextRight = right.map(transformation);
return treeNode(nextValue, nextLeft, nextRight); } };}
複製程式碼
Does this look similar? It is just like a linked list with another pointer value. Its use should also feel old hat.
var tree = treeNode(1, treeNode(2), treeNode(3));
tree.map(function(x) {
return x + 1;}
);
// treeNode(2, treeNode(3), treeNode(4))
複製程式碼
Finding Patterns
One of the simple pleasures of programming is finding patterns and making them into reusable snippets. What patterns have we found with map?
Transforming data from one state to another state is very common, map is a generic way to accomplish this. How we have defined map gives us a great deal of predictability. When we map over something we get the same type back. Take for example a function we will call identity.
function identity(x) { return x; }
//JakeSHEN(@沈烽甲)註釋:我最喜歡的單子,每個集合的正則集,拓撲的不動點;
自反身的遍歷,迴圈的尾遞迴;
道生一生二生三生萬物的無,先化坤道為容器,再加乾道為變化。複製程式碼
Quite a simple function, it returns any input you give it. What happens when we use this identity function with one of our examples?
[1, 2, 3].map(identity); // [1, 2, 3]
listNode(1, listNode(2, listNode(3))).map(identity);
// listNode(1, listNode(2, listNode(3)))
treeNode(1, treeNode(2), treeNode(3)).map(identity);
// treeNode(1, treeNode(2), treeNode(3))
複製程式碼
Nothing extraordinary, we just get our original value and type back. Nothing changed. Hold the phone though, this is cool! Our type didn’t change which means we can continue mapping over our object. What does this look like?
function addOne(x) { return x + 1; }
function multiplyTwo(x) { return x * 2; }
[1, 2, 3].map(addOne)
.map(multiplyTwo);
// [4, 6, 8]
listNode(1, listNode(2, listNode(3))).map(addOne)
.map(multiplyTwo);
// listNode(4, listNode(6, listNode(8)))
treeNode(1, treeNode(2), treeNode(3)).map(addOne)
.map(multiplyTwo);
// treeNode(4, treeNode(6), treeNode(8))
複製程式碼
Another interesting thing we notice is that these maps can flatten together. What am I talking about? Notice how in the above examples we iterate over each structure twice? Not terribly efficient. Instead we can combine the two functions (addOne and multiplyTwo) and then map only once!
function addOneAndMultiplyTwo(x) { return multiplyTwo(addOne(x)); }
[1, 2, 3].map(addOneAndMultiplyTwo);
listNode(1, listNode(2, listNode(3))).map(addOneAndMultiplyTwo);
treeNode(1, treeNode(2), treeNode(3)).map(addOneAndMultiplyTwo);
複製程式碼
One last generalization. Let’s re-visit our initial implementation of the function (not method) map. Before we used arrays. Now our map will accept a transformation and any type that has a method called map.
function map(transformation, data) {
return data.map(transformation);}
map(addOne, [1, 2, 3]);
map(addOne, listNode(1, listNode(2, listNode(3))));
map(addOne, treeNode(1, treeNode(2), treeNode(3)));
複製程式碼
How cool is that? Our map function is now type agnostic.
And What About Functors?
Now you may ask “Kevin, you promised me Functors and you never once mentioned them!” In response, I simply raise my eyebrows and exclaim “We have been talking about Functors all along!”
A Functor is nothing more than something that implements a map method which takes in a transformation. This map method also adheres to the two rules from above. Again those two rules were:
- SomeType.map(identity) == SomeType, or in other words Identity. map doesn’t change the type of our object.
- SomeType.map(f1(x)).map(f2(x)) == SomeType.map(f2(f1(x))), or in other words Composition. We can flatten multiple map calls by chaining their transformations together and vice versa.
When first learning this, I scratched my head and pondered “How in the world are Functors useful?”
The answer is quite simple. We now have a pattern that applies to any data type. If you you hand me some object and say “This is a Functor!” I can immediately modify it with map. I don’t need to worry about how any internals of the object work. I know that if I want to modify it, I just map over it.
We have taken a simple function, added two laws to it, and applied it to anything we can throw at it. It could be a simple object, a model from our database, a recursive data structure, whatever. All we need to remember is map means modify, who cares about anything else.
Conclusion
When getting into functional programming, we are bombarded by intimidating words and principles. Functors, Monoids, Monads, Comonads, Endofunctors, the list goes on. At the core of it, the principles are quite simple; we just need to get beyond the names and see their use cases.
Now, go forth! Whenever you see something which is a Functor (or just implements map), smile and rest easy. Functors are our map to success :D