Powered by AppSignal & Oban Pro

Working with Objects

livebooks/working_with_objects.livemd

Working with Objects

use AL

Setup

Connect this notebook to a running iex --sname al -S mix session first — see the intro for connection steps.

AL.Branch.head()

Creating durable classes and instances

AL has an object system in the style of CLOS and Smalltalk, and aims to make writing live, open polymorphisms easy. The details of this will become more apparent later, but the basic ideas you need to know right now are that you can create classes with defclass, and instances of those classes with new. Let's try the well-known example of a light switch, which has on/off state.

We use the redef flag so that the class & instance can be redefined by re-evaluating the block.

run do
  defclass :switch,
    super: :object,
    redef: true,
    ivars: [state: [domain: ["on", "off"], default: "off"]] do

    defmethod(:flick, [self]) do
      get_slot(self, :state, "on")
      set_slot(self, :state, "off")
    end
    
    defmethod(:flick, [self]) do
      get_slot(self, :state, "off")
      set_slot(self, :state, "on")
    end
  end

  new(:switch, %{name: :switch_instance_1, state: "on", redef: true}, s)
end

In AL, durable objects are represented by an ID, which currently takes the form of an atom (soon, we will change these to binaries, as it can eventually fill up the atom table). You can inspect basic information about a durable object using examine/2.

run do
  examine(:switch_instance_1, info)
end
run do
  examine(:switch, info)
end

As you can see, :switch is a class which inherits from :object. It provides the method :flick, which we defined. It was used to create :switch_instance_1. :switches have a 'state' which is either on or off. Let's see what happens if we 'flick' the 'switch'.

run do
  flick(:switch_instance_1)
  get_slot(:switch_instance_1, :state, state)
end

It should work! And if you evaluate again, you'll see the state change too. AL is also capable of enforcing slot invariants.

run do
  set_slot(:switch_instance_1, :state, :sideways)
end

Inheritance

As you may have already figured out, AL supports inheritance. This is not the scope to explain how inheritance itself works, but instead we will demonstrate a couple features that may be of benefit. For example, you can use call_next_method for method continuation and rely on inherited slots.

run do 
  defclass :counted_switch,
    redef: true,
    super: :switch,
    ivars: [count: [type: :number, default: 0]] do
    
    defmethod(:flick, [self]) do
      get_slot(self, :count, c)
      vm_is(c1, c + 1)
      set_slot(self, :count, c1)
      call_next_method(self)
    end
   end

  new(:counted_switch, %{name: :counted_switch_instance_1, redef: true}, c)
end
run do 
  flick(:counted_switch_instance_1)
  examine(:counted_switch_instance_1, info)
end

In fact, AL even supports multiple inheritance. Let's take a look at the classic diamond inheritance problem. To demonstrate this, we'll need another kind of switch. Let's take one that flips its colour every time that it is turned on. For now we have a couple of limitations: Firstly, we must label the var, since constraints aren't durable in the database. Secondly, we don't have an ordering on the domain, meaning we can't express an exact cycle.

run do
  defclass :colour_cycle_switch,
    redef: true,
    super: :switch,
    ivars: [colour: [domain: ["red", "green", "blue"]]] do

    defmethod(:flick, [self]) do
      get_slot(self, :state, "off")
      get_slot(self, :colour, c)
      in_domain(c1, ["red", "green", "blue"])
      dif(c, c1)
      label(c1)
      set_slot(self, :colour, c1)
      call_next_method(self)
    end
    
    defmethod(:flick, [self]) do
      get_slot(self, :state, "on")
      call_next_method(self)
    end
  end

  new(:colour_cycle_switch, %{name: :cc_switch_instance_1, colour: "red", redef: true}, c)
end
run do
  flick(:cc_switch_instance_1)
  examine(:cc_switch_instance_1, info)
end

Regardless this is sufficient for demonstrating how AL manages diamond inheritance. Here's a little tip: you can search a method by its object-associated name using the following snippet. For example:

run do
  findall([obj, id], [vm_method(obj, :flick, id)], objids)
end
run do
  defclass :diamond_switch,
    redef: true,
    super: [:counted_switch, :colour_cycle_switch] do
  end

  new(:diamond_switch, %{name: :diamond_switch_instance_1, colour: "red", state: "off", redef: true}, c)
end

The order in which the two supers are topologically sorted actually doesn't matter for flick, since the operations we defined are commutative.

run do
  flick(:diamond_switch_instance_1)
  examine(:diamond_switch_instance_1, info)
end

Ephemeral Objects (Values)

So far we have only discussed 'durable' objects, without defining what this means. Each durable object in AL has a backing existence in the underlying Mnesia relation tables (which as you will see later, is just a RAM-based view over the command log).

Ephemeral/Value objects, on the other hand, only exist for the lifespan of a transaction, which means they do not clog up the database and are perfect for calculation. This means new objects can be generated 'on the fly' without dirtying the database.

A nice thing about ephemerals is that new gets to decide what the representation of an ephemeral actually is. In fact, numbers and lists are both ephemerals.

run do
  super(:number, :value)
end