“This is the 14th day of my participation in the First Challenge 2022. For details: First Challenge 2022.”
The concept of Corrification is not a new one, but it is very useful. It is also the foundation of functional programming, a way of thinking about functions in a more modular way.
Composing functions, with the goal of creating larger, more complex, and more useful functions, seems intuitive and is a key component of functional programming.
When we start to combine them, something interesting happens. Let’s see how this works.
The Curried functions do roughly the same as any other function, but they are handled a little differently.
Suppose we want a function that checks the distance between two points: {x1, y1} and {x2, y2}, for example. This formula is a bit mathematical, but there’s nothing we can’t handle:
The formula for the distance between two points, which is an application of the Pythagorean theorem.
In general, calling our function might look like:
const distance = (start, end) => Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) ); console.log( distance( {x:2, y:2}, {x:11, y:8} ); / / logs (10.816653826391969)Copy the code
Now, currying a function is forced to take one argument at a time. So, instead of calling it distance(start, end), we call it distance(start)(end). Each argument is passed in separately, and each function call returns another function until all arguments are provided.
It’s probably easier than to explain, so let’s take a look at the distance function above as a Corrification function:
const distance = function(start){ // we have a closed scope here, but we'll return a function that // can access it - effectively creating a "closure". return function(end){ // now, in this function, we have everything we need. we can do // the calculation and return the result. return Math.sqrt( Math.pow(end.x-start.x, 2) + Math.pow(end.y-start.y, 2) ); } } console.log( distance({x:2, y:2})({x:11, y:8}); / / logs 10.816653826391969 againCopy the code
It seems like a lot of work to get the same results! We can shorten it by using the ES6 arrow function:
const distancewithCurrying =
(start) =>
(end) => Math.sqrt( Math.pow(end.x-start.x, 2) +
Math.pow(end.y-start.y, 2) );
Copy the code
Add indentation for readability without affecting runnability
But again, until we start thinking about our functionality in more abstract ways, it seems like a lot of the noise isn’t really fruitful.
Remember, a function can only return one thing. Although we can supply any number of arguments, we will only return one value, whether it be a number, array, object, or function. We only took one thing back. Now, with the Currization function, we have a function that can only accept one thing. There could be a connection there.
As it happens, the power of Currization functions is the ability to combine and combine them.
Consider our distance formula — if we’re writing a “capture the Flag” game, it might be useful to quickly and easily calculate how far each player is from the flag. We might have a group of players, each containing a {x, y} position. With a set of {x,y} values, a reusable function can come in handy. Let’s play with this idea:
const players = [
{
name: 'Alice',
color: 'aliceblue',
position: { x: 3, y: 5}
},{
name: 'Benji',
color: 'goldenrod',
position: { x: -4, y: -4}
},{
name: 'Clarissa',
color: 'firebrick',
position: { x: -2, y: 8}
}
];
const flag = { x:0, y:0};
Copy the code
Here’s our setup: We have a starting position, flag we have a set of players. We define two different functions to calculate the difference. Let’s look at the difference:
// Given those, let's see if we can find a way to map // out those distances! Let's do it first with the first // distance formula. const distances = players.map( player => distance(flag, player.position) ); /*** * accommodate == [* 5.830951894845301, * 5.656854249492381, *] ***/ / Using a curried function, we can create a function that already // contains our starting point. const distanceFromFlag = distanceWithCurrying(flag); // Now, we can map over our players to extract their position, and // map again with that distance formula: const curriedDistances = players.map(player=>player.position) .map(distanceFromFlag) /*** * curriedDistances == [ * 5.830951894845301, * 5.656854249492381, * 8.246211251235321 *] ***/Copy the code
So here, we use our distanceCurried function to apply one parameter, the starting point. This returns a function that takes another argument, the endpoint. By mapping players, we can create a new array that contains only the data we need, and then pass that data to our Coriolization function!
It’s a powerful tool that may take some getting used to. But by creating currified functions and combining them with other functions, we can create some very complex functions from smaller, simpler parts.
How do I write a Coriolization function
Being able to map curried functions is very useful, but you’ll also find that they have other good uses. This was the beginning of “functional programming” : writing small, pure functions that perform just as well as these atomic bits, and then combining them like building blocks.
Let’s see how we can use Coriolization functions and combine them into larger functions. The next exploration will go into the filter functionality.
First, a little basics. Array.prototype.filter(), the ES6 filter function, allows us to define a callback function that takes one or more input values and returns true or false based on that value. Here’s an example:
// a source array,
const ages = [11, 14, 26, 9, 41, 24, 108];
// a filter function. Takes an input, and returns true/false from it.
function isEven(num){
if(num%2===0){
return true;
} else {
return false;
}
}
// or, in ES6-style:
const isEven = (num) => num%2===0 ? true : false;
// and applied:
console.log( ages.filter(isEven) );
// [14, 26, 24, 108]
Copy the code
Filter a set of numbers to get even numbers
Now the filter function,isEven,is written in a very specific way: it takes a value (or multiple values, for example, if we want to contain the index of an array), performs some sort of internal hoojinkery, and returns true or false. At a time.
This is the essence of the “filter callback function”, although it is not unique to filters – array.prototype. every and array.prototype. some use the same style. The callback tests on each member of the array, accepts some value and returns true or false.
Let’s create some more useful filter functions, but this time a little more advanced. In this case, we might want to “abstract” our functions a little bit, let’s make them easier to reuse.
For example, some useful functions might be isEqualTo or isGreaterThan. They are more advanced because they require two values: one defined as the comparison item (called a comparator) and one from the array being compared (called value). Here’s more code:
// we write a function that takes in a value...
function isEqualTo(comparator){
// and that function *returns* a function that takes a second value
return function(value){
// and we simply compare those two values.
return value === comparator;
}
}
// again, in ES6:
const isEqualTo = (comparator) => (value) => value === comparator;
Copy the code
From this point on, I’ll stick with the ES6 version unless there’s a particularly challenging reason to extend the code to the classic version. Continue to:
const isEqualTo = (comparator) => (value) => value === comparator;
const isGreaterThan = (comparator) => (value) => value > comparator;
// and in application:
const isSeven = isEqualTo(7);
const isOfLegalMajority = isGreaterThan(18);
Copy the code
So, the first two functions are our Currization functions. They take an argument and return a function that, in turn, takes an argument.
Based on these two single-parameter functions, let’s do a simple comparison. The last two isSeven and isOfLegalMajority are simple implementations of these two functions.
So far, we haven’t gotten complicated or involved, and we can stay small again:
// function to simply invert a value: true <=> false const isNot = (value) => ! value; const isNotEqual = (comparator) => (value) => isNot( isEqual(comparator)(value) ); const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) );Copy the code
Here, we have a utility function that simply reverses the truth of a value, isNot. Using it, we can start to combine larger pieces: we get the comparator and the value, run them through the isEqual function, and then we isNot say the value isNotEqual.
It was the beginning of composition, and let’s be fair — it looked pretty stupid. Write down all of this to get a possible use for this:
// all of the building blocks... const isGreaterThan = (comparator) => (value) => value > comparator; const isNot = (value) => ! value; const isLessThanOrEqualTo = (comparator) => (value) => isNot( isGreaterThan(comparator)(value) ); // simply to get this? const isTooYoungToRetire = isLessThanOrEqualTo(65) // and in implementation: const ages = [31, 24, 86, 57, 67, 19, 93, 75, 63]; console.log(ages.filter(isTooYoungToRetire) // is that any cleaner than: console.log(ages.filter( num => num <= 65 ) )Copy the code
“In this case, the end result was very similar, so it didn’t really save us anything. In fact, given the setup of the first three functions, it takes more time to build than to simply compare!”
It’s true. I won’t argue. But it only sees a small piece of a much bigger puzzle.
- First, we are writing moreself-recordingThe code. By using expressive function names, we can tell at a glance that we are filtering
ages
valueisTooYoungToRetire
. We don’t see the math, we see the description. - Second, by using very small atomic functions, we were able to test each part individually to make sure it performed exactly the same every time. Later, when we reuse these small functions, we can be confident that they will work — as the complexity of the function increases, we are freed from testing each small piece.
- Third, by creating abstract functions, we may later find their applications in other projects. Building a library of functional components is a very powerful asset, and I highly recommend cultivating it.
Having said that, we can also take these smaller features and start combining them into bigger and bigger pieces. Now let’s try it: with both isGreaterThanand isLessThan, we can write a nice isInRange function!
const isInRange = (minComparator)
=> (maxComparator)
=> (value) => isGreaterThan(minComparator)(value)
&& isLessThan(maxComparator)(value)
const isTwentySomething = isInRange(19)(30);
Copy the code
Great — we now have a way to test multiple conditions at once. But from this point of view, it doesn’t seem very self-documenting. Middle && is not terrible, but we can do better.
Maybe if we wanted to write another function, we could call and(). The AND function could take any number of conditions and test them against the given value. This will be useful and scalable.
const and = (conditions) =
(value) => conditions.every(condition => condition(value) )
const isInRange = (min)
=>(max)
=> and([isGreaterThan(min), isLessThan(max) ])
Copy the code
Thus, the AND function takes any number of filter functions and returns true only if they are all true for a given value. The last isInRange function does exactly the same as the previous one, but it seems more readable and self-documenting.
Furthermore, it will allow us to combine any number of functions: assuming we want to get even numbers between 20 and 40, we simply combine the function at the top of isEvenWAY with the function isInRange uses an and, and it will work fine.
review
We can easily combine functions by using currified functions. We can concatenate the output of one function directly to the input of the next, since both now take a single argument.
By using composition, we can combine smaller functions or Coriolization functions into larger, more complex structures and be able to work as expected with minimal cost.