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.
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?
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.