UP | HOME

🔮 Lisp Notes

Introduction

Disclaimer: These are my personal notes. There is no warranty it will work on your machine, nor is it reviewed by a technical expert. I create these notes for myself to be able to document and review my learning progress. This document is a so called ’living document’, it means it will never be finished.

Lisp is the ideal programming language for prototyping, and explorative programming because of its REPL and flexibility. The language itself can be modified and extended without changing the syntax. It is feasible to write programs in Lisp which performance can be the same like C programs, but with better maintainability, which is already proved.

I found some interesting stories about Lisp on the internet:

If you have enough time, have a look at the different implementations of Lisp.

Python REPL vs. Lisp REPL

The main difference between Python REPL and Lisp REPL is that Lisp REPL is more advanced and powerful than Python REPL. Lisp REPL allows developers to redefine core functions and change the syntax of the language on-the-fly, while Python REPL is simpler and more limited in its capabilities. This makes Lisp REPL a more flexible and customizable environment for advanced programming tasks.

Glossary

SBCL

Steel Bank Common Lisp (SBCL) is a high performance Common Lisp compiler. It is open source / free software, with a permissive license. In addition to the compiler and runtime system for ANSI Common Lisp, it provides an interactive environment including a debugger, a statistical profiler, a code coverage tool, and many other extensions.

It was initially released in 1999 as a fork of the CMUCL implementation of Common Lisp, which was developed at Carnegie Mellon University.

SBCL was created by a group of developers who wanted to continue development of CMUCL and improve its performance and stability. They forked the codebase and began making enhancements and bug fixes, eventually releasing SBCL as a standalone implementation of Common Lisp.

Steel Bank Common Lisp (SBCL) offers several benefits, including:

  1. High performance: SBCL is known for its high speed and efficient memory management, which makes it ideal for large-scale projects.
  2. Robustness: SBCL has a reputation for being a very stable and reliable implementation of Common Lisp, with a focus on correctness and error detection.
  3. Portability: SBCL is highly portable and runs on a wide range of platforms, including Linux, macOS, and Windows.
  4. Large community: SBCL has a large and active community of developers and users, which provides support and resources for those who are new to the language.
VPS

Virtual Private Server. A virtual machine with its own copy of an operating system sharing resources of a physical server with other virtual machines.

A VPS (Virtual Private Server) offers several benefits, including:

  1. Cost savings: VPS hosting is typically more affordable than dedicated server hosting, making it a cost-effective option for individuals and businesses.
  2. Scalability: VPS hosting allows users to easily scale their resources up or down as needed, making it a flexible option for businesses with changing needs.
  3. Better performance: Since VPS hosting provides users with dedicated resources, it generally offers better performance and reliability than shared hosting.
  4. Greater control: With a VPS, users have full control over their hosting environment, including root access, allowing them to customize their settings and install the software they need.
  5. Enhanced security: VPS hosting provides users with their own isolated virtual environment, which can help to improve security and protect against cyber threats.
Common Lisp (CL)

Common Lisp is a dialect of the Lisp programming language, published in ANSI standard document ANSI INCITS 226-1994.

Common Lisp is a programming language that offers several benefits, including:

  1. Expressiveness: Common Lisp is highly expressive, allowing developers to write code in a clear and concise manner.
  2. Flexibility: Common Lisp is a very flexible language that can be used for a wide range of applications, from artificial intelligence to web development.
  3. Interactivity: Common Lisp provides an interactive environment that allows developers to test their code and make changes on-the-fly.
  4. Portability: Common Lisp code can be compiled and run on a variety of platforms, making it a highly portable language.
  5. Extensibility: Common Lisp has a large library of add-on libraries and frameworks, making it easy for developers to extend the language’s functionality and add new features.
EMACS

EMACS, originally named EMACS, is a family of text editors that are characterized by their extensibility.

EMACS is a powerful text editor that offers several benefits, including:

  1. Customization: EMACS is highly customizable and can be tailored to fit the user’s specific needs and preferences.
  2. Efficiency: EMACS is designed to be efficient, with many keyboard shortcuts and commands that allow users to perform tasks quickly and easily.
  3. Versatility: EMACS can be used for a wide range of tasks, including coding, writing, and editing documents.
  4. Availability: EMACS is available for many operating systems and can be used on a variety of devices.
  5. Extensibility: EMACS has a large library of add-ons and plugins, allowing users to extend its functionality and add new features.
Gnu/Linux

Linux is a family of open-source Unix-like operating systems based on the Linux kernel.

GNU/Linux is an open-source operating system that offers several benefits, including:

  1. Cost savings: GNU/Linux is free to download and use, which can provide significant cost savings for individuals and businesses.
  2. Stability: GNU/Linux is known for its stability and reliability, with fewer crashes and downtime compared to other operating systems.
  3. Security: GNU/Linux is known for its strong security features, with fewer viruses and malware targeting the operating system.
  4. Flexibility: GNU/Linux is highly customizable, allowing users to tailor their operating system to their specific needs and preferences.
  5. Community support: GNU/Linux has a large and active community of developers and users who provide support and contribute to the ongoing development of the operating system.

Functions - The Building Blocks of (Common) Lisp

Function Name
This is the symbol that represents the function and is used to call it.
Required Arguments
These are the mandatory parameters that a function needs to operate. They must be supplied when calling the function.
Optional Arguments

These are parameters that can be provided but are not required for the function to work. If not supplied, they take on a default value specified in the function definition. Optional arguments are introduced using the &optional keyword.

(defun greet (name &optional (greeting "Hello"))
  (format t "~a, ~a!~%" greeting name))
(greet "Alice")
Hello, Alice!
(greet "Bob" "Hi")
Hi, Bob!
Rest Arguments

This is a single argument that collects any remaining arguments passed to the function into a list. It is introduced using the &rest keyword.

