A Dirty Introduction to Common Lisp for C++ users

I started learning Lisp a while back as a way of seeking inspiration from the first wave of AI systems. The road was rocky and I wish there’s some resource comparing Lisp to a modern language… So this is one. I hope people could find this useful. As a bridge between C++ and Lisp.

I won’t be distinguishing between functions and macros in this article. Nor I’m trying to go through the advanced uses of Lisp. This is a mere introduction.

Should I learn Lisp?

No, if you want a daily-usable language. Learn Lisp for the experience. Go elsewhere and maybe learn Haskell for functional programming.

It took me a few months to learn enough of Lisp (far beyond what’s in this post) to make a conclusion. I’ve opened my eyes more because of Lisp. But I can’t recommend it for anything besides learning the history and philosophy. S-expression is unique but there’s nothing I can’t do with Python or C++. And s-expression brings too much annoyance. It’s too easy to loose track of which parentheses you are in. And although generic, s-expression is not expressive of the intent. Tracing the flow of execution can be difficult at times.

What is Common Lisp

Lisp is a family of languages, which includes Scheme, Clojure, Racket, etc.. and of course Common Lisp. Lisp was first design and released in the 60’s then blew up with the AI boom of the 80’s. Back then Lisp was marked as the AI language (like how Prolog is the logical language). Now Lisp could be found in various expert systems and the Emacs text editor. For example:

And Common Lisp is a standardized dialect of Lisp. Supported by multiple compiler/interpreters such as GNU CLISP, SBCL, clasp and ccl.

The Lisp family of languages are (in)famous for the tremendous amount of ()s, using prefix notations and the total lack of grammar (note: you likely could parse Lisp using a lexer without a parser). Here’s an example of fizzbuzz in Common Lisp:

(loop for i from 1 to 30 do
    (let ((div_5 (= (mod i 5) 0)) (div_3 (= (mod i 3) 0)))
        (if div_3 (format t "Fizz"))
        (if div_5 (format t "Buzz"))
        (if (and (not div_5) (not div_3)) (format t "~d" i))
        (format t "~%")
    )
)

Common Lisp feels alien at the first look. And people with enough programming experience may eventually figure out how it works if they stare at it long enough. This is because of the aforementioned weirdness of Lisp. Yet this is also why Lisp is powerful.

I’ll be referring Common Lisp as Lisp from now on so the article is easier to read.

String formatting and printing

Lisp uses the format function for both string formatting and printing to stdout. Hello World looks like this:

(format t "Hello World~%")

Holy… wait what? you said. Yes, Lisp is really different to any language we are used to; most of them are influenced by C. But Lisp predates it by 13 years – even predating ASCII (1963). Lisp was born in the times of large, room sized mainframes and each vendor has their own encoding for stuff. Let’s break it down one by one:

(format         t            "Hello World~%")
;;              ^            ^
;;Print to screen            |
;;Equivalent of "Hello World\n"

As a compromise, Lisp choose to use ~ as the symbol for escaping characters/indicating special processing and ~% to denote a vendor agnostic way to print a new line. And the first parameter t tells the format function to print to stdout. Setting it to nil will make the function behave like std::format("Hello World\n").

Printing numbers is done by using ~d and strings with ~a. Note: Lisp is not case sensitive, ~d and ~D are treated as the same. It’s the same for function names, etc.. you can also print with (FORMAT T "Foo") or (FoRMat t "bar").

// C++                         // Lisp
using std::format;             // ; boalier plate
using std::cout;               //
                               //
cout << format("Hello World\n")// (format t "Hello World~%")
Hello World                    // Hello World
                               //
cout << format("1+1={}", 2)    // (format t "1+1=~d" 2)
1+1=2                          // 1+1=2
                               //
format("Hello, {}", "Lisp")    // (format nil "Hello, ~a" "Lisp")
(std::string) "Hello, Lisp"    // "Hello, Lisp"
                               //
format("{:5.2f}", 3.14)        // (format nil "~5,2f" 3.14)
(std::string) " 3.14"          // " 3.14"

The prefix notations

Prefix notations are one of the characteristics of Lisp. For example, computing 1+1 in C++ would be, well.. 1+1. While Lisp demands it to be written as (+ 1 1). This is caused by + is in fact a function in Lisp (talk about operator overload, huh). And the syntax of calling function is (). So (+ 1 1) literally translates to operator + (1, 1) in C++. Ex:

// C++                         // Lisp
1+1                            // (+ 1 1)
(int) 2                        // 2
                               //
