public
Last active

A basic OOP system in lisp - see http://trapm.com/building-an-objects-from-scratch-0

  • Download Gist
basic_oop.lisp
Common Lisp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
;; I was interested in the following: http://news.ycombinator.com/item?id=1917917, 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 a 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
 
;; We'll start by making a list of these attributes
(list "Kojiro" ;; name
100 ;; health
10 ;; agility
15) ;; strength
 
;; ("Kojiro" 100 10 15)
 
;; 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 (remember lisp lists are 0-indexed), like this:
 
(nth 1 (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15))
 
;; 100
;; It works! But it's really just a list of stuff. Not only is 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!
 
(nth 1 (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15)) ;; strength
;; 100
 
(nth 0 (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15)) ;; strength
;; "Kojiro"
 
;; What we need is a way to refer to the *same* list in a convenient way. Lisp's LET will do just this:
 
(let ((character (list
"Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(nth 0 character) ;; "Kojiro"
(nth 3 character)) ;; 15
 
;; First, that just *looks* much better. But more importantly, we have a way of accessing the same list again and again. What if we made some nice functions for getting the attributes out?
 
(defun name (character)
(nth 0 character))
 
(defun health (character)
(nth 1 character))
 
(defun agility (character)
(nth 2 character))
 
(defun strength (character)
(nth 3 character))
 
;; Now we can read the code a bit better
(name (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15))
;; Kojiro
 
(strength (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15))
;; 15
 
;; Now we can use those functions inside of the LET as well:
(let ((character (list "Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(name character) ;; "Kojiro"
(strength character)) ;; 15
 
;; *Much* more readable. I like it already. There's also a subtle transition happening - we're starting to treat this list a bit less like a list, and more like an object. Not very much, but ever so slightly. If we later changed from a list to some other data structure, and also updated our name/strength/etc. functions to match, then the code above would continue working. They're implementation agnostic, as it were. Except for where we make the character (using LIST). Let's fix that:
 
(defun 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. Compare the previous example with the following:
 
(let ((character (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(name character) ;; "Kojiro"
(strength character)) ;; 15
 
;; 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, and provided a clear separation. Encapsulation is an important component of object-oriented programming. What if we tried to modify one of the values, for example, to give ol' Kojiro a nice health boost:
 
(let ((character (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(setf (second character) 150)
character)
;; ("Kojiro" 150 10 15)
 
;; Nice, we've definitely helped out Kojiro here. Because of lisp-specific semantics, we can't just use (setf (health character) 150), as nice as that seems. But we could just whip up a quick function that was easier to read anyway!
 
(defun set-health (character health)
(setf (second character) health))
 
;; Our new code should look a bit nicer:
(let ((character (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(set-health character 150)
character)
;; ("Kojiro" 150 10 15)
 
;; More readable code, and the same effect! Let's make a few helper functions here to explore this new layer:
 
(defun boost-health (character amount)
(set-health character (+ (health character) amount)))
 
(defun damage-health (character amount)
(set-health character (- (health character) amount)))
 
(defun print-health (character)
(format t "~A has ~A health~%" (name character) (health character)))
 
;; Format's a formidable function, but where you see ~A, it just means "replace this position with some real value" which comes after the string. So we print out the characters name, their health, and the ~%, which means "add a newline character here".
 
;; Let's try out some of this new code:
(let ((character (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15))) ;; strength
(print-health character)
(boost-health character 15)
(print-health character)
(damage-health character 75)
(print-health character))
 
;; Kojiro has 100 health
;; Kojiro has 115 health
;; Kojiro has 40 health
 
;; This is already getting exciting. We don't have any traces of the underlying implementation, and we have a make-shift object handling its own state. Can we handle two?
 
(let ((kojiro (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15)) ;; strength
(musashi (make-character "Musashi" ;; name
100 ;; health
5 ;; agility
25))) ;; strength
(damage-health kojiro (strength musashi))
(print-health kojiro)
(damage-health musashi (strength kojiro))
(print-health musashi))
 
 
;; Kojiro has 75 health
;; Musashi has 85 health
 
;; Can you feel the tentions growing already? I feel the need for an ATTACK function
(defun attack (attacker defender)
(damage-health defender (strength attacker)))
 
;; Simply enough, we damage the defender by the strength of the attacker. Let's look at it in action:
 
(let ((kojiro (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15)) ;;strength
(musashi (make-character "Musashi" ;; name
100 ;; health
5 ;; agility
25))) ;; strength
(print-health kojiro)
(print-health musashi)
(attack kojiro musashi)
(attack musashi kojiro)
(print-health kojiro)
(print-health musashi))
 
;; Kojiro has 100 health
;; Musashi has 100 health
;; Kojiro has 75 health
;; Musashi has 85 health
 
;; It's all a bit too formulaic though. What is we allowed some variance on the damage, and also gave the defender a chance to evade based on their agility?
 
(defun attack (attacker defender)
(let* ((evaded? (< (random 100) (agility defender)))
(attack-variance (* (/ (random 10) 10) (strength attacker)))
(attack-strength (round (if (= 0 (random 2))
(+ (strength attacker) attack-variance)
(- (strength attacker) attack-variance)))))
(if evaded?
(format t "~A dodged ~A's attack!~%" (name defender) (name attacker))
(damage-health defender attack-strength))))
 
;; Let's see how the fight goes down now:
 
(let ((kojiro (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15)) ;; strength
(musashi (make-character "Musashi" ;; name
100 ;; health
5 ;; agility
25))) ;; strength
(print-health kojiro)
(print-health musashi)
(attack kojiro musashi)
(attack musashi kojiro)
(print-health kojiro)
(print-health musashi))
 
;; Kojiro has 100 health
;; Musashi has 100 health
;; Kojiro dodged Musashi's attack!
;; Kojiro has 100 health
;; Musashi has 86 health
 
 
;; Intense, but not as epic as we might hope. These characters are not known for fighting a bit and relenting. Let's allow them their normal behavior with a few extra functions:
 
;; Can we add a dead? method?
(defun dead? (character &optional quiet)
(if (not quiet)
(format t "Is ~A dead?~%" (name character) (health character) (<= (health character) 0)))
(<= (health character) 0))
 
(defun won (victor vanquished)
(format t "We shall sadly never hear from ~A again, for ~A has been triumphant.~%" (name vanquished) (name victor)))
 
(defun fight-round (character-1 character-2)
(let ((first-attacker (if (> (agility character-1) (agility character-2))
character-1
character-2))
(second-attacker (if (> (agility character-1) (agility character-2))
character-2
character-1)))
(attack first-attacker second-attacker)
(if (dead? second-attacker t)
t
(attack second-attacker first-attacker))
(print-health character-1)
(print-health character-2)))
 
(defun duel (character-1 character-2)
(fight-round character-1 character-2)
(cond ((dead? character-1) (won character-2 character-1))
((dead? character-2) (won character-1 character-2))
(t (duel character-1 character-2))))
 
;; Alright, Let's play a round:
(let ((kojiro (make-character "Kojiro" ;; name
100 ;; health
10 ;; agility
15))
(musashi (make-character "Musashi" ;; name
100 ;; health
5 ;; agility
25))) ;; strength) ;; strength
(duel kojiro musashi))
 
;; Kojiro dodged Musashi's attack!
;; Kojiro has 100 health
;; Musashi has 76 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro has 78 health
;; Musashi has 50 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro has 63 health
;; Musashi has 41 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro dodged Musashi's attack!
;; Kojiro has 63 health
;; Musashi has 35 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro has 38 health
;; Musashi has 17 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Musashi dodged Kojiro's attack!
;; Kojiro dodged Musashi's attack!
;; Kojiro has 38 health
;; Musashi has 17 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro has 28 health
;; Musashi has 7 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; Kojiro has 28 health
;; Musashi has -8 health
;; Is Kojiro dead?
;; Is Musashi dead?
;; We shall sadly never hear from Musashi again, for Kojiro has been triumphant.
;;
;; Epic indeed!
;; But isn't this just a bunch of functions? Yes, and no. These are all just a bunch of functions, but we're using closures to maintain the state. And that's really all Object-oriented programming is, at its heart - closures, and functions that manipulate them. This is a bare-bones object system that can be expanded any which way (though probably shouldn't for anything production-oriented) - polymorphism, inheritance, etc.. There are hundreds of variations on this that 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!

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.