Introduction to Lisp for C programmers


#1

Here’s the first part of a simple introduction to learning Lisp if your previous experience is with C on the Arduino IDE. It explains how you would handle the most common C constructs in uLisp.

This topic is a Wiki - feel free to edit it to add information or improve what I’ve written (just click the Edit icon at the bottom of the topic).

28th October 2018: I’ve added a bit about statements, operators, and functions.
29th October 2018: Explained the Lisp cond statement.
13th April 2019: Added the Lisp case statement.

Statements, operators, and functions

C

In C statements are terminated by a semi-colon:

count++;

C contains a mixture of operators and functions. Operators use infix notation, and functions are followed by their arguments in parentheses:

Serial.println(count * 2 + 1);

uLisp

In Lisp statements are enclosed in parentheses (a Lisp list):

(incf count)

Operators and functions have the same syntax; they are the first item in the list, and the arguments follow in Polish notation (also called prefix notation):

(print (+ (* count 2) 1))

Variables

C

In C you define global variables by giving them a type:

int count;
int max = 60;
const int led = 13;

You assign to a variable using =:

result = val * 2 + 1;

uLisp

In uLisp variables can contain any type of value, and you define them using defvar. Constants are defined in the same way:

(defvar count)
(defvar max 60)
(defvar led 13)

You assign to a variable using a function called setq:

(setq result (+ (* val 2) 1))

Functions

C

Here’s a typical C function with three integer parameters, which returns an integer result:

int scale (int value, int factor, int offset) {
  return value * factor + offset;
}

uLisp

Here’s the equivalent in uLisp. As before, you don’t declare the type of the arguments or result:

(defun scale (value factor offset)
  (+ (* value factor) offset))

Every expression in Lisp returns a value so you don’t need an explicit return statement.

Local variables

C

You can create variables local to a function in C to store intermediate values. So, for the previous example you could write:

int scale (int value, int factor, int offset) {
  int product = value * factor;
  int result = product + offset;
  return result;
}

uLisp

Here’s the equivalent in uLisp. Local variables are defined using the let* statement:

(defun scale (value factor offset)
  (let* ((product (* value factor))
         (result (+ product offset)))
    result)

for loops

C

The most common way to do iteration in C is using the for loop. For example, if you’ve got six LEDs connected to pins 2 to 7 on an Arduino you can define them as outputs using:

for (int pin=2; pin<8; pin++) {
  pinMode(pin, OUTPUT);
}

and then blink them in turn using:

for (int pin=2; pin<8; pin++) {
  digitalWrite(pin, HIGH);
  delay(500);
  digitalWrite(pin, LOW);
  delay(500);
}

uLisp

The equivalent in Lisp is the dotimes loop, which executes the statements within the loop n times. A loop variable is set to each of the values 0 to n-1:

(dotimes (pin 6)
  (pinmode (+ pin 2) t))

(dotimes (pin 6)
  (digitalwrite (+ pin 2) nil)
  (delay 500)
  (digitalwrite (+ pin 2) t)
  (delay 500))

In uLisp the second parameter in the pinmode function is nil for INPUT and t for OUTPUT. Likewise the second parameter in the digitalwrite function is nil for LOW and t for HIGH

while loops

An alternative iteration construct in C is the while loop, which is typically used to wait until a condition is true.

For example, this loop waits until a pushbutton connected between input pin 8 and GND is pressed, taking the pin low.

C

First we define pin 8 as an input with a pullup, and pin 13 as an output:

pinMode(8, INPUT_PULLUP);
pinMode(13, OUTPUT);

Next we wait until it goes low, and then light up the LED on pin 13:

while (digitalRead(8) == HIGH) {
  delay(100);
}
digitalWrite(13, HIGH);

uLisp

The equivalent in uLisp is loop, which repeats all the statements in a block until a (return) is encountered.

First we set up the pins:

(pinmode 8 2)
(pinmode 13 t)

Then here’s the loop:

(loop
  (unless (eq (digitalread 8) t) (return)))

(digitalwrite 13 t)

or, an equivalent shorter version:

(loop
  (unless (digitalread 8) (return)))

(digitalwrite 13 t)

if statement

C

Here’s a typical example of an if statement in C. It tests an analogue input, and lights an LED if the value is greater than a specified target:

if (analogRead(A0) > target) {
  digitalWrite(13, HIGH);
} else {
  digitalWrite(13, LOW);
}

uLisp

Here’s the equivalent in Lisp. The if construct is followed by three parameters: a test, the then clause, and the (optional) else clause:

(if (> (analogread 0) target)
    (digitalwrite 13 t)
  (digitalwrite 13 nil))

You can write this more succinctly as:

(digitalwrite
  13
  (if (> (analogread 0) target) t nil))

or even:

(digitalwrite
  13
  (> (analogread 0) target))
when and unless

You can replace single branch if or if not forms by when and unless to improve code readability or to take advantage of the fact that they form a block that can contain multiple forms, e.g.

(unless *some-untrue-value*
  (princ "print this ")
  (princ "and that"))

instead of

(if (not *some-untrue-value*)
  (progn 
   (princ "print this ")
   (princ "and that")))

if … else if statement

C

A more complicated use of the if statement is typically followed by one or more else if statements to check which of a series of cases is true. The following example reads an analogue input and sets one of four outputs high depending on the value of the input::

int a = analogRead(0);
if (a < 64) { digitalWrite(2, HIGH); }
else if (a < 128) { digitalWrite(3, HIGH); }
else if (a < 192) { digitalWrite(4, HIGH); }
else { digitalWrite(5, HIGH); }

uLisp

The most convenient way of doing this in Lisp is using the cond (conditional) construct. This is followed by a series of tests, followed by one or more statements that get executed if that test succeeds:

(setq a (analogread 0))
(cond
   ((< a 64) (digitalwrite 2 t))
   ((< a 128) (digitalwrite 3 t))
   ((< a 192) (digitalwrite 4 t))
   (t (digitalwrite 5 t))))

The last test is usually t, which will always succeed if none of the previous tests have succeeded.

switch statement

C

For choosing between a number of fixed alternatives the C switch statement is often used. It is followed by one or more case statements to check which of a series of cases is true:

int key = Serial.read();
switch (key) {
  case 'C':
    clearScreen();
    break;
  case 'I':
    invertScreen();
    break;
  case 'R': case 'F':
    rotateScreen();
    break;
  default :
    printf("Key not recognised" );
}

uLisp

The equivalent in Lisp is the case construct. This is followed by a test, and then a series of keys each followed by one or more statements that get executed if that key matches:

(setq key (read-byte))
(case key
  (#\C (clear-screen))
  (#\I (invert-screen))
  ((#\R #\F) (rotate-screen))
  (t (print "Key not recognised")))

The last key is usually t, which will always match if none of the previous keys have matched.


Thinking in a Lispy way
What are the advantages/disadvantages of uLisp vs C/C++?