Let Over Lambda uLisp edition


#1

Intro

Since I started using ulisp I was wondering about how to do structs (named access to variables) and encapsulation/namespacing (using the same names in different contexts without having them clash). ULOS is fine but isn’t it since it lacks encapsulation and offers more freedom than I’d like. As I pondered these questions I remembered my training: “closures a poor man’s objects…” whispered ancient wisdom. As I looked down this path I found it seldom explored. Everyone agrees with the concept but few seem to have applied it, opting for CLOS instead.

I did find some sources however. The big one is the aptly named Let Over Lambda, another is SICP chapter 3 (which I never got to when I tried reading that book), also this blog which is ironically down as I write this. They offer solid explanations of how to use the concept of Let Over Lambda to create objects/structs. That said they’re all different enough from ulisp that I wanted to write my own tutorial since I haven’t encountered people using it in this language (though maybe I missed something?)

Basic Immutable Structs

At its simplest we can use this technique to create constant encapsulated data. This offers an alternative to lists because it allows named access to variables, and can’t be expanded like a list can. So lets make a struct that holds immutable x and y coordinates as an example.

(defun pos (x y)
   (lambda (&optional msg) 
       (case msg 
           (x x) 
           (y y) 
           (t (cons x y)))))

This is the struct prototype, which lets us construct structs

(defvar p1 (pos 3 5))

Then we can use it like

