loading...
Cover image for Practical Functional Programming in JavaScript - Intro to Transformation

Practical Functional Programming in JavaScript - Intro to Transformation

richytong profile image Richard Tong ・6 min read

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

transformation-transformers

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 as in A have to become bs 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 as into bs. Instead, filter uses boolean values derived from as 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 by:

Discussion

markdown guide