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 onlyprintln
, 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 contains0
. For this reason, the return value will constantly increase. Note the difference to(inc 0)
, which will always return1
.
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:
1
2
3
4
5
: *Run
-> ((-10000 -1251 (tty (println (inc (5))))))
6
:
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))))
10
9
8
7
...
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.
Sources