Janet Object Oriented Detour

You've heard it said Object Oriented Programming bad and Functional Programming good. However, what if OOP actually not bad? What if many game concepts map cleanly to object model? What if I've been reading too much grug brain developer?...

Anyways.. I was recently inspired by this lisp's class implementation with a state machine built into it and wanted to recreate it in Janet! Before going further, you'll need to read up on how Janet does OOP. You'll also want to read through the macro docs if you're new to this sort of thing. When you're ready here's the syntax we'll be shooting for..

(def-class Circle
  (field :x)
  (field :y)
  (field :color RED)
  (field :radius 3)
  (field :area (* math/pi radius radius))

  (defn draw-common [self]
    (tic80/circ (self :x) (self :y) (self :radius) (self :color)))

  (def-state :normal
    (defn draw [self]
      (:draw-common self)))

  (def-state :outlined
    (defn draw [self]
      (:draw-common self)
      (tic80/circb (self :x) (self :y) (self :radius) (self :color)))))

(var circle1 (-> (Circle/init {:x 10 :y 40 :radius 10 :color BLUE})
		 (GOTO :normal)))

(:draw circle1)

Step 1: The most basic possible start

But where to begin? Well with the simplest possible thing.

(defmacro def-class [name]
  ~(def ,name @{}))

This macro does nothing more then create a variable of name and assigns it to an empty table @{}. Hopefully it makes sense whats going on!

Step 2: Prototype Functions

The next step is to parse a function definition into fields in the table with the fn's as the values.

(def-class Circle
  (defn draw-common [self]
    (tic80/circ
      (self :x) (self :y)
      (self :radius) (self :color))))

The trick here is to realize that the values within def-class is really a list of values! So we'll iterate through each "defn form" and map the form to a tuple which can used by from-pairs to create a table. Note that there's some extra logic related to being able to actually match the form.


# you cant really Match symbols,
# so lets convert the first part into a keyword
(defn first-as-keyword [[form-symbol & rest]]
  [(keyword form-symbol) ;rest])

(defn to-class-prototype [forms]
  (->> forms
       (map |(match (first-as-keyword $)
	       [:defn name & body]
	       (tuple (keyword name)      # name in the table
		      ~(fn ,name ,;body)) # the actual function
	       _ []))
       (from-pairs)))                     # converts pairs into table

(defmacro def-class [name & forms]
  ~(def ,name ,(to-class-prototype forms)))

This macro converts this code...

(def-class Circle
  (defn draw-common [self]
    (tic80/circ
      (self :x) (self :y)
      (self :radius) (self :color))))

into this neat prototype table definition!

(def Circle
  @{:draw-common
    (fn draw-common [self]
      (tic80/circ
	(self :x) (self :y)
	(self :radius) (self :color)))})

Step 3: Nested States

Step 4: The init function

Step 5: The GOTO function