Practical Functional Programming in JavaScript - Intro to Transformation
Richard Tong
Posted on July 10, 2020
Welcome back ladies and gentlemen to another round of Practical Functional Programming in JavaScript. Today we will develop some intuition on transformation - a process that happens when one thing becomes another. At the most basic level, transformation is thing A becoming thing B; A => B
. This sort of thing happens quite a lot in programming as well as real life; you'll develop a strong foundation for functional programming if you approach problem solving from the perspective of transformations.
Here is a classic transformation: TransformerRobot => SportsCar
Here's a wikipedia definition of transformation:
transformation [of data] is the process of converting data from one format or structure into another format or structure.
Looks like transformation is a process, but what exactly is the "data" that we are converting? Here's a definition from the wikipedia article for data.
Data (treated as singular, plural, or as a mass noun) is any sequence of one or more symbols given meaning by specific act(s) of interpretation.
Data can be both singular or plural? What about poor old datum? I guess it didn't roll off the tongue so well. In any case, with this definition, we can refer to any JavaScript type as data. To illustrate, here is a list of things that we can call data.
Just data things in JavaScript
- a number -
1
- an array of numbers -
[1, 2, 3]
- a string -
'hello'
- an array of strings -
['hello', 'world']
- an object -
{ a: 1, b: 2, c: 3 }
- a JSON string -
'{"a":1,"b":2,"c":3}'
null
undefined
I like functional programming because it inherently deals with transformations of data, aka transformations of anything, aka As becoming Bs (or hopefully, if you're a student, Bs becoming As). Pair that with JavaScript and you have transformations that come to life. We will now explore several transformations.
Here's a simple transformation of a value using a JavaScript arrow function:
const square = number => number ** 2
square(3) // 9
square
is a function that takes a number and transforms it into its square. number => squaredNumber. A => B
.
Let's move on to transformations on collections. Here is a transformation on an Array using square
and the built in .map function on the Array prototype.
const square = number => number ** 2
const map = f => array => array.map(f)
map(square)([1, 2, 3]) // [1, 4, 9]
To get our new Array, we map
or "apply" the function square
to each element of our original array [1, 2, 3]
. We haven't changed square, we've just used it on each item of an array via map
. In this case, we've transformed the data that is the array [1, 2, 3]
into another array [1, 4, 9]
. Putting it in terms of A and B: map(a => b)(A) == B
.
The following statements are equivalent
map(square)([1, 2, 3]) == [1, 4, 9]
map(number => number ** 2)([1, 2, 3]) == [1, 4, 9]
map(number => number ** 2)(A) == B
map(a => b)(A) == B
When you map
, all the a
s in A
have to become b
s in B
to fully convert A
to B
. This is intuition for category theory, which I won't go into too much here. Basically A and B are nodes of some arbitrary category, lets say Arrays, and map(a => b)
is an "arrow" that describes how you get from A to B. Since each a
maps one-to-one to a b
, we say that map(a => b)
is a linear transformation or bijective transformation from A to B.
Here's another kind of transformation on collections for filtering out elements from a collection. Just like .map
, you can find .filter on the Array prototype.
const isOdd = number => number % 2 === 1
const filter = f => array => array.filter(f)
filter(isOdd)([1, 2, 3]) // [1, 3]
When we supply the array [1, 2, 3]
to filter(isOdd)
, we get [1, 3]
. It's as if to say we are "filtering" the array [1, 2, 3]
by the function isOdd
. Here is how you would write filter
in terms of A and B: filter(a => boolean)(A) == B
.
The following statements are equivalent
filter(isOdd)([1, 2, 3]) == [1, 3]
filter(number => number % 2 === 1)([1, 2, 3]) == [1, 3]
filter(number => number % 2 === 1)(A) == B
filter(a => boolean)(A) == B
Unlike map
, filter
does not convert a
s into b
s. Instead, filter
uses boolean values derived from a
s given by the function a => boolean
to determine if the item should be in B
or not. If the boolean is true, include a
in B. Otherwise don't. The transformation filter(a => boolean)
transforms A into a subset of itself, B. This "filtering" transformation falls under the general transformations.
Our last transformation is a generalized way to say both map(a => b)(A) == B
and filter(a => boolean)(A) == B
. Hailing once again from the Array prototype, welcome .reduce. If you've used reduce
before, you may currently understand it under the following definition:
The reduce() method executes a reducer function (that you provide) on each element of the array, resulting in single output value.
I fully endorse this definition. However, it isn't quite what I need to talk about transformation. Here's my definition of reduce that fits better into our context.
The reduce() method executes a transformation F (A => B) defined by a reducer function and initial value
All this definition says is a general formula for transformations is reduce(reducerFunction, initialValue)
== F
== A => B
. Here is a quick proof.
const reduce = (f, init) => array => array.reduce(f, init)
const sum = reduce(
(a, b) => a + b, // reducerFunction
0, // initialValue
) // F
sum( // F
[1, 2, 3, 4, 5], // A
) // 15; B
// sum([1, 2, 3, 4, 5]) == 15
// F(A) == B
// F == (A => B)
// QED.
It follows that reduce(reducerFunction, initialValue)
can express any transformation from A to B. That means both map(a => b)(A) == B
and filter(a => boolean)(A) == B
can be expressed by reduce(reducerFunction, initialValue)(A) == B
.
reducerFunction
can be expressed as (aggregate, curValue) => nextAggregate
. If you've used or heard of redux, you've had exposure to reducer functions.
The reducer is a pure function that takes the previous state and an action, and returns the next state.
(previousState, action) => nextState
initialValue
is optional, and acts as a starting value for aggregate
. If initialValue
is not provided, aggregate
starts as the first element of A
.
I will now rewrite our Array .map
example from before with .reduce
.
const square = number => number ** 2
// reduce(reducerFunction, initialValue)
const map = f => array => array.reduce(
(prevArray, curValue) => [...prevArray, f(curValue)], // reducerFunction
[], // initialValue
)
map(square)([1, 2, 3]) // [1, 4, 9]
// map(square)(A) == B
// F(A) == B
Each iteration for a given array
, tack on f(curValue)
to the end of the prevArray
.
Here's our previous Array filter
example with reduce
.
const isOdd = number => number % 2 === 1
// reduce(reducerFunction, initialValue)
const filter = f => array => array.reduce(
(prevArray, curValue) => (
f(curValue) ? [...prevArray, curValue] : prevArray
), // reducerFunction
[], // initialValue
)
filter(isOdd)([1, 2, 3]) // [1, 3]
// filter(isOdd)(A) == B
// F(A) == B
Each iteration for a given array
, tack on curValue
to the end of the prevArray
only if f(curValue)
is truthy.
So yeah, reduce
is cool and can do a lot. I should warn you that even though it's possible to write a lot of transformations in terms of reduce, map
and filter
are there for a reason. If you can do it in map
or filter
, don't use reduce
. That said, there are certain things even Array .reduce
cannot do. These things include
- reducing values of any iterable
- reducing values of an async iterable
- reducing values of an object
I think it is valuable to be able to transform these things, so I authored a functional programming library, rubico, with a highly optimized reduce that works on any collection. The same goes for map and filter. In addition, any functions you supply to these special transformation functions (or for that matter any function in rubico) have async and Promises handled automagically. That's because functional code that actually does stuff shouldn't care about async - it takes away from the mathiness.
I'll leave you today with some guidelines for map, filter, and reduce.
- If you want to apply a function to all elements of a collection, use map
- if you want to get a smaller collection from a larger collection based on some test, use filter
- Most everything else, use reduce
I hope you enjoyed this longer-ish intro to transformation. If you have any questions or comments, please leave them below. I'll be here all week. Also you can find the rest of my articles on my profile or in the awesome resources section of rubico's github. See you next time on Practical Functional Programming in JavaScript - Techniques for Composing Data
Posted on July 10, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024