Skip to content

Instantly share code, notes, and snippets.

@adkelley
Created August 21, 2018 00:32
Show Gist options
  • Save adkelley/6d442537923237a97f70903fd334f370 to your computer and use it in GitHub Desktop.
Save adkelley/6d442537923237a97f70903fd334f370 to your computer and use it in GitHub Desktop.

Build curried functions

series banner

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!

<< Introduction < Tutorial 16

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.

Currying explained

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.

When performance is critical, 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 (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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment