32
loading...
This website collects cookies to deliver better user experience
This article was originally posted at https://www.switchcasebreak.com/blog/point-free-programming
infixr :!; data L x = x :! L [x] | Nil deriving (Eq, Functor)
does in Haskell (this is not an invitation to you mathematicians, keep scrolling). I think functional programming has a ton of useful applications when working with JavaScript—it's a language that lends itself well to FP paradigms, especially when the more esoteric FP languages (Lisp, Haskell, etc.) have far fewer real-world applications. One of the most interesting and divisive paradigms in FP is point-free style.const numbers = [1, 2, 3]
const numbersPlusOne = numbers.map((num) => num + 1)
numbers
array and an inline mapping function that increments each number in that array by one. We can take the logic from that inline function and abstract it into its own function:const numbers = [1, 2, 3]
// our previous mapping logic
const incrementByOne = (num) => num + 1
const numbersPlusOne = numbers.map((num) => incrementByOne(num))
num
in our inline function (and remember, we're trying not to be concerned about the data we're operating on).const numbersPlusOne = numbers.map((num) => {
// we reference our num argument here
return incrementByOne(num)
})
num
), we can remove the wrapping declaration and pass our function reference directly in.+ const numbersPlusOne = numbers.map(incrementByOne)
- const numbersPlusOne = numbers.map((num) => incrementByOne(num))
.map()
(well, not exactly, but we'll get to that in a bit). We're expecting a single argument in incrementByOne()
, the value to increment. On each iteration of .map()
we're calling this function and invoking it with the element, index, and array. However, since incrementByOne()
has an arity of 1 (meaning it accepts a single argument), it's only concerned with the first argument it receives—in this case, the element being mapped over. That sounds like a lot, but hopefully it will make sense soon. This example demonstrates how both are functionally equivalent:// our point-free function doesn't reference the callback arguments
const numbersPlusOne = numbers.map(incrementByOne)
// this is functionally equivalent to the first example
const numbersPlusOne = numbers.map(function (element, index, array) {
return incrementByOne(element, index, array)
})
function addTwo(a, b) {
console.log(arguments) // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
return a + b
}
addTwo(1, 2, 3)
n
number of parameters can be provided. This makes JavaScript an incredibly flexible language—we don't need to work with strictly-defined function signatures. This means we can unlock incredibly powerful patterns using rest parameters, allowing our functions to accept an arbitrary number of arguments without needing to do things like creating overloaded methods.greet
function. It takes a single argument (a name) and returns a string that says "hello [name]". Super useful stuff! We can call the function independently, or use it as the callback when mapping over an array of names:const greet = (name) => `hello ${name}`
greet('Steve') // hello Steve
const greetings = ['Bill', 'Sally', 'Duane'].map(greet) // ["hello Bill", "hello Sally", "hello Duane"]
greet
function, but then I would need to think of a different example. So I ask that you please ignore how contrived the following code snippet is:function greet(firstName, lastName = '') {
return `hello ${firstName} ${lastName}`.trim()
}
greet('Steve') // hello Steve
greet('Steve', 'Smith') // hello Steve Smith
const greetings = ['Bill', 'Sally', 'Duane'].map(greet)
// ["hello Bill 0", "hello Sally 1", "hello Duane 2"]
.map()
callback function is invoked with three arguments: the element, the index, and the array. When our greet function had an arity of 1 (a unary function), we were only concerned with the first argument of the callback function (the value). After we created the scoped variable for our lastName
argument, it became initialized by the second argument, the index. Uh oh—changing the arity of our function has now created a bug in our application!This works because the signature of our callback function matches the arguments
passed from `.map()` (well, not exactly, but we'll get to that in a bit)
.map()
passes 3 arguments to the callback function. This was fine when our function arity was 1 because we only wanted to use the first argument it received. So what if we created a function that would help enforce calling the .map()
callback as a unary function? That way it would always only use the first argument, no matter how many parameters are provided. Let's see what that might look like:const unary = (f) => (arg) => f(arg)
const greetings = ['Bill', 'Sally', 'Duane'].map(unary(greet))
const unary = (f) => (arg) => f(arg)
unary
is a curried function, which means it's a function that returns another function with arguments partially applied. While it's out of scope for this article (and deserves an entire post to itself), it's a technique for converting a function that takes multiple arguments into a series of functions that each takes a single argument. We now have something like this:const unaryGreet = unary(greet)
console.log(unaryGreet) // (arg) => f(arg)
unary
function and created a new function, unaryGreet
. Let's take a look at the signature: (arg) => f(arg)
. It expects a single argument arg
, and returns the result of calling f
with it. That might be a little bit confusing, so let's look at what our unaryGreet
function looks like (I've taken the liberty of filling in the inner function and naming the arguments to make it a little clearer):function unaryGreet(name) {
greet(name)
}
unary
wraps our greet
function with another function that only accepts a single argument. Lets take a look at how this works with our previous example:const unaryGreet = unary(greet)
const greetings = ['Bill', 'Sally', 'Duane'].map(function (element, index, array) {
// unaryGreet is called with three arguments
unaryGreet(element, index, array)
})
// we are receiving the three arguments (element, index, array)
function unaryGreet(name) {
// we pass through only the first argument to our greet function
greet(name)
}
// greet now only receives a single argument meaning
// we are no longer mapping lastName to the array index
function greet(firstName, lastName = '') {
return `hello ${firstName} ${lastName}`.trim()
}
unary
, we can create functions for enforcing an arity of any size. Two arguments, three arguments, even ten arguments (but probably not ten arguments). You can also see how currying helps us create point-free functions.