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!