Build curried functions
This is Tutorial 17 in the series Make the leap from JavaScript to PureScript. Be sure to read the series introduction where I cover the goals & outline, and the installation, compilation, & running of PureScript. I will be publishing a new tutorial approximately once-per-month. So come back often, there is a lot more to come!
Index | << Introduction < Tutorial 16 | Tutorial 18 > Tutorial 27 >>
In this tutorial, we’re going to cover currying functions of multiple arguments. The process of currying is to break down a function of multiple arguments into a set of nested functions; with each taking a single argument and returning a single result. Think of it as decomposing the function into a chain of functions that have a single argument. You’ll find all the code examples in this tutorial, and supplementary ones in my github repository.
The series outline and javascript code samples were borrowed with permission from the egghead.io course Professor Frisby Introduces Composable Functional JavaScript by Brian Lonsdorf — thank you, Brian! A fundamental assumption of each tutorial is that you’ve watched his video before tackling the equivalent PureScript abstraction featured in this tutorial. Brian covers the featured concepts extremely well, and I feel it’s better that you understand its implementation in the comfort of JavaScript.
Currying explained
In PureScript, like most functional programming languages, functions take only one argument and return one result by default. This approach models the lambda calculus, which is a notation for describing mathematical functions and programs. Alonzo Church developed it in the 1930’s, and it serves as a strong theoretical foundation for functional programming.
The way to accomplish this is by currying multiple function arguments. It turns a multi-argument function into a series of nested functions that each have one argument and return one result. In the case of the last input argument, we return a result to the caller. It means that an expression like result = fn x y z
is transformed to f1 = fn x
; f2 = f1 y
; and result = f2 z
.
When there are multiple arguments to a function, PureScript transcompiles it to JavaScript as a sequence of nested functions; very similar to the curried JavaScript code that Brian showed in his video. The main advantages to currying are 1) we can partially apply functions, and 2) we can create type signatures that are functions.
Partially applying a function means that we apply the function to some of its input arguments; while returning a function that is expecting the remaining arguments later on. Take the canonical example of add10
shown below, which adds 10 to any number by partially applying the function, myAdd
.
myAdd :: Int → Int → Int
myAdd x y = x + yadd10 :: Int → Int
add10 = myAdd 10
By supplying myAdd
with the first argument of 10, myAdd 10 y
is partially applied and executes only upon supplying the y
argument. Here we see that the main benefit of currying is the ability to create specialized functions, such as add10
, without introducing new code or repetition.
type Dict key value = key → Maybe value
Given the arguments key
and value
, we create the function type, key → Maybe value
. You can think of it like a predicate that returns the value
associated with key
; assuming the key
exists in the dictionary. If it does, then return Just value
; otherwise, return Nothing
.
We create an empty dictionary with:
emptyDict :: ∀ k v. Dict k v
emptyDict _ = Nothing
The type argument to emptyDict
is a function type that initializes our dictionary. It returns a function that ignores the input argument k
and assigns our value to Nothing
. The insertion function for this dictionary is a little more interesting:
insertDict :: (Eq k) => k → v → Dict k v → Dict k v
insertDict key value dict =
\key' → if key == key'
then (Just value)
else dict key'
In insertDict
, we are returning a function type that is partially applied. The function is only fully applied after we supply it with the key'
argument. Upon which, it checks whether it matches with a key
in the front of the dictionary. If there is a match then return Just value
; otherwise, delegate the function to the previous Dict
, which was created by the previous call to insertDict
. Let's see how this works in the PureScript REPL.
First, we fire up the REPL with the command pulp --psc-package repl
. Note that, by default, pulp
assumes you're using Bower to install your dependencies. However, in my case, I chose the dependencies to be installed via the psc-package manager, and I inform pulp
of this fact by adding the --psc-package
flag. The REPL will re-compile your code and start the command line:
> import Main
> :t insertDict 'a' (1::Int) emptyDict
Char → Maybe Int
> (insertDict 'a' (1 :: Int) emptyDict) 'a'
(Just 1)
> insertDict 'b' 2 (insertDict 'a' (1::Int) emptyDict) 'b'
(Just 2)
> insertDict 'b' 2 (insertDict 'a' (1::Int) emptyDict) 'a'
(Just 1)
> insertDict 'b' 2 (insertDict 'a' (1::Int) emptyDict) 'x'
Nothing
Taking it from the top, we import our Main
module, and then we start running our tests. Using the :t
command, I often like to check my understanding of the type returned by a function. Thus, we see that insertDict 'a' (1::Int) emptyDict
returns the function type Char → Maybe Int
. This result confirms our understanding - we must supply key'
before insertDict
is fully applied. So, on the next command line, we assign key'
to the character a
which matches with key
, and the output shows that it returns Just 1
as expected.
What happens when we supply a key that’s not in the dictionary? Well, on the second to last line, we see that the key x
is not in the front of the dictionary, because b → Just 2
is in front. Therefore, we fall back to the previous (insertDict 'a' (1::Int) emptyDict)
and check again. However it is not there either, so we fall back once more to emptyDict
which returns Nothing
.
Make no mistake — currying of multi-argument functions is not all unicorns and rainbows. As you might imagine, this nesting of functions comes with a performance penalty which, in most cases, is worth the tradeoff in favor of partial function application. However, in cases where there is performance critical code, we can declare a JavaScript function that takes multiple arguments simultaneously.
In a pinch, use uncurried functions
Again, there is a performance penalty when currying multi-argument functions. In these cases, PureScript provides a solution for writing functions whose speed you deem to be critical to the performance of your program. The module Data.Function.Uncurried
has an extensive number of type constructors that represent JavaScript functions that genuinely take multiple type arguments simultaneously. For example:
foreign import data Fn2 :: Type -> Type -> Type -> Type
Note that the first argument to Fn2
is the name of the function you're calling. The next two types are the input arguments, and the resulting type follows them. Thus, if we felt that our canonical myAdd
was a critical performance function (it's not), then we can transform it to the following:
import Data.Function.Uncurried myAddFast :: Fn2 Int Int Int
myAddFast = mkFn2 \x y -> x + y
PureScript will transcompile it to something similar to the following JavaScript code:
exports.myAddFast = function(x, y) {
return x + y;
};
Using the REPL again, we apply this function of two arguments by using the runF2
function, giving us the expected result:
> runFn2 myAddFast 10 10
20
Summary
In this tutorial we learned that, in PureScript, all functions that have multiple arguments are curried by default. When we curry a function of multiple arguments, we decompose it into a chained sequence of functions, each with one argument. As we saw with the add10
example, currying gives us the ability to create specialized functions from more general ones without introducing new code and repetition. However, currying function arguments provoke a performance penalty. Instead, for performance-critical code, there is a module Data.Function.Uncurried
that you can leverage as an escape hatch.
I hope that you found the concept of currying to be easy to understand. In the next tutorial, we’ll expand on our knowledge of functors by introducing applicative functors, which are described by the Applicative
type class. Applicative functors have many use cases, including form validation. If you are enjoying these tutorials, then please help me to tell others by recommending this article and favoring it on social media. Thank you and until next time!