> (p1 'x)
3

> (p1 'y)
5

> (p1)
(3 . 5)

How it works

The pos function returns a closure that holds the variables we called it with. The closure contains a case statement which returns the variables by name. It’s interesting to note that we don’t seem to need a let statement to hold our variables and that you can use the same names for the case index and variables (let me know if this is intended behavior or not, but it seems pretty robust so far). Also note that while we return a closure from pos we put it into a variable with defvar, but then we call it like as a function by enclosing it in parenthesis. Finally, we use the t condition to return a cons cell when you call the closure without any parameters, cause we can.

More Advanced

This is all regular ulisp code, so we’re not at all bound by anything. We can stick all kinds of cases into our case statement. For example we can add an atan option to calculate the angle of the pos

(defun pos1 (x y)
   (lambda (&optional msg) 
       (case msg 
           (x x) 
           (y y) 
           (atan (atan x y))
           (t (cons x y)))))

(defvar p1 (pos1 3 5))

> (p1 'atan)

Mutable Structs

We can also modify the variables inside, but we need to take in more than one argument to do that. To do that we replace &optional with &rest which packages the arguments into a list. Then we take the car to index into the case statement and destructure the rest of the list depending on what we need.

(defun pos2 (x y) 
     (lambda (&rest msg) 
            (case (car msg) 
                      (x x) 
                      (y y) 
                      (set-x (setf x (cadr msg))) 
                      (set-y (setf y (cadr msg))))))

> (defvar p2 (pos2 5 8))
p2

> (p2 'x)
5

> (p2 'set-x 123)
123

> (p2 'x)
123

Another trick we can do is toss an eval statement into the lambda to allow arbitrary code execution with the variables that the closure is holding.

(defun pos3 (x y) 
        (lambda (&rest msg) 
                (apply eval msg)))

> (defvar p3 (pos3 1 4))
p3

> (p3 'x)
1

> (p3 '(setf x 43))
43

> (p3 'x)
43

Structs and Functions

What happens when we pass these structs into functions?

> (defun fun (p) (p 'set-x 23))
fun

> (fun p2)
23

> (p2 'x)
23

It seems like we have “pass by reference” semantics, so the closure isn’t copied. I actually didn’t know that until I wrote this section, whoops. When I wanted to reference a variable by reference I was taking advantage of ulisp’s dynamic scoping, which is messy.

> (defun fun2 () (p2 'set-x 47))

> (fun2)
47

> (p2 'x)
47

But now I’m wondering how to copy closures/structs. Maybe copy constructors? Let me know if you figure it out.

Functions Inside Structs

We can create functions that are local to structs by using writing them inline with the case index like we showed with the atan example earlier. We can also define them in the let statement, but we have to wrap them in a lambda since we can’t use defun.

 (defun pos4 (x y) 
	(let* ((set-pos (lambda (x_ y_) (setf x x_ y y_)))) 
     (lambda (&rest msg) 
		(case (car msg) 
		  (x x) 
		  (y y) 
		  (set-x (setf x (cadr msg))) 
		  (set-y (setf y (cadr msg)))
		  (set-pos (apply set-pos (cdr msg)))))))

> (defvar p4 (pos4 21 17))
p4

> (p4 'x)
21

> (p4 'set-pos 7 8)
8

> (p4 'x)
7

> (p4 'y)
8

Struct Inheritance

We can emulate inheritance simply by creating a struct inside a struct and forwarding any case indexes that aren’t taken to it. If we want to override anything we can create that index in the outer struct and it’ll see it first.

(defun incrementor ()
	(let* ((count 0))
    (lambda (&rest msg) 
		(case (car msg) 
		  (inc (incf count))
		  (dec (decf count))
		  (t count))))) ; returns the count if no arguments are given

> (defvar i (incrementor))
i

> (i 'inc)
1

> (i 'inc)
2

> (i)
2

(defun superincrementor ()
  (let* ((i (incrementor)))
   (lambda (&rest msg) 
		(case (car msg)
			(inc (dotimes (x 5) (i 'inc)) (i)) ; overrides the inc method
			(t (apply i msg))))))   ; passes the commands to the parent class

> (defvar si (superincrementor ))
si

> (si 'inc)
5

> (si)
5

> (si 'dec)
4

Drawbacks

I didn’t do any performance testing but I suspect it’ll be slower. That said afaik ulisp already does lookups every time it uses any variable or function so maybe it’ll be fine. It is also pretty boilerplatey with all the lets and lambdas everywhere. The most annoying part is introspection isn’t good unless you write print functions or accessors for all the relevant variables. This makes it tricky to debug. I often stick an eval clause into the structs so I can mess with them. There might be other tricks that I haven’t figured out.

Sample Application

To test all these concepts I developed a little GUI application that lets you scroll through the ulisp functions and see their documentation. You can check it out here, its available for both t-deck and cardputer. It showcases all these concepts in action in a more real world scenario. It’s pretty messy though cause I was figuring things out as I wrote it. Enjoy!


#2

Great concept IMHO. For me, the inheritance mechanism is somewhat hard to grasp and to read, but apart from that this looks straightforward. I really like how “object methods” are simply encapsulated in the “case” structure, so the objects responds to messages without any ado.


#3

Thanks for the feedback. Yea I kinda phoned the inheritance example in cause I’m not a fan of OOP and dislike most examples of inheritance (car is a vehicle, rectangle is a shape, etc). But it’s also the primary way I know how to write stuff like GUIs. So here’s a different example, let me know if it makes more sense to you. It’s all just Lisp code, no special constructs or syntax.


#4

Indeed, that’s easier to grasp now, and again that looks really elegant. For me, the concept seems to show the power of Lisp: With no additional syntactic elements you are able to concisely implement this system as you develop it, even with inheritance and override. Your closure based approach also looks strangely close to Python’s OOP, while ULOS seems more related to the JavaScript system. At least that’s my impression. Thanks a lot for sharing this! I suspect this approach might even run somewhat faster than ULOS, but that waits to be proven.


#5

Let Over Lambda is one of my favorite Lisp books, very cool to see this. I have some ideas to build out a code editor as an extensions of the doc-browser now.


#6

For what it’s worth if you can also package local functions in closures such that they don’t pollute the toplevel namespace. I did this in Clojure out of spite for their anti OOP dogma. :)
Scheme uses a similar method to create namespace packages.


#7

It seems like you had the same idea that I did here. I’m not exactly pro OOP myself but ulisp does lack namespacing and encapsulation mechanisms so it made sense here.

@picolisper If you want to build a code editor you can check out some prior art, such as Ginkgo’s lispbox for the teensy and my port of it for the t-deck as well as andreer’s potatop. All those were written before this post so they don’t use the techniques described here, but they do contain a lot of code that’s needed for text/code editing.


#8

I was working on converting your port over (biggest thing is missing get-key) but I broke my screen — a replacement is on the way, but in the meantime I’m on the t-deck.