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.
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).
Let's take 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)
where 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)
or even
: (mapcar (+ 22947200000 3431) (1 2 3 4))
-> (2 3 4 5)
However, 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.
Defining Functions
Instead of anonymous functions, we can also create our own functions with de
:
: (de add1 (N)
(inc N) )
-> add1
In essence, this sets the value of the symbol add1
: 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
again.
: (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
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 add2
, add3
and so on, we can write a function which returns the desired function:
: (de adder (I)
(list '(N) (list '+ 'N I)) )
-> adder
When 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.
It is more convenient to use the curry
function, as documented in the
software-lab.de/doc/refC.html#curry reference.
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
(@I)
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 (
newValue
) call.
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.