It’s a sunny day. Birds are singing, the wind is blowing, the sun is shining, and your language doesn’t support multiple-inheritance. You really don’t want to switch languages, but the way objects and classes are implemented irks you. So much bloat. So many unnecessary features, yet the most important one is missing!
Today we’ll define our own OOP-implementation.
But surely that’s something best left to committees? I’ve never made my own class system before, I always use store-bought! And how long would this all take? Wouldn’t I have to write thousands of lines of code?
Not quite.
In this post, you’ll see how single-inheritance OOP can be implemented in 28 lines of code, and multiple-inheritance OOP in 42.
Depending on the specific implementation of OOP, you may work via message-passing, you could have classes with methods, or you could have generic methods that dispatch on the methodless classes of the argument(s) passed.
Of the three listed OOP features, message-passing is demonstrated in Smalltalk; class-methods are present in most all C-descendent-languages like C++, Java, C#, Pascal, Python; and generic-methods are present in Common Lisp, aswell as many of the C-descendents.
To get started, I’ll hardcode a closure representing an “object” of the nonexistent “counter” class. This closure is a function packaged with a lexical environment, it’s a let over a lambda:
(defparameter my-cool-closure
(let ((count 0))
(lambda () (incf count))))
Calling the closure (which, again, is just an anonymous function inside its own lexical scope) causes it to increment the count variable, which is only visible to that anonymous function.
CL-USER> (funcall my-cool-closure)
1
CL-USER> (funcall my-cool-closure)
2
CL-USER> (funcall my-cool-closure)
3
Now let’s say we wanted more than one of these counter “objects”. We’ll create another anonymous function that, when called, produces the anonymous function that is the counter closure:
(defparameter my-cool-closure-maker
(lambda ()
(let ((count 0))
(lambda () (incf count)))))
And create a list that holds the result of calling my-cool-closure-maker three times, thus holding three separate counter “objects”:
CL-USER> (list (funcall my-cool-closure-maker)
(funcall my-cool-closure-maker)
(funcall my-cool-closure-maker))
(#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFCAB}>
#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFCFB}>
#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFD4B}>)
I’m going to bind this list to the symbol my-cool-closures, so we can work with it a bit:
CL-USER> (setq my-cool-closures *)
(#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFCAB}>
#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFCFB}>
#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime40") {101B0EFD4B}>)
Here I map over all the counters and funcall them, incrementing them:
CL-USER> (mapcar (lambda (cool-closure) (funcall cool-closure)) my-cool-closures)
(1 1 1)
But each counter has its own state, and can be funcalled separately. Here I map over the same counters, calling each one a random number of times, then once more to see what their current count is:
CL-USER> (mapcar (lambda (cool-closure)
(dotimes (i (random 150))
(funcall cool-closure))
(funcall cool-closure))
my-cool-closures)
(47 103 41)
This is cool and all, but the “class” isn’t anything more than a factory. Where are the class-variables? Let’s create two: “increment” and “decrement”, which are only visible to closures created by the “class” closure (it was a function, but now that it has state, it’s a closure):
(defparameter my-cool-closure-maker
(let ((increment 3)
(decrement 10))
(lambda ()
(let ((count 0))
(lambda () (incf count increment))))))
Now when we call the closure-maker, it returns a closure that increments its own instance-variable count by the class-variable increment:
CL-USER> (setq my-cool-closure (funcall my-cool-closure-maker))
#<CLOSURE (LAMBDA () :IN "H:/RenTemp/slime42") {101B3CFF8B}>
CL-USER> (funcall my-cool-closure)
3
CL-USER> (funcall my-cool-closure)
6
CL-USER> (funcall my-cool-closure)
9
To show that this class-variable is in fact shared by all instances, I’ll make another list of three closures and increment them a random number of times:
CL-USER> (mapcar (lambda (cool-closure)
(dotimes (i (random 150))
(funcall cool-closure))
(funcall cool-closure))
(list (funcall my-cool-closure-maker)
(funcall my-cool-closure-maker)
(funcall my-cool-closure-maker)))
(81 288 51)
And as we can see, their counts are all divisible by three:
CL-USER> (mapcar (lambda (x) (mod x 3)) `(81 288 51))
(0 0 0)
We have an issue though. Our instances have only one method, “funcall”, making the decrement class-variable superfluous. To remedy this, we’ll implement a message-passing style dispatch within our instances. Here’s a simple example of an anonymous function that, depending on the value passed to it when funcalled, calls within itself a function bound to one of the symbols within its private lexical environment:
CL-USER> (let* ((count 0)
(inc (lambda () (incf count)))
(dec (lambda () (decf count))))
(lambda (message)
(case message
(:inc (funcall inc))
(:dec (funcall dec))
(t count))))
#<CLOSURE (LAMBDA (MESSAGE)) {101B7F461B}>
CL-USER> (setq my-cool-dispatch-closure *)
#<CLOSURE (LAMBDA (MESSAGE)) {101B7F461B}>
Having bound that “dispatch-closure” to my-cool-dispatch-closure, we’ll funcall it with the message :inc to increment, :dec to decrement, and any other value to invoke the default case which just returns the current count of the object:
CL-USER> (funcall my-cool-dispatch-closure :inc)
1
CL-USER> (funcall my-cool-dispatch-closure :inc)
2
CL-USER> (funcall my-cool-dispatch-closure :inc)
3
CL-USER> (funcall my-cool-dispatch-closure :dec)
2
CL-USER> (funcall my-cool-dispatch-closure :dec)
1
CL-USER> (funcall my-cool-dispatch-closure nil)
1
Splendid, though our anonymous function can only accept one argument. The object, obviously, may want to contain functions with different argument counts. The remedy is to use #’apply, while changing our arg-list from a simple lambda-list to an extended-lambda-list, letting us use &rest to denote that the function being made is to be variadic, and to accumulate all passed args into a list:
CL-USER> (let* ((count 0)
(inc (lambda () (incf count)))
(dec (lambda () (decf count))))
(lambda (&rest messages)
(case (car messages)
(:inc (apply inc (cdr messages)))
(:dec (apply dec (cdr messages)))
(t count))))
#<CLOSURE (LAMBDA (&REST MESSAGES)) {101BBA324B}>
CL-USER> (setq my-cool-dispatch-closure *)
#<CLOSURE (LAMBDA (&REST MESSAGES)) {101BBA324B}>
CL-USER> (funcall my-cool-dispatch-closure :dec)
-1
CL-USER> (funcall my-cool-dispatch-closure :dec)
-2
CL-USER> (funcall my-cool-dispatch-closure :dec)
-3
CL-USER> (funcall my-cool-dispatch-closure)
-3
The “car” of the messages parameter is the head of the linked list that makes it up, the “cdr” is the tail. The tail of a linked-list of length 1, in Common Lisp, is nil. Nil is an empty list. Applying a function with nil as its args causes #’apply to call the given function without passing any args at all. Very useful.
Now let’s improve our prior cool closure class to create objects that can decrement, increment, and return their current counts:
(defparameter my-cool-closure-maker
(let ((increment 3)
(decrement 10))
(lambda ()
(let* ((count 0)
(inc (lambda () (incf count increment)))
(dec (lambda () (decf count decrement))))
(lambda (&rest messages)
(case (car messages)
(:inc (apply inc (cdr messages)))
(:dec (apply dec (cdr messages)))
(t count)))))))
And see that our instances now have multiple methods, one to increment by 3, one to decrement by 10, and one to simply return their current count:
CL-USER> (setq my-cool-closure (funcall my-cool-closure-maker))
#<CLOSURE (LAMBDA (&REST MESSAGES) :IN "H:/RenTemp/slime43") {101BBCE54B}>
CL-USER> (funcall my-cool-closure :inc)
3
CL-USER> (funcall my-cool-closure :inc)
6
CL-USER> (funcall my-cool-closure :inc)
9
CL-USER> (funcall my-cool-closure :dec)
-1
CL-USER> (funcall my-cool-closure :dec)
-11
CL-USER> (funcall my-cool-closure :dec)
-21
CL-USER> (funcall my-cool-closure)
-21
Moving away from this more basic rundown, we want a class to have class-variables that can be retrieved and modified. This means the class itself must have multiple methods: a method to create an instance, a method to get the value of a class-variable, a method to set the value of a class variable, and (for hypothetical later use, if we continued hardcoding) a method to return all the class-variables of a class along with their current values.
(defparameter my-cool-class
(let* ((class-variables nil)
(class-variable-set (lambda (class-variable-name class-variable-value) (setf (getf class-variables class-variable-name) class-variable-value)))
(class-variable-get (lambda (class-variable-name) (getf class-variables class-variable-name)))
(class-variables-get (lambda () class-variables))
(make-instance (lambda ()
(let* ((count 0)
(inc (lambda () (incf count (funcall class-variable-get :increment))))
(dec (lambda () (decf count (funcall class-variable-get :decrement)))))
(lambda (&rest messages)
(case (car messages)
(:inc (apply inc (cdr messages)))
(:dec (apply dec (cdr messages)))
(t count)))))))
(funcall class-variable-set :increment 3)
(funcall class-variable-set :decrement 10)
(lambda (&rest messages)
(case (car messages)
(:class-variable-get (funcall class-variable-get (cadr messages)))
(:class-variable-set (funcall class-variable-set (cadr messages) (caddr messages)))
(:class-variables-get (funcall class-variables-get))
(:make-instance (funcall make-instance))
(t 'invalid-message)))))
I’ve done a few things here, but a couple things to notice are that there’s a class-variables symbol. It’s a property-list, which is a list filled with pairs of keys and values, like so: (:x 1 :y 2 :z 3). Individual values can be fetched with (getf class-variables :key), and set by wrapping the getter in a setf form. Think of it as a discount hashtable, if you don’t understand.
Calling the function bound to my-cool-class with the message :make-instance produces a new function, which we bind to my-cool-instance, which we then verify to be working as expected:
CL-USER> (setq my-cool-instance (funcall my-cool-class :make-instance))
#<CLOSURE (LAMBDA (&REST MESSAGES) :IN "H:/RenTemp/slime50") {101CC54B7B}>
CL-USER> (funcall my-cool-instance :inc)
3
CL-USER> (funcall my-cool-instance :dec)
-7
And again with three separate instances, incremeting them a random number of times each and then decrementing them each once:
CL-USER> (setq my-cool-instances (list (funcall my-cool-class :make-instance)
(funcall my-cool-class :make-instance)
(funcall my-cool-class :make-instance)))
(#<CLOSURE (LAMBDA (&REST MESSAGES) :IN "H:/RenTemp/slime50") {101CFBF5BB}>
#<CLOSURE (LAMBDA (&REST MESSAGES) :IN "H:/RenTemp/slime50") {101CFBF63B}>
#<CLOSURE (LAMBDA (&REST MESSAGES) :IN "H:/RenTemp/slime50") {101CFBF6BB}>)
CL-USER> (mapcar (lambda (cool-instance)
(dotimes (i (random 3))
(funcall cool-instance :inc))
(funcall cool-instance))
my-cool-instances)
(6 6 3)
CL-USER> (mapcar (lambda (cool-instance)
(funcall cool-instance :dec))
my-cool-instances)
(-4 -4 -7)
We now call our class with the a message to change the value of the class-variable :increment to 100, and increment each instance again to see they are, in fact, referencing the class variable:
CL-USER> (funcall my-cool-class :class-variable-set :increment 100)
100
CL-USER> (mapcar (lambda (cool-instance)
(funcall cool-instance :inc))
my-cool-instances)
(96 96 93)
That’s classes, class-variables, class-methods, instances and instance-variables down.
To implement inheritance, I’d like to first stop hardcoding this one class. I want to make many classes, and having to type all this drivel out each time is unacceptable. To do so, I’m going to make a macro that constructs the code to make a class, based on given lists of instance-variables, methods, class-variables and class-methods. [1]
(defmacro make-class (&optional instance-vars methods class-vars class-methods)
`(let* (,@class-vars
(metadata (quote (:instance-vars ,instance-vars
:methods ,methods
:class-vars ,class-vars
:class-methods ,class-methods))))
(lambda (&rest args)
(apply
(case (car args)
(:definition (lambda () metadata))
,@class-methods
(:make-instance (lambda ()
(let* ,instance-vars
(lambda (&rest args)
(apply
(case (car args)
,@methods
(t (lambda () 'invalid-message-for-instance)))
(cdr args))))))
(t 'invalid-message-for-class))
(cdr args)))))
We’ve done away with a good deal of the code, all the stuff about getting/setting class-variables and class-methods have been replaced by simply injecting the passed code directly into let-forms and case-statements.
The class itself is a lexically-wrapped variadic anonymous function that reads in the passed arguments into a case statement to try find an existing class-method to call. If one is found, the cdr of the args are passed as the args for the method, which is called. If no class-method is found, then the symbol INVALID-MESSAGE-FOR-CLASS is returned. [2]
If the message passed in args is :make-instance, then a function is called to create and return a closure with all the instance-vars and methods injected into it. That closure itself uses the same logic as the class to evaluate and dispatch on the args (message) passed to it, determining which of its methods to call, and if none found, returning the symbol INVALID-MESSAGE-FOR-INSTANCE.
If the message (args) passed’s head is :definition, the class returns a list of its instance-vars, methods, class-vars and class-methods. This will be needed later for inheritance. Using a macro allows us to store the unevaluated, quoted code that was passed to make-class for later introspection.
With that done, let’s make a class with two class-variables [increment 3, decrement 1], and class methods to set those two variables:
CL-USER> (setq counter-class (make-class ((count 0))
((:inc (lambda () (incf count increment)))
(:dec (lambda () (decf count decrement))))
((increment 3)
(decrement 1))
((:inc-set (lambda (new-inc) (setf increment new-inc)))
(:dec-set (lambda (new-dec) (setf decrement new-dec))))))
#<CLOSURE (LAMBDA (&REST ARGS)) {101386812B}>
And make an instance…
CL-USER> (setq my-counter (funcall counter-class :make-instance))
#<CLOSURE (LAMBDA (&REST ARGS)) {10138C20BB}>
Then demonstrate the instance…
CL-USER> (funcall my-counter :inc)
3
CL-USER> (funcall my-counter :inc)
6
CL-USER> (funcall my-counter :inc)
9
CL-USER> (funcall my-counter :dec)
8
Change the class-variable for :decrement from 1->100…
CL-USER> (funcall counter-class :dec-set 100)
100
And demonstrate that the change is reflected in our extant instance:
CL-USER> (funcall my-counter :dec)
-92
Wonderful.
Now, inheritance.
Since everything we need to construct a class is stored in its definition, merging them is trivial.
We will need to modify our make-class macro a bit though, adding a new argument for the parent-class, and then some code to append our new class data to the end of our parent class data. Note that variables are in the order parent->child, while methods are ordered child->parent. This is because serial-let-forms override older definitions with newer ones, while case statements grab the first match they see.
(defmacro make-class (&optional parent-class instance-vars methods class-vars class-methods)
(let* ((parent-class (eval parent-class))
(parent-definition (when (functionp parent-class)
(funcall parent-class :definition)))
(instance-vars (append (getf parent-definition :instance-vars) instance-vars))
(methods (append methods (getf parent-definition :methods)))
(class-vars (append (getf parent-definition :class-vars) class-vars))
(class-methods (append class-methods (getf parent-definition :class-methods))))
`(let* (,@class-vars
(metadata (quote (:instance-vars ,instance-vars
:methods ,methods
:class-vars ,class-vars
:class-methods ,class-methods))))
(lambda (&rest args)
(apply
(case (car args)
(:definition (lambda () metadata))
,@class-methods
(:make-instance (lambda ()
(let* ,instance-vars
(lambda (&rest args)
(apply
(case (car args)
,@methods
(t (lambda () 'invalid-message-for-instance)))
(cdr args))))))
(t 'invalid-message-for-class))
(cdr args))))))
And now for the classic OOP demonstration: coloured shapes.
We’ll create a polygon class with the class-variable sides, and a method “speak”, along with “sides” to return the number of sides the polygon has.
CL-USER> (setq polygon-class (make-class nil
((sides 4))
((:speak (lambda () (format nil "I am a polygon with ~a sides" sides))))
nil
((:sides (lambda () sides)))))
#<FUNCTION (LAMBDA (&REST ARGS)) {10132C43AB}>
CL-USER> (setq my-polygon (funcall polygon-class :make-instance))
#<FUNCTION (LAMBDA (&REST ARGS)) {10132C4DFB}>
CL-USER> (funcall my-polygon :speak)
"I am a polygon with 4 sides"
CL-USER> (funcall my-polygon :sides)
4
Then we’ll create a shape class that inherits from polygon and has the variable “colour”, and overrides the method “speak”. For demonstration purposes, I’ll be adding in a class-variable to shape called shade, and a method to change the value of shade.
CL-USER> (setq shape-class (make-class polygon-class
((colour 'red))
((:speak (lambda () (format nil "I am a ~a ~a shape with ~a sides" shade colour sides))))
((shade 'dark))
((:shade-set (lambda (new-shade) (setf shade new-shade))))))
#<CLOSURE (LAMBDA (&REST ARGS)) {1014BC3DBB}>
CL-USER> (setq my-shape (funcall shape-class :make-instance))
#<CLOSURE (LAMBDA (&REST ARGS)) {1014BF519B}>
CL-USER> (funcall my-shape :speak)
"I am a DARK RED shape with 4 sides"
We call the function to change the class-variable shade, from dark->light:
CL-USER> (funcall shape-class :shade-set 'light)
LIGHT
And ask our dear shape to speak once more:
CL-USER> (funcall my-shape :speak)
"I am a LIGHT RED shape with 4 sides"
All variables are private by default. You need to explicitly create getters and setters to manage them. We could change our shape’s colour by creating a method such as
(:colour-set (lambda (new-colour) (setf colour new-colour))).
And there it is, friends, a 28 line OOP-implementation.
I did mention multiple-inheritance earlier though, so here that is in 42 lines:
(defun merge-classes (classes)
(let ((instance-vars)
(methods)
(class-vars)
(class-methods))
(loop for definition in (mapcar (lambda (class) (funcall class :definition)) classes)
do (setf instance-vars (append (getf definition :instance-vars) instance-vars)
methods (append methods (getf definition :methods))
class-vars (append (getf definition :class-vars) class-vars)
class-methods (append class-methods (getf definition :class-methods))))
(list :instance-vars instance-vars
:methods methods
:class-vars class-vars
:class-methods class-methods)))
(defmacro make-class (&optional parent-classes instance-vars methods class-vars class-methods)
(let* ((parent-classes (remove-if-not #'functionp (mapcar #'eval parent-classes)))
(parent-definition (merge-classes parent-classes))
(instance-vars (append (getf parent-definition :instance-vars) instance-vars))
(methods (append methods (getf parent-definition :methods)))
(class-vars (append (getf parent-definition :class-vars) class-vars))
(class-methods (append class-methods (getf parent-definition :class-methods))))
`(let* (,@class-vars
(metadata (quote (:instance-vars ,instance-vars
:methods ,methods
:class-vars ,class-vars
:class-methods ,class-methods))))
(lambda (&rest args)
(apply
(case (car args)
(:definition (lambda () metadata))
,@class-methods
(:make-instance (lambda ()
(let* ,instance-vars
(lambda (&rest args)
(apply
(case (car args)
,@methods
(t (lambda () 'invalid-message-for-instance)))
(cdr args))))))
(t 'invalid-message-for-class))
(cdr args))))))
Along with a quick demonstration:
(setq counter-class (make-class nil ((count 0)) ((:inc (lambda () (incf count increment)))) ((increment 1)) nil))
(setq colour-class (make-class nil ((colour 'red)) ((:speak (lambda () (format nil "i am ~a ~a" shade colour)))) ((shade 'light)) ((:shade-set (lambda (new-shade) (setf shade new-shade))))))
(setq leg-class (make-class nil ((legs 4)) ((:speak (lambda () (format nil "i have ~a legs" legs)))) nil ((:legs-set (lambda (new-legs) (setf legs new-legs))))))
(setq multi-class (make-class (colour-class counter-class leg-class) nil ((:speak (lambda () (format nil "I am a ~a ~a, ~a legged amalgamation that can count to ~a" shade colour legs count))))))
CL-USER> (setq my-multi (funcall multi-class :make-instance))
#<CLOSURE (LAMBDA (&REST ARGS)) {101777BA2B}>
CL-USER> (funcall my-multi :inc)
1
CL-USER> (funcall my-multi :inc)
2
CL-USER> (funcall my-multi :inc)
3
CL-USER> (funcall my-multi :speak)
"I am a LIGHT RED, 4 legged amalgamation that can count to 3"
Could it be shorter? Yes.
Could it have more features? It is decidedly so.
Could it be more performant? Signs point to yes.
Could it be more robust? Reply hazy, try again.
Could it be more robust? Ask again later.
Could the interface be streamlined? Outlook good.
Could it be more robust? Reply hazy, try again.
Stupid Magic 8 Ball.
Now why on earth would you want to implement OOP yourself? A few reasons.
Firstly, it’s fun.
Secondly and most obviously, you’re now no longer working with a blackbox abstraction that is your language’s OOP system. You know exactly why, how and when classes and objects are made.
Another reason is that, when a language is designed, the designers must choose one implementation of OOP that they hope will suit the needs of everyone using the language. OOP can be implemented in various ways, and each of those ways have benefits and drawbacks. If your language’s implementation doesn’t match your project’s needs, you can define your own. No multiple inheritance? Set it up yourself. Want generic methods? Go ahead. Is the default OOP implementation too bloated and slow? Make your own version with only the essentials.
This implementation is something like a message-passing, multiple-inheritance OOP.
Fourthly, you’re not limited to using just one implementation of OOP in your program.
As I said, each implementation has benefits and drawbacks and you may want, for example, NPCs in your videogame to use an implementation of OOP tailored for that requirement. You’d then want terrain objects to use a different version of OOP with less overhead for simpler objects in larger amounts. You’d maybe then want a third implementation of OOP for handling items in your game. By implementing your own OOP, you can tailor-make it to be exactly suited for the job at hand.
And to go even further beyond, one could define another layer of abstraction over the set of OOP implementations you’ve made, creating a selector that determines which OOP implementation to use based off some provided values.
For example;
(make-class (parent-classes) (instance-vars) (methods) (class-vars) (class-methods) (choose-oop-implementation :speed 3 :memory 2 :introspection 0))
Wherein an OOP implementation is chosen by the priorities listed. The arguments to this selector function can, too, be programmatically determined. Consider two OOP-implementations; A and B. A uses more CPU cycles but less memory, while B uses less CPU cycles but more memory. You could query the user’s system for how much memory has been allocated to the program, and translate that into the :memory argument to the selector func. The same could be done for all other arguments provided to the selector.
This all comes back to the idea that the design of a language is in fact a point in the latent-space of language-designs, that point often being chosen by committee to be generic and mildly suit everyone’s needs instead of perfectly fitting the needs of the few. By taking up the reigns of language design ourselves, we can programatically select, even during runtime, from various points within the language-design-space to best suit our needs at each specific moment. [3]
Having a dispatch/selector function to determine which OOP implementation to use does, naturally, now mean that the set of OOP implementations you have is incrementally extensible. You can work in a data-driven fashion, because OOP implementations are code, and code is data.
You might even want to start making OOP implementations into objects of the OOP-IMPLEMENTATION class, to leverage the benefits OOP provides in managing data.
But then, what OOP implementation should the OOP-IMPLEMENTATION class use? [4]
=)
—————–
[1] I’m using a macro here so I can inject the passed functions into the lexical environment of the class. This needs to be done because Common Lisp, like basically every other language, captures the environment around a function when it’s created, NOT when it’s called. If it were a function instead of a macro, all the arguments passed would be evaluated before the function itself is called, creating the methods and class-methods outside the desired environment. Using a macro lets us delay their evaluation until they’re fitted snugly in their class’ nest of cozy parentheses.
[2] This macro could stand to be prettified, but the sun is setting on my Sunday and I’ve got other things to do.
[3] Having multiple methods to achieve the same goal is also a virtue unto itself; we see it in biology with degeneracy. Our bodies have methods to digest food in various ways, we can digest carbohydrates for energy, fats for energy, proteins for energy. If you’re born with a mutation that breaks your protein-digestion method, you may yet still live by eating carbs and fats. If some change to the dominant operating-system in the future breaks one OOP-implementation we’ve created, the program may yet still live by automatically falling-back onto another OOP-implementation. The same can be said across all layers of abstraction in the strata that makes up our programs; selectors can be made to choose the best data-types, algorithms, functions, language-design-choices, etc, for the situation at hand, falling back onto alternative choices in the event the current choice is for some reason not working. In having multiple ways to reach the same goal (the program running), it becomes robust, it gains degeneracy. This is a topic for another post though.
[4] I suppose we could write a selector function to dispatch based off the user’s queried system specs. Though, in the event that the selector function fails, it’d help if we had a selector function to choose another selector function. But if the selector-function-selector-function fails? And now that we have so many selector functions, should we manage them as objects? With what OOP-implementation? Maybe we can write a selector-function to choose the OOP-implementation to manage the selector-function-selecting-functions to select OOP-implementations…
It’s no use, Mr. James—it’s turtles all the way down.