Skip to content

Instantly share code, notes, and snippets.

@variousauthors
Last active March 29, 2018 17:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save variousauthors/10ab79815d17930814f63d3203ef44c2 to your computer and use it in GitHub Desktop.
Save variousauthors/10ab79815d17930814f63d3203ef44c2 to your computer and use it in GitHub Desktop.
Declarative

Preface: The Case for Map

In a declarative style the idea is to write code that describes what will be done, not how it will be done. My fav examples are from recursion:

const reverseString = (str) => {
  if (str.length < 2) return str
  
  const [first, ...rest] = str
  return reverseString(rest) + first
}

The function never answers the question "how do you reverse a string" in a way that would satisfy a child. Imagine:

Q: "how do I reverse a word"

A: "oh that's easy: move the first letter to the end, and then reverse the rest"

Code is nice when it is easy to write and easy to read.

const getId = (input) => {
  return input.id
}

const getIds = (input) => {
  let output = []
  for (let i = 0; i < input.length; i++) {
    output[i] = getId(input[i])
  }
  return output
}

const getIds = map(getId)

map is a great function. I remember when I first started working on a ruby codebase and I couldn't find my familiar for loops anywhere. I felt alienated at first (more so by reduce) but once I understood forEach then suddenly map made sense and honestly... I would never go back! That imperative code just isn't as nice, but with one easy refactor:

const map = (input, fn) => {
  let output = []
  for (let i = 0; i < input.length; i++) {
    output[i] = fn(input[i])
  }
  return output
}

const getIds = (input) => map(input, getIds)

This is just the obvious refactor, it isn't some philosophical or rhetorical move away from imperative programming. The programmer didn't think "oh, I can make this more declarative", they probably just noticed repetition in the code and refactored it this way.

The imperative code is only imperative because we can see the definition not because of some categorical difference in philosophy... it's just a matter of abstraction. One might go so far as to say that (in any given language) "the imperative/declarative binary is a false dichotomy". There are imperative and declarative programming languages (javascript is imperative, where as haskell is declarative) but inside of each language code is good if it achieves a nice level of abstraction.

Implementation Details

How is map implemented? Well we "don't care", right?

// somewhere in lodash...
function map(array, iteratee) {
  let index = -1
  const length = array == null ? 0 : array.length
  const result = new Array(length)

  while (++index < length) {
    result[index] = iteratee(array[index], index, array)
  }
  return result
}

Turns out you only need to go 1 layer deep to find shameless imperative programming. I mean, a while loop!? Come on guys! They could have used a nice, declarative implementation:

// my much nicer implementation
const map = (fn, arr) => {
  if (isEmpty(arr)) return []

  const [x, ...xs] = arr
  
  return prepend(fn(x), map(fn, xs))
}

How do you map a function over a collection? Well... map it over the first element and then map it over the rest! This is a declarative implementation of map, no implementation details available, no nasty imperative programming.

The lodash maintainers chose to go with an imperative definition. Why?

Map is not a function

Trick question: map is not a function, and it doesn't have an implementation. It isn't even code!

What I mean by this is, map is a property of the universe. All implementations of map are isomorphic with one another, so the details actually, literally, "don't matter". The only reason to read the implementation of map would be to decide whether it is an implementation of map or not. The implementation of map has 0 semantic value, since it is 100% interchangeable with any other implementation of map. You can't refactor map in any semantically meaningful way. There is 1 map function, and it lives with God in Heaven.

So with semantics out of the way, the only thing left to care about is performance. The best implementation of map is not the "most readable", it is "the most performant" and anything that you do to make map perform less well is a moral evil. You literally should not implement map less efficiently than you possibly can. If you implement map less efficiently than you could, you are actually literally boiling the oceans, burning fossil fuel, and financing violent regimes in foreign countries who kill people to extract cobalt from the earth. I mean it! I'm not being hyperbolic! If my map saves even 1 cpu cycle over your map, then you must adopt my map because there is no argument that can be made for your map.

The implementation of map is not code. It will never be updated, and it should never be read. Its properties are simple and easy to understand, and knowledge of its workings should be cultural. If you are confused by code that uses map, ask a friend to help you. Making this code less readable does not make it harder to maintain, because the cost of maintaining this code is as close to zero as it can possibly be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment