Why I prefer arrow functions

tl;dr:

Designing your own function syntax

Imagine that you were in charge of designing the syntax for function in Javascript. You can do whatever you want.

The only things that exist so far are:

let a = 1
let b = 'hello world'
let c = [a, b]
let d = { e: 2, f: 'blah' }
let g = d.e > a

This is my thought experiment so let's pretend that there's no trailing semicolons and that var never existed, so we use let.

Ok, now it's up to you! How would you design function syntax?

Functions are transformations

Well you didn't invent the concept of functions. So if you want to design from first principles, it's a good idea to remember that functions come from math.

In math, functions take inputs and produce and output. You might sketch that in your notebook with an arrow, like:

Function: input --> output

Unlike in math, programming functions can have side-effects and can also blow up with errors, so you'll need to deal with that.1

Though modeling side-effects2 and error handling3 are super interesting challenges, let's not innovate on those for this and just say that:

Arrow syntax

Looking at the notebook, the arrow -> is what you used to connote "transforms". Perfect, let's use that!

Now if there were good reasons not to use the -> characters, you can fallback to => to represent an arrow.

Input syntax

Functions can take one or multiple inputs. So you'll need to pick a delimiter between the inputs. The simple , works well:4

// our syntax so far:
input1, input2, input3 => <output>

Functions can also take other functions as inputs. Hmm... you'll need some way to disambiguate cases like:

animal, bear => cat, dog, eagle => lion => zebra

// Is it?
((animal, bear => cat, dog, eagle) => lion) => zebra

// Or?
(animal, bear => cat, (dog, eagle => lion)) => zebra

Well, your intuition already used parens () to disambiguate them in our notebook, so let's use those!

(input1, input2, ...) => <output>

Output syntax

If the input can be transformed in a single expression, why not use that expression as the "output"?

(world) => "Hello " + world

And if you need multiple statements to compute the output, you'd need syntax for a scope. Let's borrow curly braces {} from almost every other programming language. Those work well and will be familiar to users. And you can also use return within a function scope to denote what the output finally turns out to be:

(name) => {
  let sanitized = sanitizeUserInput(name)
  let goodWords = censorBadWords(sanitized)
  return goodWords
}

Naming functions

Nice! You've designed anonymous functions!

Now you just need a way to assign names to functions that need to be reused or referenced elsewhere.

What if you added the name of a function just before the parens?

hello(world) => 'Hello ' + world

That would work. But do you want that? What affordances does that create for usage?

Ideally, you want something that encourages users to define anonymous functions when needed, but makes it clear that names can be assigned. And that variables that point to functions can be passed around just like any other value.

And that's the key insight: "just like any other value".

Instead of inventing a new convention for naming functions, you could reuse the existing syntax for naming any value. That way users get a sense that "yes, functions are another type of value". Namely, it should feel as natural to use functions, say as arguments to another function, as it was to pass in strings or numbers.

let hello = (world) => 'Hello ' + world

Typescript function types

Ok so you've designed a nice function syntax. But how does it interop with Typescript?

If you had to design Typescript types for your function syntax, how would you do it?

Leaning on our brain's capacity for pattern matching, why not mirror the function syntax in the syntax for function types?

// pattern: <function name> = (<inputs>) => <output>
type Hello = (world: string) => string
let hello = (world) => 'Hello ' + world

As a benefit of using standard assignment for naming functions, you can reuse how types are bound to variable assignments:

type Hello = (world: string) => string
let hello: Hello = (world) => 'Hello ' + world

Comparison to function

If instead, you designed function syntax like so:

function hello(name) {
  return `Hello ${name}`
}

then, you'd lose out on most of the affordances and design wins we've discussed so far.

When not to use arrow functions

I hope this little story conveyed my love of arrow functions to you. ❤️

But there are a couple cases where you should use functions instead of arrow functions:

Hoisting?

Proponents of functions tend to use "hoisting" as one of the main reasons not to use arrow functions:

// this will fail since `hello` is called before it is defined
hello("friends!")
const hello = (world: string) => 'Hello ' + world

// Javascript will "hoist" the function declaration
// as if `function salutations` was on the first line
// so this will run just fine
salutations("pals!")
function salutations = (earth: string) => 'Salutations ' + earth

But I never run into this. I don't expect to be able to use variables before they are assigned, whether the value is a number, string, or function.

Just like any other value, if I want to use it on a line above where it's defined I use scopes:

export const doingStuff = () => {
  hello("friends!")
}
const hello = (world: string) => 'Hello ' + world

I don't conciously think to do this, it just happens because I don't like using the outermost scope of a module so that I can avoid having side-effects for importing my modules.

And for files that are simple scripts that I want to run, I just wrap statements in a main function and call main at the end:

const main = () => {
  hello("friends!")
}
const hello = (world: string) => 'Hello ' + world

main()

In fact, function hoisting can make things harder to understand.

For me, it's extremely rare to run into hoisting issues and when I do they're easily solved by the strategies above. Definitely worth it to keep using the beautiful affordances of arrow functions.

Footnotes

  1. There are some technical names for these things. Functions without side-effects are called pure functions. Programs that produce a valid output for every input are called total functions. In programming, our functions can be impure and partial.

  2. If you've ever heard of monads, they are all about explicitly modeling side-effects to keep functions pure.

  3. Java does model checked errors in its function signature e.g. public String Hello(string world) throws Exception.

  4. Haskell uses a space ( ) to delimit its function inputs. E.g. add x y = x + y is the equivalent to let add = (x,y) => x + y.

  5. There's a TC39 proposal for generator arrow functions. If that's accepted, I'd probably use arrow functions for generators too.