Other ramda posts:
Many articles already cover this, so I’ll keep it short.
Let’s start with a function that takes two numbers and adds them together:
// add :: (Number, Number) -> Number
const add = (a, b) =>
a + b
As our fake type signature describes, add
takes two arguments (essentially, a tuple) that are both of type Number
and returns a value of type Number
.
But if we wanted to create a function that adds 10
to anything, we could write the following:
// add :: Number -> Number -> Number
const add = a => b =>
a + b
// which is the same as
function add(a) {
return function(b) {
return a + b
}
}
// and then
// add10 :: Number -> Number
const add10 = add(10)
add10 // => Function
add10(4) // => 14
Note the change in type signature: we now have singular arguments that are accepted at a time instead of the tuple style. When we provide the first argument, we are then returned a function that will sit and wait until all the functions are applied before giving us a value. This method can be useful in many situations, but consider the following:
add(10)(4)
That feels awkward, right? Fear not! There is a way.
curry
FunctionRamda provides us a function named curry
that will take what might be considered a “normal” JavaScript function definition with multiple parameters and turn it into a function that will keep returning a function until all of its parameters have been supplied. Check it out!
import curry from 'ramda/src/curry'
const oldAdd = (a, b) =>
a + b
const add = curry(oldAdd)
add(10) // => Function
add(10)(4) // => 14
add(10, 4) // => 14
Or if you want to have curry
baked in to your original add
function:
// add :: Number -> Number -> Number
const add = curry((a, b) ->
a + b
)
The magical curry
function doesn’t care when you provide arguments or how you do so – it will just keep returning you partially applied functions until all arguments have been applied, at which point it will give you back a value.
curry
Work?This might seem blasphemous, but to understand how curry
works under the hood, we’re going to dive into a different library’s implementation of it: crocks by @evilsoft. (Crocks is similar to ramda but dives more into abstract data types (ADTs) and is more towards the deeper end of the Functional Programming pool.) I think crocks’ implementation is excellent, and 99% of it being in one file makes for a great teaching tool.
If you want to jump ahead, here is a link to crocks’ curry
function: https://github.com/evilsoft/crocks/blob/master/src/core/curry.js
Where do we start with understanding this next-level JavaScript? Always start with the types, as they can tell a story.
curry
’s StoryWhat does this tell us?
// curry :: ((a, b, c) -> d) -> a -> b -> c -> d
((a, b, c) -> d)
tells us that it accepts a function that has n parameters of any type and returns a value of any type-> a -> b -> c
tells us that it then accepts each parameter – but only 1 at a time!-> d
tells us that it ultimately returns the value as specified in the functionSounds simple, right? Easier said than done!
// curry :: ((a, b, c) -> d) -> a -> b -> c -> d
//
// 1. we accept a function
const curry = (fn) => {
// 2. we return a function taking any `n` arguments
return (...xs) => {
// make sure we have a populated list to work with;
// `undefined` is the value for the Unit type in
// crocks and calling our function must utilize some
// sort of value.
const args =
xs.length ? xs : [ undefined ]
// if the number of args sent are
// less than that required, then
// don't do more work; go ahead and
// return a new version of our function
// that is still waiting for more
// arguments to be applied.
if (args.length < fn.length) {
// way of safely creating a new function
// and binding arguments to it without
// calling it.
return curry(Function.bind.apply(fn, [ null ].concat(args)))
}
// if we've provided all arguments,
// then let's apply them and give
// back the result.
//
// otherwise, let's do some work
// and see if, based on the number
// of arguments, we return a new
// function with fewer arguments
// or go ahead and call the function
// with the final argument so we can
// get back a value.
//
// NOTE: `applyCurry` is defined below.
const val =
args.length === fn.length
? fn.apply(null, args)
: args.reduce(applyCurry, fn)
// 3. if our value is still a function, then
// let's return the curried version of our
// function that still needs some arguments
// to be applied and repeat everything above.
//
// otherwise, we're all done here, so
// let's return the value.
return isFunction(val)
? curry(val)
: val
}
}
const applyCurry = (fn, arg) => {
// return whatever we received if
// fn is actually NOT a function.
if (!isFunction(fn)) { return fn }
// if we have more than 1 argument
// remaining to be applied, then let's
// bind a value to the next argument and
// keep going.
//
// otherwise, then yay let's go ahead
// and call that function with the argument;
// our `[ undefined ]` default saves us from
// some potential headache here.
return fn.length > 1
? fn.bind(null, arg)
: fn.call(null, arg)
}
const isFunction = x =>
typeof x === 'function'
With all of these checks in here, we can now run the following code and have it all work:
const add = curry((a, b) => a + b)
add // => Function
add(1) // => Function
add(1)(2) // => 3
add(1, 2) // => 3
add(1, 2, 99) // => 3 (we don't care about the last one!)
add(1, 2, 99, 2000) // => 3 (we don't care about the last two!)
curry
In ActionIf all of your functions are curried, you can start writing code that you never would have been able to before. Here is a small taste that we will cover more fully in a future Ramda Chops:
// addOrRemove :: a -> Array -> Array
const addOrRemove = x =>
ifElse(
contains(x),
without(of(x)),
append(x)
)
// addOrRemoveTest :: Array -> Array
const addOrRemoveTest =
addOrRemove('test')
addOrRemoveTest([ 'thing' ]) // => ["thing", "test"]
addOrRemoveTest([ 'thing', 'test' ]) // => ["thing"]
(View this example in a live REPL)
The addOrRemove
function almost reads like English: “If something contains x
, give me back that something without x
; otherwise, append x
to that something.” What is worth understanding here is that these functions each accept a number of arguments where the most generic/reusable are provided first (this is a tenet of Functional Programming). Here, we are able to create a very reusable function with partially applied values that sits and waits until the final bit – an array – is provided.
Thanks for reading! Until next time,
Robert