Functional Programming: The Power of Currying | Bits and Pieces
source link: https://blog.bitsrc.io/functional-programming-part-3-the-powers-of-currying-213eb69b234b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
What is Currying
A curried function is a function that keeps returning functions until all its params are fulfilled
How Currying Works
Let’s say we have add
function
const add = (a, b) => a + b
The simplest implementation of currying is to make a function return a function and so on, like:
const add = (a) => (b) => a + b
Where that can be used like this:
const addOne = add(1) // addOne = (b) => 1 + b
But let’s imagine that we have a curry
function that takes the function and curry it. For example:
const add = curry((a, b) => a + b)
As we see, curry
is a function that takes another function to lazi-fy the params. So now we can invoke it like this:
const addOne = add(1) // addOne = (b) => 1 + b
So, first we created addOne
by passing 1
as a first param (a
) to the curried add
function. Which yielded another function that waits for the rest of the params, where the logic of add
will not be executed until all params are provided.
addOne(2) // 3
Now, passing 2
(as b
) to addOne
; executes the logic 1 + 2
Quick conclusion:
curry
function takes a function and makes its params lazy, in other words you provide these params as you need/go. Just like addOne
Quick note:
You still can call the curried version of add
function like this:
const three = add(1, 2)
So it either takes the arguments piece by piece or all the arguments at once.
Why Currying
Currying will make our code:
- Cleaner
- Less repetitive params passing and less verbose code
- More composable
- More reusable
Why Currying Makes our Code Better
Mainly, some functions take “config” data as input
If we have functions that take “config” params, we better curry them because these “configs” will probably be repeated over and over again.
For example, let’s suppose we have a translator
function, that takes a locale
and a text
to be translated:
const translator = (locale, text) => {/*translation*/}
The usage would look like this:
translator('fr', 'Hello')
translator('fr', 'Goodbye')
translator('fr', 'How are you?')
Every time we call translator
we should provide locale
and text
. Which is redundant and dirty to provide the locale
on every call.
But instead, let’s curry translator
like this:
const translator = curry((locale, text) => {/*translation*/})const inFrench = translator('fr')
Now inFrench
has fr
as locale
provided to the curried translator
function and waits for text
to be provided. We can use it like this:
inFrench('Hello')
inFrench('Goodbye')
inFrench('How are you?')
Currying did us a great favour indeed, we don’t need to specify the locale
each time, instead the curried inFrench
has locale
due to currying.
After currying -in this specific example. Code is:
- Cleaner
- Less verbose and less redundant
Because we separated “config” from actual “data”. Which is quite handy in many areas and use cases.
In real life
In practice we have dynamic locale
(each user has a different language) might be fr
, en
, de
or anything. So instead we better rename inFrench
to translate
, where translate
can be loaded with any locale
.
Now we have translator
that takes a locale
as “config” and text
as data. Due to the fact that translator
is curried, we were able to separate “config” from “data” params.
Why separate “config” from “data” params?
Many components and functions need the use of some functionalities (translate
in our case) but shouldn’t or can’t know about the “config” part (locale
). Where these components or functions have the “data” only part (text
). So these functions will be able to use that function without the need of knowing about the “config” part.
Thus, that component or function will be less coupled with the system, which will make the components more composable and more maintainable.
When do we apply this idea
When we know that there is “config” and there is “data” in a function, we better curry it.
Currying will give us the ability to separate them. And that is a sign of a mature system design. Because one of the large pillars of code qualities is separation of concerns.
Even if a function needs all the params to operate well, we still know better when to pass the params and on which layer of that app.
Closure and Currying Relationship
A Closure: is a function returned by a “parent” function and has access to the parent function’s internal state. (described earlier here)
Currying: will always result a closure. Because each function returned by a curried function will be provided with parents’ internal state.
More Examples
Before we dive deeper
Let’s introduce some utilities, so we can have a deeper look.
Array prototype has utilities like filter
, map
and others. But they are not curry-able, because they use dot (.
) notation.
So let’s convert them to curry-able format:
const filter = (fn, list) => list.filter(fn)
const map = (fn, list) => list.map(fn)
const startsWith = (starter, s) => s.startsWith(starter)
Now we can use them like this:
const lessThan18 = user => user.age < 18// Converting this format
const filteredUsers = users.filter(lessThan18)
// To this format instead
const filteredUsers = filter(lessThan18, users)
(We eliminated the dot notation, and passed processed data as a last param)
Then we curry
them. Where this curry
function will take a function and return a curried function (you can find the implementation here):
const filter = curry((fn, list) => list.filter(fn))
const map = curry((fn, list) => list.map(fn))
const startsWith = curry((starter, s) => s.startsWith(starter))
Examples
Now we can do some meaningful examples…
Example 1️:
Given a list of numbers, increment all numbers by 1
Input: [1, 2, 3, 4, 5]
Output: [2, 3, 4, 5, 6]
Solution:
// the curried `add` function that we defined earlier
const addOne = add(1)const incrementNumbers = map(addOne)const incrementedNumbers = incrementNumbers(numbers)
Example 2️:
Given a string, keep all words that start with ‘C’ letter
Input: "currying is awesome”
Output: “currying”
Solution:
const startsWithC = startsWith('c')const filterStartsWithC = filter(startsWithC)const filteredWords = filterStartsWithC(words)
Example 3️:
Given a list of ranges and a list of numbers; Create an array of functions that can filter numbers based on provided ranges.
Input:
const ranges = [
{min: 10, max: 100},
{min: 100, max: 500},
{min: 500, max: 999}
]const numbers = [30, 50, 110, 200, 650, 700, 1000]// 30 & 50 within 1st range
// 110 & 200 within 2nd range
// 650 & 700 within 3rd range
// 1000 isn't in any range
Output: an array of functions; Each function can take numbers and return filtered numbers that are within the given range.
Solution:
const isInRange = curry(
(range, val) => val > range.min && val < range.max
)const filters = ranges.map((range) => filter(isInRange(range)))
This example has double curry if you can spot it, the filter
and the isInRange
filters
now is a list of functions, each is waiting for numbers
to process, loaded (“config”-ed) with min
and max
Explanation:
The best explanation would be to unfold the currying, and use regular functions instead…
const isInRange = (range, val) => val > range.min && val < range.maxconst filters = ranges.map(
(range) => (numbers) => numbers.filter(
number => isInRange(range, number)
)
)
Remember, () => () => ...
is still another implementation of currying. The simple version of currying.
And all thanks to currying! ❤️
Conclusion
Currying is just about making the params lazy. Where the function keeps returning function until all of its arguments are fulfilled then it computes and returns the result.
We also saw how it makes our code cleaner, less verbose, more composable and even more reusable through practical examples. And that leveraged separation of concerns principle.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK