Created
August 2, 2020 05:22
-
-
Save LoganBarnett/814ea9fb715632a364b69d725d367c60 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Typically when we think of map we think of lists. Let's take the ML notation | |
* for map of a list: | |
* | |
* ((a -> b) -> List a -> List b) | |
* | |
* With ML notation, the best way to read this is that the last arrow is the | |
* return type. The reason the notation exists this way is because functions in | |
* a functional language can be modeled as unary, or meaning they only have one | |
* argument. One can imagine multiple argument functions as sytactic sugar. | |
* | |
* Take this add example: | |
*/ | |
const add = x => y => x + y | |
/** | |
* The ML notation for add is (number -> number -> number), where the first two | |
* numbers are paramters and the third is the return type. | |
* | |
* Embedded functions are delieniated with parenthesis. Looking back at our map | |
* example: | |
* | |
* ((a -> b) -> List a -> List b) | |
* | |
* (a -> b) means the first argument is a function that takes an "a" and returns | |
* a "b". In ML's type notation, type parameters can appear as lowercase | |
* letters. It's also notable that type parameters such as a and b can be the | |
* same type, but they are permitted to differ. Depending on how deep you get | |
* into type theory, a and b could represent any different form of data. For | |
* example the type of a could the number 5 (not just any number), and type b | |
* would be something related to 5 by virtue of the transformation function. | |
* | |
* In functional programming, usually the arguments that "configure" or | |
* specialize the function come first. The data portion of the function comes | |
* last. If you come from a background such as Ruby, C#, Java, or JavaScript | |
* (think lodash), this will seem backwards to you. The reason this is done is | |
* to aid with partial application - an important tool in function composition. | |
* | |
* We could write our list's map function like this: | |
*/ | |
const mapList = f => xs => xs.map(f) | |
/** | |
* Yes, we're borrowing from the existing Array.prototype.map here. We will come | |
* back to this function later. | |
* | |
* Back to map: What if we took a step back from our map signature. Should we | |
* care about whether or not it's a list we want to transform? Could we apply | |
* map in other contexts? Some languages allow map to operate on a map | |
* structure, where each value undergoes a transformation and the operation | |
* produces a new map structure. | |
* | |
* In category theory, we can represent lists in a more generic sense: A | |
* functor. In the vaguest sense a functor is a kind of structure. Even calling | |
* a functor a "container" might make some intuitive sense, but that can be too | |
* constraining a term. | |
* | |
* We generally write functors as "F a", meaning a functor parameterized by type | |
* a. Functors themselves are type parameters, which are further paramterized by | |
* "a". This feature in a typed language is called Higher Kinded Types, or HKT. | |
* Without it, we must express these types individually. So that means one map | |
* type for lists, one for associative lists, and so on. | |
* | |
* With the functor, the ML expression for map becomes: | |
* | |
* ((a -> b) -> F a -> F b) | |
* | |
* Tiny! Now let's take a look at something more complicated in JS: Promises. | |
* Stay with me while we stitch this together. Promise in JS has a then method | |
* that can be used to operate on the promises' data. You could think of it as a | |
* callback that is executed when the promise resolves, but that's very | |
* imperative thinking and we don't do that here. For a moment, let's make a | |
* functional-esque version of Promise's then: | |
*/ | |
const then = f => p => p.then(f) | |
/** | |
* This looks familiar, but the names are different. This is essentially the | |
* same thing as map. Let's do some renaming and create a real method for | |
* Promise: map | |
*/ | |
Promise.prototype.map = function(fn) { return this.then(fn) } | |
/** | |
* So basically we made an alias here. There's no material difference between | |
* map and then. The map method just delegates to then. Let's go back to our | |
* list version of map and rewrite it to account for functors: | |
*/ | |
const map = fn => f => f.map(fn) | |
/** | |
* Now map works for both Array and Promise. One of the things we get from doing | |
* this is we create an ecosystem that begs for function composition. With | |
* function composition we're just stitching together functionality from things | |
* we already understand because they are very simple. | |
* | |
* Even if we don't go deep into a rich ecosystem of curried functions, we can | |
* still benefit having these two things nudged a little closer. Take our add | |
* function we wrote earlier, and let's us it with map. | |
*/ | |
[1, 2, 3].map(add(1)) // [2, 3, 4] | |
[1, 2, 3] | |
.map(add(2)) // [2, 3, 4] | |
.map(add(1)) // [3, 4, 5] | |
/** | |
* And then we do a similar thing with Promise. | |
*/ | |
Promise.resolve(1) | |
.map(add(2)) // Resolves as 3. | |
.map(add(1)) // Resolves as 4. | |
/** | |
* Remember we didn't actually change anything meaningful in Promise. We just | |
* renamed a method, essentially. When we apply map, we keep the structure and | |
* operate on something the structure is responsible for. We can extend this to | |
* Maybe and Either as well. | |
*/ | |
new Just(1) // Just is the "value" form of Maybe. | |
.map(add(1)) // Maybe(2) | |
.map(add(2)) // Maybe(4) | |
new Nothing() // Nothing is the "null" form of Maybe. | |
.map(add(1)) // Nothing | |
.map(add(2)) // Yep. Still Nothing. | |
new Right(1) // Right is the "value" form of Either, by convention. | |
.map(add(1)) // Either(2) | |
.map(add(2)) // Either(3) | |
new Left(1) // Left is the "left" form of Either. | |
.map(add(1)) // Either(1) | |
.map(add(2)) // Either(1) | |
/** | |
* I hope this has been informative of the power of map. When you hear FP | |
* enthusiasts talking about how most everything can be handled with some | |
* combination of map, filter, and fold (reduce), one can see how it's more than | |
* just list comprehensions which few of us get to remain in when writing real | |
* world software. | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment