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-week. So come back often, there is a lot more to come!
In this tutorial, we're going to talk about 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. Like most functional programming languages, PureScript functions take only one argument and return one result by default. This approach models the lamda 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.
Like referential transparency, currying is one of the main pillars of functional programming. Again, it is the process of turning 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 also functions.
Partially applying a function means that we apply the function to some of its arguments; while returning a function that's expecting additional 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 + y
add10 :: Int → Int
add10 = myAdd 10
By supplying myAdd
with just 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 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
.
Let's be clear - 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.
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
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 (e.g., myAdd
) 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!