PicoLisp Explored: The curry function

PicoLisp Explored: The curry function

Mia Temma's photo
Mia Temma
·Dec 5, 2021·

5 min read

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 curry.


Why it is called curry function?

Of course the curry powder in the banner is just a little joke. More adequate but less colorful would have been this one:

curry.png

The 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 curry:

kareraisu.png


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 curry concept:

(de recipe (@variant)
   ( curry (@variant) (N) (* @variant 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)

where 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)

where spicy and 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.

The classical 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 curry and job

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=0 and B=0. Now 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

A and 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 do loop.

(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 and de?

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 N1=0 and 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 lib.l.


In the next post, we will take a more general, and less academic view on "functional programming" as programming paradigm.


Sources

irasutoya.com/2014/11/blog-post_1.html
software-lab.de/doc/faq.html#closures

 
Share this