true == true                   // (= t t)
(bool) true                    // t
                               //
1*2                            // (* 1 2)
(int) 2                        // 2
                               //
3+4/2                          // (+ 3 (/ 4 2))
(int) 5                        // 5
                               //
false != true                  // (not (= nil t))
(bool) true                    // t
                               //
exp(3)                         // (exp 3)
(double) 20.085537             // 20.085537


It should be obvious that there’s no need to worry about operator precedence in Lisp. There’s function calls and function calls only. And by definition calling functions in Lisp uses the exact same syntax. Lisp takes the notion of everything is a function cal (or macros, we won’t talk about them) l to the extreme. Loops? function call. Flow controls? function call. Variable deceleration? function calls.

Flow control and the boolean system

Conditional execution in Lisp is done through the if command.

// C++                         // Lisp
if (2+2 == 4)                  // (if (= (+ 2 2) 4)
    std::cout << "Yes\n";      //     (format t "Yes~%"))
Yes                            // Yes
                               //
if(false)                      // (if nil 
    std::cout << "false\n";    //     (format t "false~%")
else                           //     (format t "true~%")
    std::cout << "true\n";     // )
false                          // false

Hopefully I’ve done a good enough job for you to see how the Lisp if command is analogous to C and C++ if. If the expression evaluates to true, then the second argument is executed, otherwise the third runs (if supplied). The syntax is

(if (cond) (run-if-true) (run-if-false: optional))

However, Lisp’s doesn’t evaluate truthiness like C does. To lisp, everything is true beside a literal nil. Unlike C, 0 is considered as true in Lisp. Therefor the following code have different behavior in C++ and Lisp

// C++                         // Lisp
if (1-1)                       // (if (- 1 1)
    std::cout << "True\n";     //   (format t "True~%")
else                           //   (format t "False~%")
    std::cout << "False\n";    // )
False                          // True

Looping in Lisp works like a standard C loop

// C++                         // Lisp
for(int i=0;i<6;i++)           // (loop for i from 0 to 5 do 
    std::cout << i << "\n";    //     (format t "~d~%" i))
0                              // 0
1                              // 1
2                              // 2
3                              // 3
4                              // 4
5                              // 5

BUT, Lisp does not support the conventional continue and break statements. You can return from a loop, but it doesn’t work 100% of the time. And break in Lisp send you into the debugger instead of exiting the loop. You have to write your loop carefully to not need the statements. Lisp loops does provide a when clause to skip iterations.

// C++                         // Lisp
for(int i=0;i<6;i++)           // (for i from 0 to 5 when (= (mod i 2) 0) do
    if(i%2 == 0)               //     (format t "~d~%" i)
        std::cout << i << "\n";// )
0                              // 0
2                              // 2
4                              // 4

Variables and scoping

Lisp is a dynamically typed language. But unlike newer languages, Common Lisp doesn’t have a global scope. It have lexical and dynamic scopes. Which are really confusing for a beginner so I won’t get into that here. You can declare a dynamic scoped variable using defparameter.

// C++                         // Lisp
int n = 7;                     // (defparameter n 7)
float v = 3.14;                // (defparameter v 3.17)
const char* str = "Tom";       // (defparameter str "Tom")

Nor Lisp have automatic variable scopes like most language does. Scopes have to be created using the let command. And all variables must be declared along with the scope. Followed by all the commands/expressions you want to run within the scope.

(let ((foo 123) (bar "Hello") baz)
	(format "~d~%" foo)
	(format "~a~%" bar)
)

In this example, the code created a scope with 3 variables local to the scope. foo, bar and baz. foo and bar are initialized to their respective values. While baz is set to nil as no default is provided. Then it prints “123\n” then “Hello\n”.

(More or less) Formally the syntax of let is as follows:

(let (list-of-variables) (expression) ...)

As far as I know, declaring a locally scoped variables in the middle of a scope is impossible. You have to either create a new scope or declare all variables beforehand, then assign values to it. Therefor printing n*7 if a variable n is not 2:

if(n != 2) {
	int z = n*7;
	std::cout << std::format("{}\n", z);
}

Have a rather ugly Lisp equivalent

(if (= n 2) (let ((z (* n 7)))
	(format t "~d~%" z)
))

Be aware that Lisp is not a Functional only language. Like C++, Lisp is multi paradigm. You can assign new values to variables.

