Building Objects from Scratch in Lisp
I was interested in a discussion on HN, and it reminded me of a few months ago when I ended up writing a make-shift object system in scheme, almost entirely by accident. It was eye-opening to understand the simple blocks that it was all built on. So to that end, Iâd like to explore the fundamentals of object-oriented programming.
Goal
Build a simple character model using the most basic building blocks available.
Weâll give the character the following characteristics:
- name - The characterâs name
- health - Remaining health (can change)
- agility - The likelihood of dodging an attack
- strength - The amount of damage inflicted when attacking
Check out this gist if youâd like to jump straight to the source code.
From the Basics
Weâll start by making a list of these attributes:
'("Musashi" 100 50 80)
Thatâs a start. We have a list, and we can just remember (for now anyway), the order of attributes. For example, to get the characterâs health, we could grab the 2nd item in the list.
But itâs really just a list of stuff. Not only is it unpleasant to type in all the time, itâs impossible to access more than one element - each time we type in that list, a new list is created, completely unrelated to any earlier lists!
What we need is a way to refer to the same list in a convenient way. Lispâs LET will do just this:
(let ((musashi '("Musashi" 100 50 80)))
(list-ref musashi 1))
;; => 100
Much better. Now letâs make some nice accessor functions:
(define (get-name character) (list-ref character 0))
(define (get-health character) (list-ref character 1))
(define (get-agility character) (list-ref character 2))
(define (get-strength character) (list-ref character 3))
Now we can read the code a bit better:
(get-health musashi) ;; => 100
Encapsulate!
Letâs make a constructor:
(define (make-character name health agility strength)
(list name health agility strength))
MAKE-CHARACTER is a tiny, tiny wrapper around list, but we get two nice advantages: First, our code is much more readable. And second, weâve completely removed any knowledge of how the internals of a character are handled from the code using it. Weâve encapsulated that functionality away into the few functions that have to know about it.
Encapsulation is an important component of object-oriented programming.
Another Enters the Arena
Can we handle two characters?
(let ((musashi (make-character "Musashi" 100 50 80))
(kojiro (make-character "Kojiro" 100 40 90)))
(display-status musashi)
(display-status kojiro))
Can you feel the tensions growing already? I feel the need for an ATTACK function:
(define (attack attacker defender)
(let ((damage (get-strength attacker)))
(set-health! defender (- (get-health defender) damage))))
A Battle Brewing
Letâs allow them their normal behavior with a few extra functions and watch them fight:
(let ((musashi (make-character "Musashi" 100 50 80))
(kojiro (make-character "Kojiro" 100 40 90)))
(battle musashi kojiro))
Epic indeed! Looks like our old friend Kojiro faired a bit better in our simulation than he did in real life.
Epilogue
But isnât this just a bunch of functions? Yes, and no. These are all just a bunch of functions, but still able to maintain state and interact with these states through an interface. And thatâs the heart of OOP.
This approach is very much âa lot of functions, few objectsâ which I prefer, but it takes some getting used to. Itâs also a bare-bones object system that can be expanded any which way (though probably shouldnât be for anything production-oriented) - polymorphism, inheritance, etc. There are hundreds of variations we could go into: sticking more state into the closures, sticking the functions into the closures, putting more into the functions and less into the closure, etc., but the first step is building something and seeing it grow. So give building your own object system a try!