In the next posts, I want to talk a little bit more about functional programming as a concept - what it is, and what features of PicoLisp make it a functional language. As a little warm-up, let's talk today about the
curry function which incorporates many typical "functional" features, and the concept of "closures" that can be realized with help of
Why it is called
Of course the curry powder in the banner is just a little joke. More adequate but less colorful would have been this one:
curry function is named after Haskell Brooks Curry who was an American mathematician and logician working on combinatory logic, which is one of the theoretical foundations of functional logic.
Besides the currying concept by itself, there are also three programming languages named after him: Haskell, Brooks and Curry.
I think still it's a rather unfortunate naming, because even though I know the background, I still think first of this when I hear about
What is currying?
Currying is a technique to convert a function that takes multiple arguments into a sequence of function that take one argument each and can be called in sequence:
x = f(a, b, c) h = g(a) i = h(b) x = i(c) --> x = g(a)(b)(c)
Why is this helpful? It helps to reduce redundancy in the function declarations.
Let's say wee want to write a function that prints out a curry recipe. We have Indian, Thai and Japanese Curry, and we want to be able to cook for an open number of people.
Of course we could just define three functions:
(de thaiRecipe (N),
(de indianRecipe (N) and so on, where
N is the number of persons. But the more curry variants we have, the more tedious it gets. So instead, let's use the
(de recipe () ( curry ( ) (N) (* N)) )
Now we defined a function
recipe that takes another expression
@variant as an argument. We can call it with
(recipe thaiCurry 5) (recipe indianCurry 3)
thaiCurry is a function that returns the Thai curry recipe for one person. In other words, we avoid to define the "number of persons" parameter in each curry recipe, and secondly, we gain flexibility in our curry definition.
For example, we could define the spice level for each function separately from the recipe:
(recipe (spicy thaiCurry) 5) (recipe (mild thaiCurry) 3)
mild are functions that control the chili content of the recipe (or whatever else).
By reducing the complexity in each of these functions, we reduce the error potential, make the code more flexible and easier to test and maintain.
How it works
We have seen the
@ mark in a similar use before in the Pilog series, and it also serves a similar purpose here: It is a pattern wildcard that is replaced by the interpreter.
curry function only generates single-argument functions, however the PicoLisp implementation is a general higher-order function. In this sense, maybe the function name
curry is maybe slightly misleading.
Creating closures with
A "closure" is a technique that creates a separate environment for a function. In other words, a closure "remembers" the values that its variables had during the last call.
In PicoLisp, the fundamental function for this behaviour is the
job function. It takes a list of symbols (defined as lists or cons-pairs) and binds the symbol to the values during the execution. Let's look at an example:
: (de closureDemo () (job '((A . 0) (B . 0)) (println (inc 'A) (inc 'B 2)) ) )
We defined a function
closureDemo which calls
job with two initial values
A is increased by one and
B by 2. As expected, it prints A=1 and B=2:
: (closureDemo) 1 2
Now what happens if we call it again?
: (closureDemo) 2 4
B are still available inside
job. Their values are maintained. In this regards closures are similar to object properties which can also be defined and maintained by methods.
Now we can use this together with
curry. As an example, let's calculate the Fibonacci Sequence, but we add a little feature: a counter that tells the program how many iterations to do. This is realized with a simple
(def 'fiboCounter (curry ((N1 . 0) (N2 . 1)) (Cnt) (do Cnt (println (prog1 (+ N1 N2) (setq N1 N2 N2 @) ) ) ) ) )
Side Note: What's the difference between
def directly assigns the evaluated arguments to
fiboCounter. This is different to
de (like in
(de fiboCounter ())), where the arguments are assigned unevaluated. Now if we call
(fiboCounter 3), we get back a value for
def, but a curried function for
de (you can try it yourself on the REPL!).
Note that this time we didn't define curry with
@ wildcards, but we bind the values locally to
N2=1. Now each time we start
fiboCounter, it will start where it has stopped before:
: (fiboCounter 5) 1 2 3 5 8 -> 8 : (fiboCounter 3) 13 21 34 -> 34
The reason is because
curry is internally calling the
jobfunction if no
@ wildcards are found, as we can easily verify by checking the
curry source code in
In the next post, we will take a more general, and less academic view on "functional programming" as programming paradigm.