// C++                         // Lisp
int n = 7;                     // (defparameter n 7)
std::cout << n << "\n";        // (format t "~d~%" n)
n = 8;                         // (setf n 8)
std::cout << n << "\n";        // (format t "~d~%" n)
                               //
7                              // 7
8                              // 8

Lists and functional programming

LISP stands for LISt Processing. So lists are a critical part of Lisp. Most introductory articles will tell you that is functions like an array. And it does. But it is in fact a linked list. Random access of elements of a list have complexity of O(N). And unlike std::list in C++, Lisp lists are unidirectional and may only store a pointer to the start of the list. Thus computing the length of a list may potentially be O(N) depending on the implementation. A few common operations:

// C++                         // Lisp
std::list<int> l = {1, 2, 3};  // (defparameter l (list 1 2 3))
                               //
l.size()                       // (length l)
(size_t) 3                     // 3
                               //
*l.begin()                     // (nth 0 l)
(int) 1                        // 1
                               //
// l[2] // Impossible!         // (nth 2 l)
// Nope                        // 2
                               //
l.push_front(0)                // (push 0 l)
                               //
l.push_back(4)                 // (setf l (append f (list 1)))
                               //
for(auto v : l)                // (loop for v in l do (
    std::cout << v << "\n";    //      (format t "~d~%" v))
0                              // 0
1                              // 1
2                              // 2
3                              // 3

Lisp is one of the first languages to contain aspects of functional programming. Lisp has built-in support for mapping, reducing and generating. I’m going to compare Lisp features to C++20’s ranges library as C++ can’t really do pure functional programming before that.

// C++                         // Lisp
using std::ranges;             //
                               //
std::list<int> l = {1, 2, 3};  // (defparameter l (list 1 2 3))
                               //
views::transform(l,            // (maplist (lambda (v) (* v 2) 
    [](auto v){return v*2;});  //     l)
(view) {2, 3, 6}               // (2 4 6)
                               //
views::filter(l,               // (for loop v in l when (= (mode v 2) 0)
    [](auto v) {return v%2==0};//     collect v)
(view) {2}                     // (2)
                               //
//(ranges-v3)                  //
accumulate(l)                  // (apply '+ l)
(int) 6                        // 6

Functions

Functions are an essential part of Lisp. Declaring them is done using the defun command and no type specification is needed ( i.e. Every function is Lisp is a generic function). For example: the following function adds p1 and p2 together. Print the result, then returns 1.

(defun foo (p1 p2)
	(format t "~d~%" (+ p1 p2))
)

Unlike C++ where the return value of a function is determined by the return statement. Return values of a Lisp function is simply the return value of the last executed statement. So the two seemly identical C++ and Lisp functions have different return type values.

// C++                         // Lisp
auto func(auto p1, auto p2) {  // (defun func (p1, p2)  
    p1 + p2;                   //     (+ p1 p2)
    p1 - p2;                   //     (- p1 p2)
    p1 * p2;                   //     (* p1 p2)
    p1 / p2;                   //     (/ p1 p2)
    p1 == p2;                  //     (= p1 p2)
    std::to_string(p1);        //     (format nil "~d" p1)
}                              // )
                               //
func(1, 2)                     // (func 1 2)
(void)                         // "1"

Like C++, functions in Lisp can be passed as parameters to other functions (first order functions).

// C++                         // Lisp
auto inc(auto n) {             // (defun inc (n)
    return n+1;                //     (+ n 1)
}                              // )
auto act(auto n, auto f) {     // (defun act (n f) (
    return f(n);               //     (f n)
}                              // )
                               //
act(1, inc)                    // (act 1 inc)
(int) 2                        // 2

Computation on types

Computing using types is essential to C++. Lisp provides at minimum the same capability with it’s dynamically types. type-of gives you the type of a variable. Then use the equal function to compare types or typep to compare the type of an object with a type. etc… Ex:

// C++                         // Lisp
int n = 7;                     // (decparameter n 7)
                               //
if constexpr(                  // (if (typep n
    std::is_same_v<decltype(n) //              integer)
                    , int>)    //     (format t "n is a int~%")
    std::cout << "n is int\n"; // )

With this, we could implement lexical_cast<bool> in lisp.

// C++                         // Lisp
template <typename T>          //
T lexical_cast(std::string v)  // (defun lexical_cast (rt, v)
{                              //     (if (equal rt boolean)
    if constexpr (std::is_same //         (equalp v "true")
        <T, bool>::value)      //     )
        return v == "true";    // )
}                              //

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Website Powered by WordPress.com.

Up ↑

%d bloggers like this: