In the last post, we discussed what functional programming actually means. The functional programming paradigm:
- distinguishes clearly between "actions" (functions with side effects) and "calculations" (functions without side effects),
- handles direct mutation of objects with care (immutability),
- aims to reach a high reusability of functions.
Now let's see how we can relate that to PicoLisp.
Functional Programming in PicoLisp
PicoLisp is a multi-paradigm language, and like any Lisp it supports a functional programming style. A very powerful fact is that in PicoLisp, functions are treated like any other data. We will see now what that means and why it helps us to write functional code.
Code and Data
In PicoLisp, there is absolutely no formal difference between code and data. "Code" can be created, modified, stored in variables, and passed to and returned from functions ("first-class citizens"). A piece of code is just a normal list which happens to be executable. Such code can take the form of a function, which has formal parameters, and can be applied to (= called with) arguments.
But PicoLisp goes beyond that: Functions are considered as just a special case of code, and PicoLisp encourages a style - unlike most other languages - where pieces of executable code are handled directly, without the need of wrapping them into functions.
But let's first look at functions. A function in PicoLisp is either:
- a number. Then it is taken as a pointer to binary code (in the PicoLisp executable or in an external library) and can be called. Or it is
- a list. Then the CAR of that list holds the function parameter(s), and the CDR holds the body (a list of expressions).
mapcar as an example. It expects a function as its first argument, applies it to the list in the second argument, and returns a new list.
mapcar has no side effects, it is a calculation. Since it accepts other functions as input values, it is highly reusable in all kinds of contexts.
: (mapcar inc (1 2 3 4)) -> (2 3 4 5)
inc is a function which increments its argument:
: (inc 3) -> 4
Note that the value of the symbol
inc is a number (
inc is a built-in function), which is the pointer to the binary code:
: inc -> 22947203431
so according to the above rule, we might indeed write
: (mapcar 22947203431 (1 2 3 4)) -> (2 3 4 5)
: (mapcar (+ 22947200000 3431) (1 2 3 4)) -> (2 3 4 5)
mapcar can't only be combined with built-in functions. We can also define our own functions in form of lists ("anonymous function"):
: (mapcar '((N) (inc N)) (1 2 3 4) ) -> (2 3 4 5)
and we can easily build this function on the fly too:
: (mapcar (list '(N) (list 'inc 'N)) (1 2 3 4) ) -> (2 3 4 5)
This is just to highlight how little restriction PicoLisp puts on defining and combining functions.
Instead of anonymous functions, we can also create our own functions with
: (de add1 (N) (inc N) ) -> add1
In essence, this sets the value of the symbol
: add1 -> ((N) (inc N))
so instead of the 'de'-call we might set it this way:
: (setq add1 '((N) (inc N))) -> ((N) (inc N)) : add1 -> ((N) (inc N))
Now let's call
: (mapcar add1 (1 2 3 4)) -> (2 3 4 5)
mapcar does not notice any difference.
add1 is evaluated, so
mapcar receives the list exactly as in the examples above.
Why is this helpful to write functional code?
The flexibility of functions allow us to modify and expand the functionality of our code just by reusing already defined components. There is no need to increase the code base or define many similar functions for similar purposes. This keeps the codebase short and maintainable.
We know now that we can use
mapcar with different kind of functions, for example with
inc. Now let's go one step further and replace
inc again by a function that takes different arguments. This construct is called "Higher-Order Function":
Higher-order functions take functions as arguments and/or give functions as return values. This is no big thing, because functions are just data in PicoLisp. Let's look at an example.
Up to now, we used
mapcar to increase the list by
1. But what if we want to add an arbitrary number which is maybe only known at runtime? Instead of writing an infinite number of functions
add3 and so on, we can write a function which returns the desired function:
: (de adder (I) (list '(N) (list '+ 'N I)) ) -> adder
adder is called, it builds a function and returns it:
: (adder 1) -> ((N) (+ N 1)) $: (adder 7) -> ((N) (+ N 7))
Now using it with 'mapcar' gives
: (mapcar (adder 1) (1 2 3 4)) -> (2 3 4 5) $: (mapcar (adder 2) (1 2 3 4)) -> (3 4 5 6) $: (mapcar (adder 3) (1 2 3 4)) -> (4 5 6 7)
Side note: This is a conceived example, to have a simple use case. In real programs we can let 'mapcar' do that directly:
: (mapcar + (1 2 3 4) 2) -> (3 4 5 6) $: (mapcar + (1 2 3 4) 3) -> (4 5 6 7)
Now we don't need to stop here. Building functions like the above
adder, with nested
list calls, quickly becomes tedious if the functions are bigger.
The 'curry' Function
Let's re-create the
adder function by currying:
: (de adder (@I) (curry (@I) (N) (+ N @I) ) ) -> adder
The advantage is that instead of building the function manually, you can write it down directly, and
curry will take care to build the final result.
curry takes a list of variables used inside the function
and the function itself
(N) (+ N @I)
The behavior is the same:
: (adder 3) -> ((N) (+ N 3)) $: (adder 4) -> ((N) (+ N 4))
Functional programming in plain code
In PicoLisp it is common to use code without a "surrounding" Function (i. e. plain code). If you think about what a function is in its essence, you see that it consists of an environment and a code body. Let's return to our example:
: add1 -> ((N) (inc N))
Here the environment is
(N) specifying the formal parameter
N, which is bound to the argument while the code body
(inc N) is running. However, often the environment is implicit from the context, which means that only the code bodies need to be specified.
This is typical for example in the PicoLisp GUI. Imagine a button in the GUI which should
- be enabled or disabled depending on a condition in the database,
- show a personalized label text, and
- execute some code when the button is pressed.
This is the corresponding code:
(gui '(+Able +Button) '(>= 7 (: home obj acc)) (pack "Click me, " (get *Login 'name)) '(set> (field 1) (newValue)) )
- The button is enabled if the 'acc' value of the current object in the input form is less or equal to 7.
- It shows a text like "Click me, Tom".
- When pressed, it sets the next input field to the return value of a hypothetical (
These three expressions don't need any further environment, they run in the context of the current form and database, and are written directly inline in the application source where they are needed (locality principle).
Thanks to the prefix classes in the PicoLisp GUI, we can be sure that we will have calculations where we expect tham, and actions where we want them. We can pass any mix of data and functions to the GUI components, ensuring that even very complex applications stay readable and maintainable.