Why PicoLisp is great for Functional Programming

Why PicoLisp is great for Functional Programming

·

7 min read

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

  1. be enabled or disabled depending on a condition in the database,
  2. show a personalized label text, and
  3. 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)) )
  1. The button is enabled if the 'acc' value of the current object in the input form is less or equal to 7.
  2. It shows a text like "Click me, Tom".
  3. 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.


Sources

software-lab.de/doc/index.html