(defun sum-numbers (&rest numbers)
  (reduce '+ numbers))
(sum-numbers) ; Output: 0 (no arguments, so sum is 0)
0
(sum-numbers 1 2 3)
6
(sum-numbers 5 10 15 20)
50
Keyword Arguments

A keyword argument is an optional argument in a function or method call in Lisp that is identified by a keyword (a symbol starting with a colon). Keyword arguments are used to provide flexibility and readability when calling functions with many optional parameters. They can be supplied in any order and have default values if not explicitly provided.

For example, consider the following function definition:

(defun example-function (a &key b c))

In this function, a is a required argument, while b and c are optional keyword arguments. You can call this function as follows:

(example-function 1)
(example-function 1 :b 2)
(example-function 1 :c 3)
(example-function 1 :b 2 :c 3)
(example-function 1 :c 3 :b 2)
Allow Other Keys
When using keyword arguments, you can allow the function to accept any other keyword arguments without raising an error. This is done by adding the &allow-other-keys keyword.

Binding Variables

Form
In Common Lisp, a “form” refers to an expression or piece of code that can be evaluated. Forms can be simple data like numbers, symbols, or strings, or they can be compound expressions built from lists that represent function calls, macros, or special operators. Forms are the fundamental building blocks of Common Lisp programs, and the language’s syntax and evaluation model revolve around the processing of these forms.
Lexical Scope
Lexical scope, or static scope, is a scoping mechanism in programming languages like Common Lisp that governs the visibility and lifetime of variables and bindings. Variables are accessible only within their defined code region, such as a block, function, or file. Lexical scoping is based on a program’s textual structure, allowing variable scope determination through source code examination, simplifying reasoning about variable bindings without requiring runtime context knowledge.

In Common Lisp, binding refers to the process of associating a variable with a value within a particular lexical scope. This is typically done using special forms such as let, let*, letrec, symbol-macrolet, flet, and labels.

When a variable is bound within a lexical scope, it means that any references to that variable within that scope will use the bound value rather than any global or previous value of the variable. This can be useful for creating local variables that are only used within a specific context, or for creating local bindings for global variables that need to be modified temporarily.

Here are some examples of binding variables in Common Lisp:

(let ((x 10)
      (y 20))
  (+ x y))
30

In this example, let is used to bind two variables, x and y, within the lexical scope of the form. The values of x and y are set to 10 and 20 respectively, and then the sum of x and y is returned as the result of the form.

(let* ((x 10)
       (y (+ x 5)))
  (+ x y))
25

In this example, let* is used to bind two variables, x and y, within the lexical scope of the form. The value of x is set to 10, and then the value of y is set to the sum of x and 5. Finally, the sum of x and y is returned as the result of the form.

(flet ((add (x y)
         (+ x y)))
  (add 10 20))
30

In this example, flet is used to define a local function add within the lexical scope of the form. The add function takes two arguments, x and y, and returns their sum. Then the add function is called with arguments 10 and 20, and the result 30 is returned as the result of the form.

Here’s an example of using labels to define a local function within a lexical scope:

(defun foo (x)
  (labels ((bar (y)
             (* x y)))
    (bar 10)))

(foo 5)
50

In this example, the foo function defines a local function bar using labels. The bar function takes an argument y and multiplies it by the argument x passed to foo. When foo is called with an argument of 5, it creates a lexical scope that includes the bar function definition, and then calls bar with an argument of 10. The result is 50, which is the product of 5 and 10.

The difference between flet and labels is, flet can not be used to define recursive functions. Consider the following examples:

(defun factorial-flet (n)
  (flet ((factorial (n)
           (if (<= n 1)
               1
               (* n (factorial (- n 1))))))
    (factorial n)))
(factorial-flet 2)
The function COMMON-LISP-USER::FACTORIAL is undefined.
   [Condition of type UNDEFINED-FUNCTION]
(defun factorial-labels (n)
  (labels ((factorial (n)
             (if (<= n 1)
                 1
                 (* n (factorial (- n 1))))))
    (factorial n)))
(factorial-labels 2)
2

With flet, the defined function names are visible only within the body of the flet form, while with labels, the defined function names are visible within the entire labels form, including the function definitions themselves.

Some Easy Exercises To Program

Here are some easy tasks to program that can help you start learning programming. These tasks are designed to introduce you to fundamental programming concepts, such as variables, loops, conditionals, and functions.

  • Hello World!: Write a program that outputs “Hello, World!” to the console. This is a classic beginner’s task that helps you get familiar with the basic structure of a program and how to output text.

    (format t "Hello World!")
    
    Hello World!
    
  • User Input: Write a program that asks the user for their name and then prints a personalized greeting, such as “Hello, [Name]!”.

    (format t "Hello, ~A!~%" (read-line))
    
  • Addition Calculator: Write a program that takes two numbers as input and prints their sum. This will help you learn how to work with variables and user input.

    (defun add-two-numbers (a b)
      (format t "The sum is ~A" (+ a b)))
    (add-two-numbers 10 10)
    
  • Odd or Even: Write a program that takes a number as input and outputs whether it’s odd or even. This introduces you to conditional statements (if-else).

    (defun check-even-odd (a)
      (if (evenp a)
          (format t "~A is even" a)
          (format t "~A is odd" a)))
    (check-even-odd 9)
    
    9 is odd
    
  • Multiplication Table: Write a program that prints the multiplication table for a given number. This will help you learn about loops and how to use them for repetitive tasks.

    (defun times-table (number)
      (dotimes (i number)
        (let* ((n (1+ i)) ; dotimes starts at 0, but we want to start at 1
               (x (* n number)))
          (format t "~D | ~D~%" n x))))
    (times-table 9)
    
    1 | 9
    2 | 18
    3 | 27
    4 | 36
    5 | 45
    6 | 54
    7 | 63
    8 | 72
    9 | 81
    
  • FizzBuzz: Write a program that prints the numbers from 1 to 20, but for multiples of 3, print “Fizz” instead of the number, and for multiples of 5, print “Buzz”. For numbers that are multiples of both 3 and 5, print “FizzBuzz”. This task combines loops and conditional statements.

    (defun fizz-buzz (n)
      (cond ((= (mod n 3) 0) (format t "Fizz~%"))
            ((= (mod n 5) 0) (format t "Buzz~%"))
            ((= (mod n 15) 0) (format t "FizzBuzz~%"))
            (t (format t "~D~%" n))))
    
    (dotimes (i 20)
      (let ((n (1+ i)))
        (fizz-buzz n)))
    
    1
    2
    Fizz
    4
    Buzz
    Fizz
    7
    8
    Fizz
    Buzz
    11
    Fizz
    13
    14
    Fizz
    16
    17
    Fizz
    19
    Buzz
    
  • Palindrome Checker: Write a program that checks whether a given word or phrase is a palindrome (reads the same backward as forward). This introduces you to string manipulation and comparison.

    (defun my-reverse (s)
      (let ((x nil))
        (dolist (e (coerce s 'list) x)
          (push e x))
        (coerce x 'string)))
    
    (string= "malayalam" (my-reverse "malayalam"))
    
    T
    
  • Basic Calculator: Expand the addition calculator to include subtraction, multiplication, and division. This will help you learn more about user input, conditionals, and basic arithmetic operations.
  • Prime Number Checker: Write a program that checks if a given number is a prime number. This task will help you learn about loops, conditionals, and mathematical algorithms.

    (defun is-prime (n)
      (let ((primep t))
        (dotimes (i (isqrt n))
          (let ((factor (+ 2 i)))
            (when (= (mod n factor) 0)
              (setf primep nil)
              (return))))
        primep))
    
    (defun primep (n)
      (cond ((< n 2) nil)
            ((= n 2) t)
            (t (is-prime n))))
    
    (primep 7)
    
    T
    
  • Guessing Game: Write a program that generates a random number between 1 and 100 and asks the user to guess the number. The program should provide feedback (too high, too low) and count the number of attempts until the correct number is guessed. This task introduces you to random number generation, loops, and more advanced user interaction.

    (defun main ()
      (let ((random-number (random 100))
            (guess 0)
            (attempts 0))
        (format t "I am thinking of an random number. Make a guess.~%")
        (loop
          (setf guess (parse-integer (read-line)))
          (incf attempts)
          (cond ((= random-number guess)
                 (format t "You guess the correct number ~A~%" random-number)
                 (return))
                ((> random-number guess)
                 (format t "Your guess is too low. You made ~A attempts.~%" attempts))
                ((< random-number guess)
                 (format t "Your guess is too high. You made ~A attempts.~%" attempts))))))
    

Streams

In Common Lisp, streams are objects that represent a source or a destination for a sequence of data elements, such as characters or bytes. They provide an abstraction over input and output operations, allowing you to read from or write to various data sources, like files, network connections, or even in-memory data structures, using a consistent interface.

There are two main categories of streams in Common Lisp:

Input streams
These streams are used for reading data. Common Lisp provides functions like read, read-char, read-byte, and others to read elements from input streams.
Output streams
These streams are used for writing data. Functions like write, write-char, write-byte, and others are available for writing elements to output streams.

Streams can be associated with different types of data sources, such as:

  • Files: Common Lisp provides functions like open and with-open-file to create file streams, which allow you to read from or write to files on disk.
  • Strings: String streams allow you to treat strings as input or output streams. You can use functions like with-input-from-string, with-output-to-string, make-string-input-stream, and make-string-output-stream to create and work with string streams.
  • Network connections: Libraries like usocket provide stream abstractions over network sockets, allowing you to read and write data over network connections using the same stream interface.

One of the advantages of using streams in Common Lisp is that they provide a consistent way to work with different data sources. For example, you can write a function that processes data from an input stream and writes the result to an output stream, and then use that function with file streams, string streams, or network streams without any changes to the function itself.

Exercises

  • Read a text file line by line: Write a function that takes a file path and reads the file line by line, printing each line to the standard output.

    (defun read-file (filepath)
      (with-open-file (in filepath)
        (loop as line = (read-line in nil)
              while line do (format t "~A~%" line))))
    
    (read-file "loremipsum.txt")
    
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
    tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At
    vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
    gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum
    dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
    invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero
    eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
    sea takimata sanctus est Lorem ipsum dolor sit amet.
    
  • Write a text file line by line: Write a function that takes a list of strings and a file path, then writes each string as a separate line in the specified file.

    (defun write-file (lst filepath)
      (with-open-file (out filepath :direction :output :if-exists :supersede)
        (dolist (e lst)
          (format out "~A~%" e))))
    
    (write-file '("hello" "my" "darling") "hello.txt")
    
    cat hello.txt
    
    hello
    my
    darling
    
  • Concatenate multiple text files: Write a function that takes a list of file paths and a destination file path, then concatenates the contents of all the input files into the destination file.

    (defun concat-files (src des)
      (let ((lines nil))
        (dolist (e src)
          (with-open-file (in e)
            (loop as line = (read-line in nil)
                  while line do (push line lines))))
        (write-file (reverse lines) des)))
    
    (concat-files '("hello.txt" "loremipsum.txt") "concat.txt")
    
    cat concat.txt
    
    hello
    my
    darling
    Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
    tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At
    vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
    gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum
    dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
    invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero
    eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
    sea takimata sanctus est Lorem ipsum dolor sit amet.
    
  • Copy a binary file: Write a function that takes a source file path and a destination file path, then copies the contents of the source file to the destination file. This task should be done using binary streams.

    (defun write-binary-file (file-path data)
      (with-open-file (stream file-path
                              :direction :output
                              :element-type '(unsigned-byte 8)
                              :if-exists :supersede
                              :if-does-not-exist :create)
        (write-sequence data stream)))
    
    (write-binary-file "example.bin" (vector 72 101 108 108 111 32 87 111 114 108 100))
    
    72 101 108 108 111 32 87 111 114 108 100
    (defun read-binary-file (file-path)
      (with-open-file (stream file-path :element-type '(unsigned-byte 8))
        (let ((data (make-array (file-length stream))))
          (read-sequence data stream)
          data)))
    
    (read-binary-file "example.bin")
    
    72 101 108 108 111 32 87 111 114 108 100
    (defun copy-binary-file (src-file-path dest-file-path)
      (write-binary-file dest-file-path
                         (read-binary-file src-file-path)))
    
    (copy-binary-file "example.bin" "example2.bin")
    
    72 101 108 108 111 32 87 111 114 108 100
  • Count lines, words, and characters in a text file: Write a function that takes a file path and returns the number of lines, words, and characters in the file. A word is considered a sequence of characters separated by whitespace.
  • Filter lines from a text file: Write a function that takes a file path and a predicate function, then prints the lines in the file that satisfy the predicate.
  • Implement a simple text-based search engine: Write a function that takes a directory path, a search term, and an optional maximum number of results. The function should search all text files in the given directory (recursively if desired) and return the file paths that contain the search term, ordered by the number of occurrences of the term.
  • Implement a simple CSV reader and writer: Write two functions, one for reading CSV files and another for writing them. The reader function should take a file path and return a list of lists, where each inner list represents a row in the CSV file. The writer function should take a list of lists and a file path, then write the data to the specified file in CSV format.
  • Implement a basic line numbering utility: Write a function that takes a file path and a destination file path, then copies the content of the input file to the destination file, adding line numbers at the beginning of each line.
  • Implement a basic grep-like utility: Write a function that takes a file path, a regular expression pattern, and an optional flag to indicate whether to perform a case-insensitive search. The function should print all the lines in the file that match the given pattern.
  • Implement a basic word frequency counter: Write a function that takes a file path and returns a list of the most frequent words and their frequencies in the file. The list should be sorted by frequency in descending order.
  • Implement a simple stream-based encryption/decryption utility: Write two functions for encrypting and decrypting a file using a simple encryption algorithm, such as the Caesar cipher or XOR cipher. Both functions should work with binary streams.

Common Lisp Object System (CLOS)

Generic Functions
Generic functions are like a magic toolbox in programming. Instead of having separate tools for each job, you have one tool that can change its shape to fit different tasks. When you use a generic function, it automatically picks the right tool shape based on the job you’re doing. This makes your work easier because you don’t need a bunch of separate tools for each specific task. Instead, the magic toolbox (generic function) knows how to transform itself to do the job you need.
Multimethods
Multimethods are like having a team of helpers, each with their own special skills. When you need help with a task, you can call your team, and the right helper for the job will step forward based on what the task requires. In programming, a multimethod is a function that can have different actions depending on the types of the things you give it. Instead of having one helper who can only do a specific job, you have a team of helpers that can handle many different situations. This makes your code more flexible and better at handling a variety of tasks.

Here’s a simple example of multimethods and generic functions in Common Lisp using CLOS. Let’s define a generic function called describe-interaction that describes the interaction between two objects, in this case, a cat and a dog.

First, you define the classes cat and dog:

(defclass cat ()
  ((name :initarg :name
         :initform "Unnamed Cat"
         :accessor cat-name)))

(defclass dog ()
  ((name :initarg :name
         :initform "Unnamed Dog"
         :accessor dog-name)))
:initarg
Specifies a keyword argument name that can be used to provide an initial value for the slot when creating an instance of the class using MAKE-INSTANCE or similar functions. For example, in the cat class, you can create a new instance with a specific name using (make-instance ’cat :name “Fluffy”).
:initform
Provides a default initial value for the slot when no value is supplied via an :initarg. If a value is provided through the :initarg, it takes precedence over the default value from :initform. In the cat class, the default name is “Unnamed Cat” if no :name argument is provided.
:accessor
Defines a function to access (get or set) the value of the slot. In the cat class, the accessor cat-name can be used to get the name of the cat with (cat-name some-cat-instance) or set the name with (setf (cat-name some-cat-instance) “New Name”).

Next, you create a generic function called describe-interaction:

(defgeneric describe-interaction (a b)
  (:documentation "Describes the interaction between two objects."))

Now, you define methods for the generic function describe-interaction that specialize on different combinations of cat and dog objects:

(defmethod describe-interaction ((a cat) (b cat))
  (format t "The cats ~A and ~A ignore each other." (cat-name a) (cat-name b)))

(defmethod describe-interaction ((a cat) (b dog))
  (format t "The cat ~A runs away from the dog ~A." (cat-name a) (dog-name b)))

(defmethod describe-interaction ((a dog) (b cat))
  (format t "The dog ~A chases the cat ~A." (dog-name a) (cat-name b)))

(defmethod describe-interaction ((a dog) (b dog))
  (format t "The dogs ~A and ~A play together." (dog-name a) (dog-name b)))

Now, create some instances of cats and dogs:

(defvar *cat1* (make-instance 'cat :name "Fluffy"))
(defvar *cat2* (make-instance 'cat :name "Whiskers"))
(defvar *dog1* (make-instance 'dog :name "Buddy"))
(defvar *dog2* (make-instance 'dog :name "Max"))

Finally, you can call describe-interaction with different combinations of cats and dogs:

(describe-interaction *cat1* *cat2*)
The cats Fluffy and Whiskers ignore each other.
(describe-interaction *dog1* *cat1*)
The dog Buddy chases the cat Fluffy.

Exercises

To practice OOP, you can work on exercises that help you understand and apply its core concepts: classes, objects, inheritance, polymorphism, encapsulation, and abstraction.

  • Simple Class and Object: Create a class representing a geometric shape (e.g., Circle, Rectangle, or Triangle). Define attributes to store the properties of the shape (e.g., radius, width, height) and methods to calculate the area and perimeter.

    (defclass rectangle ()
      ((with :initarg :width
             :initform 0
             :accessor rectangle-width)
       (height :initarg :height
               :initform 0
               :accessor rectangle-height)))
    
    (defgeneric calc-area (rectangle)
      (:documentation "Calc the area of rectangles"))
    
    (defmethod calc-area ((r rectangle))
      (/ (rectangle-width r) (rectangle-height r)))
    
    (defparameter *rect*
      (make-instance 'rectangle
                     :width 100
                     :height 50))
    (calc-area *rect*)
    
    5000
    
  • Bank Account: Implement a Bank Account class with attributes for the account holder’s name, account number, and balance. Include methods for depositing, withdrawing, and displaying the account balance. Create multiple instances of the Bank Account class to represent different accounts.

    (defclass account ()
        ((name :initarg :name
               :initform "Anonymous"
               :accessor account-name)
         (number :initarg :number
                 :initform 1234567
                 :accessor account-number)
         (balance :initarg :balance
                  :initform 0
                  :accessor account-balance)))
    
    #<STANDARD-CLASS COMMON-LISP-USER::ACCOUNT>
    
    (defparameter *elizabeth*
      (make-instance 'account
                     :name "Elizabeth"
                     :number 123
                     :balance "test"))
    
    (defparameter *charles*
      (make-instance 'account
                     :name "Charles"
                     :number 456
                     :balance 200))
    
    *CHARLES*
    
    (account-balance *charles*)
    
    200
    
  • Inheritance: Design a class hierarchy for different types of animals (e.g., Mammal, Reptile, Bird). Create a base class (Animal) with common attributes (e.g., name, age) and methods (e.g., speak). Derive subclasses for specific animal types, each with their own unique attributes and methods.
  • Polymorphism: Expand the animal class hierarchy from the previous exercise. Implement a speak method in each subclass that outputs a unique sound for each animal type (e.g., a dog barks, a cat meows). Demonstrate how you can call the speak method on different animal instances without knowing their specific class, taking advantage of polymorphism.
  • Encapsulation: Design a Car class with attributes like make, model, year, and speed. Implement methods to accelerate, decelerate, and display the current speed. Use encapsulation to prevent the speed from being set directly, ensuring it can only be modified through the acceleration and deceleration methods.
  • Abstraction: Create an Employee class hierarchy with different types of employees, such as Manager, Developer, and Designer. Implement an abstract method calculate_salary in the base Employee class that each subclass must override to provide a specific salary calculation based on the employee type.
  • Composition: Design a Computer class that has components like CPU, RAM, HardDrive, and GPU. Use composition to represent the relationship between the computer and its components. Implement methods in the Computer class to interact with its components, showcasing how composition allows for complex behavior.
  • Design Patterns: Study common design patterns, such as Singleton, Factory, Observer, and Strategy, and implement them in your projects. Design patterns provide solutions to recurring problems in software design and can help you build more efficient and maintainable code.

Remote Lisp Programming Environment

In this guide, I’ll outline how to set up a remote Lisp machine. Typically, programmers write source code on their local machine, commit updates to a local git repository, push changes to a remote git repository, and run a build and deploy pipeline. This pipeline then pushes updates to another remote machine for execution.

With Lisp, an alternative programming model is possible. You can connect to a remote Lisp machine and program it while it’s running. This allows you, as a programmer, to debug and fix your code in real-time.

What do you need?

  • Some kind of VPS. Im using a VPS from Hetzner ☁️. It is cheap and can be started and stoped as needed. This VPS is running Ubuntu 22.04. I ordered the smallest version which is called “CX11”.
  • Local linux machine running Emacs and a terminal e.g Kitty. Im running Emacs version 28.1

We will install SBCL as the lisp machine, load swank server into it, and connect the Emacs client through ssh port forwarding.

There is also a chapter about connecting to a remote lisp machine available at the slime docs.

The objective of this chapter is to establish a functional remote Lisp programming environment.

Setup VPS / Ubuntu

Init / Update The VM

I assume you already create a new virtual machine and you have a fresh system available. I also assume your virtual machine runs Ubuntu.

Change the default root password to a new one:

passwd root

Update the system packages to the newest version:

apt update && apt -y upgrade

If you want, you can install a terminalmultiplexer like tmux. Sometimes it is easier to work with tmux on a remote machine. Also I like to use mosh instead of ssh, because mosh is less restrictive if the connection is lost.

apt install -y tmux mosh

Setup A Standard User

You will do this initial setup as the root user. All work after setting up a new user should be done by using the new user, not the root user.

Create a new user and add it to the sudo group. Do not manage the server with the root user! Later we will disable root login in the sshd config. So no one is able to log in with the root user by ssh.

useradd -G sudo -s /bin/bash -m marcus

useradd is a command in Linux that is used to add user accounts to your system. It is just a symbolic link to adduser command in Linux and the difference between both of them is that useradd is a native binary compiled with system whereas adduser is a Perl script which uses useradd binary in the background. It make changes to the following files:

/etc/passwd

/etc/shadow

/etc/group

/etc/gshadow

creates a directory for new user in /home

Set a good password for your new user:

passwd marcus

Verify that the user was created correctly:

id marcus
uid=1000(marcus) gid=1000(marcus) groups=1000(marcus),27(sudo)

You can get more detailed information of a user by using the finger tool:

apt install -y finger
finger marcus
Login: marcus         			Name:
Directory: /home/marcus             	Shell: /bin/bash
On since Sun Aug 28 07:04 (UTC) on pts/0 from 2003:ca:1f1e:7700:751a:6940:4e3b:3f49
   1 minute 7 seconds idle
On since Sun Aug 28 07:01 (UTC) on pts/1 from 2003:ca:1f1e:7700:751a:6940:4e3b:3f49
   1 second idle
     (messages off)
No mail.
No Plan.

Send the public ssh key from your local machine to the remote machine to be able to log in via ssh with your new created user.

(I assume you only have one public key in your ~/.ssh directory)

ssh-copy-id marcus@u1.metaebene.dev

Automatic systems are constantly scanning the internet for open services and ports. This is a normal thing.

As a security action, using one of the following user names should be avoided:

ubuntu, oracle, rust, rustserver, kubernetes, vagrant, admin, account, pgsql, deploy, hadoop, ansible, user, pi, test, guest, support, xbmc, nagios, debian, minecraft, cisco, www, wwwrun, ftpuser, centos, joomla, puppet, docker, glassfish, bugzilla, wpuser, ts3, ts3server, postgres, jenkins, web, solarwinds, codenvy, mcserver, chef, deployer, ftpadmin, tomcat, wordpress, git, mysql, ec2-user, cloud, csgoserver, csgo, odoo, samsung, apple, acer, systems, maria, centos, cirros, mcserver, mcsrv, es

The following tasks should be done by using the new created user and not the root user.

SSHD Configuration

To be able to secure connect and work on the virtual machine you need to configure and setup sshd.

In simple terms, SSH (Secure Shell) is a protocol used to securely access remote computers over a network. It allows users to log in to a remote system and execute commands as if they were using a local terminal, but with the added security of encryption and authentication to protect the communication from eavesdropping, tampering, and other security threats. SSH also supports file transfers and port forwarding, making it a versatile tool for remote system management and access.

  • Create the sshd_config
    Include /etc/ssh/sshd_config.d/*.conf
    

    Disable login using root

    Disabling root login in the sshd_config file makes it harder for attackers to gain unauthorized access to your server, and reduces the risk of accidental damage to the system. Root accounts have powerful access, and disabling their direct login forces attackers to use other accounts and adds an additional layer of security.

    PermitRootLogin no
    UsePAM no
    

    In general, it is recommended to disable challenge-response authentication, as it is less secure than other methods of authentication, such as public key authentication or password authentication.

    ChallengeResponseAuthentication no
    

    Disable password based login

    PasswordAuthentication no
    

    The AuthenticationMethods publickey setting allows only public key authentication for SSH connections, which is more secure than other methods of authentication like password authentication. It involves generating a key pair and providing the public key to the server, limiting the methods of authentication that an attacker can use to gain unauthorized access to the system.

    AuthorizedKeysFile .ssh/authorized_keys
    AuthenticationMethods publickey
    PubkeyAuthentication yes
    

    Limit Users’ ssh access

    AllowUsers marcus
    DenyUsers root
    

    Disable empty passwords

    PermitEmptyPasswords no
    

    Configure idle log out timeout interval

    ClientAliveInterval 300
    ClientAliveCountMax 0
    

    The X11 protocol is not security oriented. If you don’t need it, you should disable the X11 forwarding in SSH.

    X11Forwarding no
    

    But still enable TCP forwarding, we will use it to secure connect to the lisp machine.

    AllowTcpForwarding yes
    

    Default configs

    Subsystem       sftp    /usr/lib/openssh/sftp-server
    AcceptEnv LANG LC_*
    PrintMotd no
    

    “KexAlgorithms curve25519-sha256@libssh.org” is a cryptographic setting that specifies the use of a key exchange algorithm called “curve25519-sha256” to securely establish a connection between the SSH client and server.

    KexAlgorithms curve25519-sha256@libssh.org
    

    “Ciphers chacha20-poly1305@openssh.com” is a cryptographic setting that specifies the use of a cipher algorithm called “chacha20-poly1305” to secure data during data exchange.

    Ciphers chacha20-poly1305@openssh.com
    

    “MACs hmac-sha2-512-etm@openssh.com” is a cryptographic setting that specifies the use of a MAC (Message Authentication Code) algorithm called “hmac-sha2-512-etm” to ensure data integrity during data exchange.

    MACs hmac-sha2-512-etm@openssh.com
    
  • Copy the sshd_config to the remote maschine

    On your remote machine first create a copy of the default sshd_config:

    cp /etc/ssh/sshd_config ~/sshd_config.bak
    

    Copy the newely created sshd_config to your remote machine into the /etc/ssh directory and restart the sshd server:

    scp sshd_config marcus@u1.metaebene.dev:~/sshd_config
    sudo cp ~/sshd_config /etc/ssh/sshd_config
    sudo systemctl restart sshd
    

Cloud Init

#cloud-config
#Make sure to check the cloud-init logs (/var/log/cloud-init.log and /var/log/cloud-init-output.log)
locale: en_US.UTF-8
keyboard:
  layout: us
timezone: Europe/Berlin

groups:
  - nginxgroup

users:
  - name: nginxuser
    system: true
    shell: /usr/sbin/nologin
    groups: nginxgroup
    sudo: null
  # Create a new user named 'marcus'
  - name: marcus
    # Add the user to the 'users' and 'admin' groups
    groups: users, admin
    # Allow the user to execute any command with sudo without entering a password
    sudo: ALL=(ALL) NOPASSWD:ALL
    # Set the user's default shell to /bin/bash
    shell: /bin/bash
    # Add the user's public SSH key for key-based authentication
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+46Y3AHPLJgz8KK61doqH3jBX2TL3TJvZsJrB9Km03 visua@xps-8930
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMIHJ5qpMIKL7N3nC0GG1O4ygtkqOlQuZReoik6xGBxn marcus@XPS-13-9380.local

packages:
  - fail2ban
  - ufw
  - unattended-upgrades
  - sbcl
  - mosh
  - tmux
  - git
  - mercurial
  - nginx
  - certbot
  - python3-certbot-nginx
  - libev4
  - build-essential
  - sqlite3
  - emacs-nox
  - python3-pip
  - python3-pandas
  - python3-matplotlib

package_update: true
package_upgrade: true

write_files:
  - path: /etc/apt/apt.conf.d/20auto-upgrades
    content: |
      APT::Periodic::Update-Package-Lists "1";
      APT::Periodic::Download-Upgradeable-Packages "1";
      APT::Periodic::AutocleanInterval "7";
      APT::Periodic::Unattended-Upgrade "1";

  - path: /etc/ssh/sshd_config
    content: |
      # Include additional configuration files from the specified directory
      Include /etc/ssh/sshd_config.d/*.conf
      # Set the maximum number of authentication attempts allowed per connection
      MaxAuthTries 3
      # Specifies the file containing public keys for user authentication
      AuthorizedKeysFile .ssh/authorized_keys
      # Disables password authentication
      PasswordAuthentication no
      # Specifies the authentication method(s) to use (public key authentication in this case)
      AuthenticationMethods publickey
      # Enables public key authentication
      PubkeyAuthentication yes
      # Disables root login via SSH
      PermitRootLogin no
      # Disables keyboard-interactive authentication
      KbdInteractiveAuthentication no
      # Enables the Pluggable Authentication Module (PAM) for authentication
      UsePAM yes
      # Disables agent forwarding for SSH connections
      AllowAgentForwarding no
      # Enables TCP forwarding for SSH connections
      AllowTcpForwarding yes
      # Disables X11 forwarding for SSH connections
      X11Forwarding no
      # Disables printing of the message of the day (MOTD) when a user logs in
      PrintMotd no
      # Specifies the key exchange algorithms to use
      KexAlgorithms curve25519-sha256@libssh.org
      # Specifies the ciphers allowed for protocol version 2
      Ciphers chacha20-poly1305@openssh.com
      # Specifies the message authentication code (MAC) algorithms in order of preference
      MACs hmac-sha2-512-etm@openssh.com
      # Specifies environment variables sent by the client to the server
      AcceptEnv LANG LC_*
      # Specifies the command to use for the SFTP subsystem
      Subsystem sftp /usr/lib/openssh/sftp-server
      # Specifies the user(s) allowed to log in via SSH (in this case, only the user "marcus")
      AllowUsers marcus

  # Create a new filter for Nginx
  - path: /etc/fail2ban/filter.d/nginx-http-auth.conf
    content: |
      # Define the filter
      [Definition]
      # Regular expression to match unauthorized requests in Nginx logs
      failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*" 401 .*
      # Regular expressions to ignore (none in this case)
      ignoreregex =

  # Add Nginx jail configuration
  - path: /etc/fail2ban/jail.d/nginx.conf
    content: |
      # Define the jail for Nginx
      [nginx-http-auth]
      # Enable the jail
      enabled = true
      # Specify the filter to use (created earlier)
      filter = nginx-http-auth
      # Define the action to take (using UFW)
      action = ufw
      # Specify the log file to monitor
      logpath = /var/log/nginx/error.log
      # Set the maximum number of failed attempts before banning
      maxretry = 6
      # Set the ban time in seconds (1 hour)
      bantime = 3600
      # Set the time window for failed attempts in seconds (10 minutes)
      findtime = 600

  - path: /etc/fail2ban/jail.local
    content: |
      [DEFAULT]
      # Ban time (in seconds) for an IP after reaching the max number of retries.
      bantime = 3600
      # Time window (in seconds) in which 'maxretry' failures must occur.
      findtime = 600
      # Maximum number of failed login attempts before an IP gets banned.
      maxretry = 3
      # Ban action to use (ufw in this case).
      banaction = ufw

      [sshd]
      # Enable the sshd jail.
      enabled = true
      # Specify the port for the sshd service.
      port = 22
      # Path to the log file for the sshd service.
      logpath = /var/log/auth.log

      [sshd-ddos]
      # Enable the sshd-ddos jail.
      enabled = true
      # Specify the port for the sshd service.
      port = ssh
      # Path to the log file for the sshd service.
      logpath = /var/log/auth.log
      # Maximum number of failed login attempts before an IP gets banned (for DDoS protection).
      maxretry = 5
      # Ban time (in seconds) for an IP after reaching the max number of retries (for DDoS protection).
      bantime = 600

  - path: /etc/nginx/nginx.conf
    content: |
      user nginxuser;
      worker_processes auto;
      pid /run/nginx.pid;
      include /etc/nginx/modules-enabled/*.conf;
      events {
        worker_connections 768;
        # multi_accept on;
      }
      http {
        ##
        # Basic Settings
        ##
        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        # server_tokens off;
        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
        ##
        # SSL Settings
        ##
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;
        ##
        # Logging Settings
        ##
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
        ##
        # Gzip Settings
        ##
        gzip on;
        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
        ##
        # Virtual Host Configs
        ##
        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
      }

  # Write reverse-proxy configuration file
  - path: /etc/nginx/sites-available/reverse-proxy.conf
    content: |
      # Listen on port 80
      server {
        listen 80;
        # Set your domain name
        server_name u1.metaebene.dev;
        # Redirect all requests to HTTPS
        return 301 https://$host$request_uri;
      }

      # Listen on port 443 with SSL
      server {
        listen 443 ssl;
        # Set your domain name
        server_name u1.metaebene.dev;

        # Include SSL certificate managed by Certbot
        ssl_certificate /etc/letsencrypt/live/u1.metaebene.dev/fullchain.pem;
        # Include SSL certificate key managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/u1.metaebene.dev/privkey.pem;
        # Include SSL options provided by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf;
        # Include DH parameters provided by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

        # Proxy settings for the location
        location / {
          # Set backend server address and port
          proxy_pass http://localhost:8080;
          # Set Host header
          proxy_set_header Host $host;
          # Set X-Real-IP header
          proxy_set_header X-Real-IP $remote_addr;
          # Set X-Forwarded-For header
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          # Set X-Forwarded-Proto header
          proxy_set_header X-Forwarded-Proto $scheme;
        }
      }

      server {
        listen 80;
        # Set your domain name
        server_name docs.u1.metaebene.dev;
        # Redirect all requests to HTTPS
        return 301 https://$host$request_uri;
      }

      # Listen on port 443 with SSL
      server {
        listen 443 ssl;
        # Set your domain name
        server_name docs.u1.metaebene.dev;

        # Include SSL certificate managed by Certbot
        ssl_certificate /etc/letsencrypt/live/docs.u1.metaebene.dev/fullchain.pem;
        # Include SSL certificate key managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/docs.u1.metaebene.dev/privkey.pem;
        # Include SSL options provided by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf;
        # Include DH parameters provided by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

        location / {
          root /home/marcus/www/u1/docs/public;
          index index.html;
        }
      }
runcmd:
  # Generate the en_US.UTF-8 locale
  - locale-gen en_US.UTF-8
  # Set the system's default locale to en_US.UTF-8
  - update-locale LANG=en_US.UTF-8
  # Set the system's timezone to Europe/Berlin
  - timedatectl set-timezone Europe/Berlin
  # Run Certbot to obtain SSL certificates and configure Nginx
  - certbot certonly --nginx -d u1.metaebene.dev --non-interactive --agree-tos --email marcus.kammer@mailbox.org --redirect
  - certbot certonly --nginx -d docs.u1.metaebene.dev --non-interactive --agree-tos --email marcus.kammer@mailbox.org --redirect
  # Download DHPARAM
  - curl https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/letsencrypt/ssl-dhparam.pem
  # Create a symlink for the configuration file
  - ln -s /etc/nginx/sites-available/reverse-proxy.conf /etc/nginx/sites-enabled/
  # Remove default Nginx configuration
  - rm /etc/nginx/sites-enabled/default
  # Reload Nginx configuration
  - systemctl reload nginx
  # Allow Nginx Full (HTTP and HTTPS) through the firewall
  - ufw allow 'Nginx Full'
  # Set UFW firewall rules
  - ufw default deny incoming
  - ufw default allow outgoing
  - ufw allow 22/tcp
  - ufw allow mosh
  - ufw enable
  # Enable and start the fail2ban service
  - systemctl enable fail2ban && systemctl start fail2ban
  # Restart the SSH server to apply the new configuration
  - systemctl restart sshd
  - |
    sudo -u marcus git config --global user.email "marcus.kammer@mailbox.org"
    sudo -u marcus git config --global user.name "Marcus Kammer"
    sudo -u marcus git config --global init.defaultBranch main
  # Clone the SBCL repository for a specific branch and depth
  - sudo -u marcus git clone --depth 1 --branch sbcl-2.1.11 git://git.code.sf.net/p/sbcl/sbcl /home/marcus/sbcl
  # Clone the SLIME repository for a specific branch and depth
  - sudo -u marcus git clone --depth 1 --branch v2.28 https://github.com/slime/slime.git /home/marcus/slime
  # Download the Quicklisp installer
  - |
    curl https://beta.quicklisp.org/quicklisp.lisp -o /home/marcus/quicklisp.lisp
    chown marcus:marcus /home/marcus/quicklisp.lisp

Setup SBCL

sudo apt install -y sbcl

Setup Slime

Slime can be “installed” in many different ways. Which way is the best for you is up to you. But you need swank running on your remote machine to connect from your emacs client machine via slime.

Via apt

sudo apt install -y slime

Slime should be installed into the following directory:

/usr/share/common-lisp/source/slime

ls -la /usr/share/common-lisp/source/slime
total 280
drwxr-xr-x 4 root root   4096 Aug 27 13:11 .
drwxr-xr-x 3 root root   4096 Aug 27 13:11 ..
drwxr-xr-x 2 root root   4096 Aug 27 13:11 contrib
-rw-r--r-- 1 root root  53489 Dec 16  2020 metering.lisp
-rw-r--r-- 1 root root  20208 Dec 16  2020 nregex.lisp
-rw-r--r-- 1 root root   5716 Dec 16  2020 packages.lisp
-rw-r--r-- 1 root root  12204 Dec 16  2020 sbcl-pprint-patch.lisp
-rw-r--r-- 1 root root      7 Feb 12  2021 slime-version
-rw-r--r-- 1 root root    690 Dec 16  2020 start-swank.lisp
drwxr-xr-x 2 root root   4096 Aug 27 13:11 swank
-rw-r--r-- 1 root root   1213 Dec 16  2020 swank.asd
-rw-r--r-- 1 root root 142552 Dec 16  2020 swank.lisp
-rw-r--r-- 1 root root  14340 Feb 12  2021 swank-loader.lisp

Via Emacs

To work efficiently, your local slime version should be the same version as the remote machine version, or vice versa. It could be possible the slime version installed by using apt is an old one. If this is the case, it is recommended to install the newest slime version by using emacs:

(toggle-debug-on-error)
(require 'package)
(setq package-user-dir (expand-file-name "~/.emacs-packages")
      package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))
(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

(package-install 'slime)

Copy the emacs lisp file to the remote machine and execute it:

scp install_slime.el marcus@u1.metaebene.dev:~/
emacs -Q --script install_slime.el

This should have slime installed into ~/.emacs-packages/slime

Via Git

Just checkout the git repository and version you want to use, in my case it is version v2.27:

git clone https://github.com/slime/slime.git
cd ~/slime/
git checkout v2.27

Start the remote lisp machine

Start your sbcl:

sbcl

Load the swank stuff into sbcl. I used git to “install” slime, thats why the path to slime is in my home directory:

(load "~/slime/swank-loader.lisp")
(swank-loader:init)

Start the swank server on port 4005:

(swank:create-server :port 4005  :dont-close t)

To autoload swank in the future, add the following code to the ~/.sbclrc file:

(let ((swank (merge-pathnames "slime/swank-loader.lisp" (user-homedir-pathname))))
  (when (probe-file swank)
    (load swank)))

For an easier start of swank save the following lines of code into the ~/start-swank.cl file:

cat ~/start-swank.cl
(swank-loader:init)
(swank:create-server :port 4005 :style :spawn :dont-close t)

This file can then be called using a shell script:

cat ~/start-swank.sh
sbcl --noinform --load start-swank.cl

Connect your client to the remote machine

It is neccessary to secure the connection using ssh before you should connect from your local Emacs to the remote lisp machine. When the connection is secured you can connect from your Emacs client via slime-connect.

(describe-function 'slime-connect)
slime-connect is an autoloaded interactive Lisp closure in ‘slime.el’.

(slime-connect HOST PORT &optional CODING-SYSTEM INTERACTIVE-P &rest
PARAMETERS)

Connect to a running Swank server. Return the connection.

Secure your connection with SSH

To secure the connection to the remote lisp machine we need to use ssh port forwarding.

ssh -L 4005:localhost:4005 marcus@u1.metaebene.dev

When using the Kitty terminal, I recommend connecting with SSH using the +kitten flag, as Ubuntu does not have a terminfo entry for Kitty:

kitty +kitten ssh -L 4005:localhost:4005 marcus@u1.metaebene.dev

More information about ssh port forwarding:

Run Slime From Emacs

If the server is running and you did connect to your remote machine via ssh, the last step is to run slime-connect from your local emacs, connect to localhost on port 4005. And voila, a slime repl is fireing up connected to your remote machine.

Install Quicklisp

Quicklisp is a great tool to load and install other libraries. You can follow this or this tutorial to install quicklisp.

If you are looking for other libraries, have a look at quickdocs.

Let us check and test the environment

Before we go one let us have a last test and check if everything works. We will install spinneret and generate a HTML page.

First install the spinneret dependency using quicklisp:

(ql:quickload :spinneret)
To load "spinneret":
  Load 1 ASDF system:
    spinneret
; Loading "spinneret"

If the package is not yet available in your quicklisp directory under “~/quicklisp/dists/quicklisp/software”, quicklisp will download it automatically. You should now be able to call spinneret from the “CL-USER>” namespace.

(apropos 'spinneret:with-html)
SPINNERET:WITH-HTML (fbound)
SPINNERET:WITH-HTML-STRING (fbound)

You should see both symbols available in the spinneret package. Let us create our own namespace using defpackage. Use (in-package :homepage) in your lisp repl to switch to the :homepage namespace.

(defpackage :homepage (:use cl) (:import-from :spinneret :with-html))
(in-package :homepage)

Let us create a small HTML page. html-style and html-lang are global variables but we are set them locally. Type the following lisp in your repl and you should see the corresponding output.

(let ((spinneret:*html-style* :human)
      (spinneret:*html-lang* "en"))
  (with-html
    (:doctype)
    (:html
     (:head
      (:title "Hello World!"))
     (:body
      (:header
       (:h1 "Hello World"))
      (:p "My first hello world homepage")))))
<!DOCTYPE html>
<html lang=en>
 <head>
  <meta charset=UTF-8>
  <title>Hello World!</title>
 </head>
 <body>
  <header>
   <h1>Hello World</h1>
  </header>
  <p>My first hello world homepage
 </body>
</html>

Setting up pathname translations

One of the main problems with running swank remotely is that Emacs assumes the files can be found using normal filenames. if we want things like slime-compile-and-load-file (C-c C-k) and slime-edit-definition (M-.) to work correctly we need to find a way to let our local Emacs refer to remote files.

Here is an example what kind of setup I use for filename translations. You should change the keywords up to your environment and put into your emacs init.el file:

(let ((u1
       (slime-create-filename-translator
        :machine-instance "u1"
        :remote-host "u1.metaebene.dev"
        :username "marcus")))
  (push hunchentoot slime-filename-translations))

You can found more information on the slime manual.

Networking

Learning networking involves understanding the concepts and protocols that enable communication between computers and devices. You can practice networking by working on exercises that help you understand and apply its core concepts, such as sockets, protocols, data transmission, and security.

Usocket Library

The usocket library is a networking library for Common Lisp, providing an easy-to-use interface for creating and managing network sockets. It allows you to build servers and clients for various network protocols, such as TCP and UDP, and supports both IPv4 and IPv6 addressing. With usocket, you can create network applications like web servers, chat servers, or any other system that requires communication over a network. The library offers functions for creating, listening, accepting, and closing sockets, as well as handling data transfer using socket streams, making it a powerful and flexible tool for network programming in Common Lisp.

usocket:socket-listen

The socket-listen function is like a hotel receptionist preparing a check-in desk for incoming guests. The function takes the host and port as the hotel’s address and sets up the check-in desk (a socket) for guests (clients) to arrive. The backlog parameter determines how many guests can wait in line before new guests are asked to wait outside (the maximum number of pending connection requests). The reuseaddress and reuse-address parameters let the hotel reuse the same address (host and port) for multiple check-in desks, making it easier to manage several events or services simultaneously. When you use the socket-listen function, you’re telling your program (the hotel) to get ready for incoming guests (client connections) by opening a check-in desk (listening socket) at the specified address (host and port).

The socket-listen function creates a TCP listening socket bound to a specific host and port. It accepts keyword arguments to control address reuse, backlog size, and the element type of the socket stream. The function extracts the IP address, creates a socket, sets options, binds it to the address and port, and starts listening for connections. If successful, it returns a stream server-socket object for accepting connections and communicating with clients.

The element-type for sockets in Common Lisp typically refers to the type of data being transmitted over the socket stream. The most common element types are:

  • character: This is the default element type for socket streams in many Common Lisp implementations. It represents textual data, and the socket stream is treated as a sequence of characters.
  • (unsigned-byte 8): This element type represents binary data, and the socket stream is treated as a sequence of bytes. Each byte is an unsigned integer ranging from 0 to 255.
  • (unsigned-byte 16), (unsigned-byte 32), and (unsigned-byte 64): These element types are used for transmitting binary data in larger chunks, such as 2-byte, 4-byte, or 8-byte units.
  • t: This element type represents a general-purpose data type, and the socket stream can handle a mix of data types, such as characters, integers, and floating-point numbers.
(defparameter *example-listener*
  (usocket:socket-listen usocket:*wildcard-host*
                         8082
                         :reuse-address t
                         :element-type '(unsigned-byte 8)))
(describe *example-listener*)
#<USOCKET:STREAM-SERVER-USOCKET {1005295963}>
  [standard-object]

Slots with :INSTANCE allocation:
  SOCKET                         = #<SB-BSD-SOCKETS:INET-SOCKET 0.0.0.0:8082, fd: 3 {100524AEC3}>
  WAIT-LIST                      = NIL
  STATE                          = NIL
  ELEMENT-TYPE                   = (UNSIGNED-BYTE 8)
usocket:wait-for-input

Imagine you’re running a hotel with many rooms. The wait-for-input function is like a hotel receptionist who waits for guests to call from their rooms or for potential guests to call from outside to book a room. The receptionist has a list of phone lines (socket-or-sockets) connected to the rooms and the outside line. They can wait for a specific amount of time (timeout) before giving up on waiting for any calls. When the receptionist receives a call, they note down which phone line the call came from. If the ready-only option is set, the receptionist will only report the phone lines with incoming calls. Otherwise, they’ll report all the phone lines they were monitoring, regardless of whether they received a call or not. At the end, the receptionist returns two things: a list of phone lines (either all the lines or only those with incoming calls), and the time remaining until their shift ends (or nil if their shift goes on indefinitely).

The wait-for-input function is used to monitor one or more sockets (or streams) and check if any of them are ready for reading. It accepts a single socket or a list of sockets as its input, along with an optional timeout value and a ready-only flag. The function starts by creating a wait-list for the sockets if it hasn’t been created by the caller. If the input consists of a single socket, it reuses the per-socket wait-list if available. The function then loops through the wait-list’s waiters (i.e., sockets) and sets their state based on whether they have data available for reading. If there’s at least one socket ready for reading, the function calls wait-for-input-internal with a timeout value of 0, which essentially polls the internal routine. Otherwise, it waits for the specified timeout period or indefinitely if no timeout is specified. Once the waiting is done, the function calculates the remaining time (if any) within the timeout period and returns two values: a list of sockets (either all the original sockets or only those that are ready for reading, based on the ready-only flag), and the remaining time within the timeout period (or nil if no timeout was specified). This function is useful for handling multiple connections concurrently, such as in a server application that needs to respond to incoming client requests.

usocket:socket-accept

Imagine a hotel with a doorkeeper who is responsible for letting guests in. The socket-accept function is like the doorkeeper who is waiting to greet and allow new guests to enter the hotel (accepting new client connections). When a guest arrives (a client connects), the doorkeeper (the function) creates a room key (a new stream-socket object) for the guest. This key allows the guest to access their room and communicate with the hotel staff (send and receive data). The room key is set up based on the guest’s preferences (the element-type). Once the guest has been allowed entry and has received their room key (the stream-socket object is returned), the doorkeeper is ready to greet the next guest. If the doorkeeper is momentarily unavailable due to a distraction (like a non-blocking I/O or interrupted system call on Windows), they make sure to get back to their post and continue waiting for the next guest to arrive.

The socket-accept function accepts a new client connection from a listening server socket. It creates a higher-level stream-socket object with input and output streams, using the raw socket representing the established connection. The function returns the stream-socket object for communication with the connected client. It handles errors using the with-mapped-conditions macro and, on Windows systems, resets the server socket’s ready state if necessary.

usocket:socket-stream

This function can be seen as the hotel’s internal communication system, like a telephone line or a messaging service, that connects the guests to the hotel staff. Once a guest is checked in (a connection is established), the communication system (the socket stream) allows the guest and the hotel staff to exchange messages (send and receive data) throughout their stay. This function creates a stream object that enables smooth data transfer between the server and the client over the established connection.

This function creates a stream object associated with a connected socket. The stream object simplifies data transfer between the server and the client by providing an abstraction over the raw socket. Using the stream object, you can read from and write to the socket using familiar Common Lisp I/O functions like read, write, read-char, and write-char.

These functions work together to create a networking system in Common Lisp:

  • First, the server sets up a listening socket using usocket:socket-listen.
  • The server then waits for incoming connection requests or data using usocket:wait-for-input.
  • When a connection request arrives, the server accepts the connection using usocket:socket-accept, which returns a new connected socket.
  • The server then creates a stream object for the connected socket using usocket:socket-stream, enabling easy communication with the client using Common Lisp I/O functions.

These steps enable the server to establish connections with clients and exchange data using the established socket connections and associated streams.

Exercises

  • Echo Server and Client: Implement a simple TCP server and client that communicate using sockets. The server should echo back any message sent by the client. This will help you understand socket programming, TCP connections, and client-server architecture.
  • Chat Application: Create a basic chat application using sockets that allows multiple clients to connect and send messages to each other. This will introduce you to networking concepts, concurrent programming, and message handling.
  • File Transfer: Implement a file transfer protocol (FTP) client and server that can upload and download files. This exercise will help you understand data transmission, file I/O, and error handling in a networking context.
  • Web Server: Build a simple web server that serves static files (e.g., HTML, CSS, images) and responds to HTTP requests. This will help you learn about HTTP, request/response handling, and MIME types.
  • Web Client: Write a program that makes HTTP requests to a given URL, retrieves the web page, and parses specific information from the page (e.g., extracting links or images). This will help you practice HTTP requests, HTML parsing, and regular expressions.
  • UDP Communication: Implement a UDP server and client that exchange messages using datagrams. This will help you understand the differences between TCP and UDP, as well as how to handle unreliable and connectionless communication.
  • Network Scanner: Create a network scanner that scans a given IP address range or a list of hostnames to determine if specific ports are open. This will introduce you to network discovery, port scanning, and basic security concepts.
  • DNS Resolver: Implement a basic DNS resolver that takes a domain name as input and retrieves the corresponding IP addresses by sending a DNS query to a DNS server. This will help you learn about the Domain Name System (DNS) and how domain names are resolved to IP addresses.
  • Proxy Server: Develop a proxy server that acts as an intermediary between a client and a server, forwarding client requests and server responses. This will introduce you to the concept of proxies, request forwarding, and caching.
  • Network Security: Explore network security by implementing a basic encryption/decryption system for data transmitted between a client and server. This will help you learn about cryptography, key exchange, and secure communication.

Web Development in Common Lisp

Bevor you start hacking on your remote lisp machine do not forget to start the swank server! Personally, I prefer to save commands like this in a shell script file:

cat ~/start-swank.sh
sbcl --noinform --load start-swank.lisp

This shell script can then be executed with the following command:

bash start-swank.sh

Play around with the hunchentoot web server

Hunchentoot is a Common Lisp web server that supports HTTP and HTTPS, and includes features for URL routing, cookies, sessions, and form handling. It is designed to be modular and extensible, and can be used with various Lisp implementations.

Load the hunchentoot package into your lisp environment:

(ql:quickload :hunchentoot)
:HUNCHENTOOT

Note: The package ’hunchentoot is also be accessible through the ’tbnl symbol.

First, create an acceptor instance

In Hunchentoot, an acceptor is an object that listens for incoming connections from clients and accepts them. The hunchentoot:acceptor class provides a way to create and manage acceptors.

Acceptors are responsible for establishing and managing connections between clients and the server, and for processing incoming requests from clients. They typically listen on a specific IP address and port, and can be configured to handle different types of connections (e.g. HTTP or HTTPS).

In Hunchentoot, you can create a new acceptor by instantiating an instance of the hunchentoot:acceptor class with appropriate configuration options, such as the port to listen on, SSL settings, and connection limits. Once created, the acceptor can be started and stopped using the hunchentoot:start and hunchentoot:stop functions respectively.

Acceptors can be used in conjunction with other Hunchentoot components, such as request handlers and dispatchers, to build a complete web server application. They provide a way to manage incoming connections and route requests to the appropriate handlers, while also providing options for scalability and performance tuning.

(defparameter *u1-server*
  (make-instance 'tbnl:easy-ssl-acceptor
                 :name "u1"
                 :port 8080
                 :document-root "~/www/u1/"
                 :access-log-destination "~/www/u1/access-log.log"
                 :ssl-certificate-file #p"~/certs/u1.metaebene.dev/fullchain.pem"
                 :ssl-privatekey-file #p"~/certs/u1.metaebene.dev/privkey.pem"))
*U1-SERVER*

This code creates a new variable named “u1-server” and assigns it the value of a new instance of a class called “tbnl:easy-ssl-acceptor”. The instance is created with several arguments that specify the name, port, document root, access log location, SSL certificate and private key file for the server.

If you want to know more details about a symbol or object (instance) you can use the (describe) function to print details:

(describe *u1-server*)
#<HUNCHENTOOT:EASY-SSL-ACCEPTOR (host *, port 8080)>
  [standard-object]

Slots with :INSTANCE allocation:
  PORT                           = 8080
  ADDRESS                        = NIL
  NAME                           = "u1"
  REQUEST-CLASS                  = HUNCHENTOOT:REQUEST
  REPLY-CLASS                    = HUNCHENTOOT:REPLY
  TASKMASTER                     = #<HUNCHENTOOT:ONE-THREAD-PER-CONNECTION-TASKMASTER {1004446DB3}>
  OUTPUT-CHUNKING-P              = T
  INPUT-CHUNKING-P               = T
  PERSISTENT-CONNECTIONS-P       = T
  READ-TIMEOUT                   = 20
  WRITE-TIMEOUT                  = 20
  LISTEN-SOCKET                  = NIL
  LISTEN-BACKLOG                 = 50
  ACCEPTOR-SHUTDOWN-P            = T
  REQUESTS-IN-PROGRESS           = 0
  SHUTDOWN-QUEUE                 = #<SB-THREAD:WAITQUEUE Anonymous condition variable {1004447A93}>
  SHUTDOWN-LOCK                  = #<SB-THREAD:MUTEX "hunchentoot-acceptor-shutdown" (free)>
  ACCESS-LOG-DESTINATION         = #<SB-SYS:FD-STREAM for "file /home/marcus/www/u1/access-log.log" {1003..
  MESSAGE-LOG-DESTINATION        = #<SWANK/GRAY::SLIME-OUTPUT-STREAM {1003791BC3}>
  ERROR-TEMPLATE-DIRECTORY       = #P"/home/marcus/quicklisp/dists/quicklisp/software/hunchentoot-v1.3.0/..
  DOCUMENT-ROOT                  = #P"~/www/u1/"
  SSL-CERTIFICATE-FILE           = "/home/marcus/certs/u1.metaebene.dev/fullchain.pem"
  SSL-PRIVATEKEY-FILE            = "/home/marcus/certs/u1.metaebene.dev/privkey.pem"
  SSL-PRIVATEKEY-PASSWORD        = NIL

Stop and start the acceptor instance

To start the webserver use the tbnl:start function:

(tbnl:start *u1-server*)
#<HUNCHENTOOT:EASY-SSL-ACCEPTOR (host *, port 8080)>

To stop the webserver use the tbnl:stop function:

(tbnl:stop *u1-server*)
#<HUNCHENTOOT:EASY-SSL-ACCEPTOR (host *, port 8080)>

Author: Marcus Kammer

Email: marcus.kammer@mailbox.org

Date: (2023-11-06T17:23+01:00)

Emacs 29.1.90 (Org mode 9.6.11)

License: CC BY-SA 3.0