PicoLisp Explored: The task function

PicoLisp Explored: The task function


5 min read

Before continuing with the "Mobile Development" series, I decided to write about the task function, because it is a very useful function as we will see in the mobile app series.

Realizing repetitive tasks with task

With help of task, we can schedule tasks to be executed. This can be useful in many situations, like when we want to fetch data periodically from the server, or if we are waiting for an input stream via a socket.

We check the docs:

(task 'num ['num] [sym 'any ..] [. prg]) -> lst

A front-end to the *Run global. If called with only a single num argument, the corresponding entry is removed from the value of *Run. Otherwise, a new entry is created. If an entry with that key already exists, an error is issued. For negative numbers, a second number must be supplied. If sym/any arguments are given, a job environment is built for the *Run entry.

A simple counter

This is a lot of information, so let's look at the most straightforward example first: Printing a counter 1, 2, 3, 4, 5.... We want to print one number per second (i. e. 1000 ms), and we want to start immediately (i. e. after 0 ms).

The syntax for this is as follows:

: (task -1000 0 (tty (println (inc (0)))))

The first number defines the frequency, the second number defines the delay until the command is first executed. The first number needs to be negative.

  • Note 1: we use (tty (println ...)) instead of only println, because this will keep the command prompt in the REPL stable, as you can see in the following gif:


Without tty: The command prompt gets overwritten by the println-output, the result is very messy.


  • Note 2, for PicoLisp enthusiasts: (inc (0)) destructively increments the cell which initially contains 0. For this reason, the return value will constantly increase. Note the difference to (inc 0), which will always return 1.

Now how can we stop the counter? As the documentation tells us, the first number servers as identifier. This means in order to stop this task, we can simply call:

(task -1000)

The *Run global variable

As the docs tell us, task is only a front-end to the global variable *Run. So let's execute task and check the content of *Run to understand what that means.

If no task is running, *Run is NIL:

: *Run
-> NIL

Now let's set a task and try again:

: (task -10000 -10000 (tty (println (inc (0)))))
-> (-10000 -10000 (tty (println (inc (0)))))
: *Run
-> ((-10000 -8102 (tty (println (inc (0))))))

We have set a task that fires after 10 seconds and then every ten seconds. If we check *Run, we see that the second item of the list seem to contain the time difference up to the next execution, as well as the current values:

: *Run
-> ((-10000 -1251 (tty (println (inc (5))))))

Conditional Execution of task

Now let's extend our little example: We want to count down from 10 to 0, after that the task should be stopped.

task implicitly defines a closure (read here for more on PicoLisp closures). This means we can also execute more complex functions:

  • first we set N to 10,
  • then we print it,
  • then we decrement it with (dec 'N)
  • then we check if the result is equal to 0.
  • If it is equal to zero, the task is stopped.

In other words:

: (task -1000 0 N 10 (tty (println N)) (and (=0 (dec 'N)) (task -1000)))
-> (-1000 0 (job '((N . 10)) (tty (println N)) (and (=0 (dec 'N)) (task -1000))))

As you can see, task wraps a job function around the prg part of the task. job first takes a list with the current values of the symbols (in this case (N . 10)), and then the program body which defines the new values of the symbols.

Working with file descriptors

There is a second way to work with task: Instead of a periodic time in milliseconds, we can define a file descriptor. File descriptors are unique identifiers for files or other input/output resources such as pipes or networks in Unix systems ("File descriptors" on Wikipedia).

The file descriptors 0, 1 and 2 are defined as Standard Input, Standard Output and Standard Error. After that, each new file or socket we open will define a new file descriptor, which is also the return value for the picolisp functions like open and port.

: (open "myfile.txt")
-> 3
: (port 4444)
-> 4

Note: port 4444 opens a TCP port on 4444. For an UDP port, the second arguments should be T: port T 4444.

Now what can we do with this? We can define a task which "listens" on a port and executes all commands that it receives:

: (task (port T 4444) (eval (udp @)))
-> (3 (eval (udp @)))

(udp <FD>) receives one UDP package at the corresponding file descriptor, which is set to the socket at port 4444 in the example above.

Next, we can send data to this socket via

: (udp "localhost" 4444 '(println *Pid))  # Send RPC message
-> (println *Pid)

and it will be evaluated because of the eval function defined in the task.

In the next post, we will go a little bit deeper into the socket handling with task by exploring a very simple messenger app.