Skip to content

Object Orientation in zeptoforth

tabemann edited this page Dec 4, 2022 · 8 revisions

zeptoforth as of version 0.44.0 comes with an object system included, in the oo module. Its object system uses single-inheritance, by default uses late binding, and does not provide a namespace mechanism except when early binding is in use. One must use modules if one wishes to limit one's namespace while using zeptoforth's object system. All classes ultimately inherit from the <object> class, for which the constructor and destructor methods new and destroy are declared.

zeptoforth's object system is agnostic to how objects are stored in memory; all it cares about is that an object is stored in memory of size equal to class-size applied to the object's class. Objects may be stored in a dictionary, a heap, an array, a structure, or another object. The only real restriction upon objects is that they must be cell-aligned, or otherwise attempting to use them will crash on Cortex-M0+ MCU's such as the RP2040.

Declaring the methods and members which belong to a class and its subclasses are distinct from defining methods' implementations in a class and any subclasses which do not override it. This can be seen here:

oo import
<object> begin-class <my-class>
  cell member my-member
  method my-method
end-class
<my-class> begin-implement
  :noname
    dup <object>->new
    0 swap my-member !
  ; define new
  :noname my-member @ 1+ ; define my-method
end-implement

Note that every class must have both a begin-class...end-class block and a following begin-implement...end-implement block even if it declares no methods or members or defines no methods. Methods are declared in a begin-class...end-class block with method and members are declared with member as can be seen above. Execution tokens can be bound as method definitions with define in a begin-implement...end-implement block.

Note that abstract methods simply are methods which are not implemented in a class or its superclasses or which are implemented with abstract-method. Note that these are equivalent, as unimplemented methods are automatically implemented with abstract-method. This word raises x-method-not-implemented if the method is called.

Classes have constructors and destructors, denoted by new and destroy respectively. Each constructor and destructor should call its superclass's constructor or destructor, respectively, using early binding, as will be explained below. A superclass's constructor should be called before the remainder of the subclass's constructor executes. A superclass's destructor should be called after the body of the subclass's destructor executes with the key exception of that a portion of a destructor that physically frees the memory in which the object exists must execute last, after all other destructor functionality. Here is an example of this:

oo import
<object> begin-class <my-superclass>
end-class
<my-superclass> begin-implement
  :noname
    <object>->new
    cr ." Constructing my-superclass"
  ; define new
  :noname
    cr ." Destroying my-superclass"
    <object>->destroy
  ; define destroy
end-implement
<my-superclass> begin-class <my-subclass>
end-class
<my-subclass> begin-implement
  :noname
    <my-superclass>->new
    cr ." Constructing my-subclass"
  ; define new
  :noname
    cr ." Destroying my-subclass"
    <my-superclass>->destroy
  ; define destroy
end-implement

When initializing a block of memory to be a new object of a given class one should call init-object with the object's class and the location in memory of the object. This will initialize the block in memory to be an instance of the class and then call its new method. Note that any arguments placed before the class may be made use of by the object's new method.

All classes inherit from a single superclass, including <object>, which inherits from itself. All methods declared in a superclass can be overridden by a subclass, and will be inherited by a subclass if they are not. This can be seen from the following:

oo import
<object> begin-class <my-superclass>
  method foo
  method bar
end-class
<my-superclass> begin-implement
  :noname drop ." FOO " ; define foo
  :noname drop ." BAR " ; define bar
end-implement
<my-superclass> begin-class <my-subclass> end-class
<my-subclass> begin-implement
  :noname drop ." QUUX " ; define foo
end-class

When these classes the following can be seen:

<my-superclass> class-size buffer: super-object  ok
<my-superclass> super-object init-object  ok
<my-subclass> class-size buffer: sub-object  ok
<my-subclass> sub-object init-object  ok
super-object foo FOO  ok
super-object bar BAR  ok
sub-object foo QUUX  ok
sub-object bar BAR  ok

Early binding is carried out by using the find hook installed by the oo module which parses tokens such that tokens containing -> look up the class name, which may be a path containing one or more ::s, before the ->, and then look up the method within that class. From that point on this is a normal name lookup, and such names can be used wherever names are looked up, including by ', ['], and postpone. Examples of this can be seen in the examples above. Note that early binding ignores the binding of a given method in the object passed in, and a common use case of it is to call a superclass's method.

If one wishes to have private methods or members of a class, the zeptoforth module system is one's answer. Take the following example:

begin-module counter
  oo import
  <object> begin-class <counter>
    private-module
      cell member my-counter
      method clear
    end-module> import
    method increment
    method counter>
  end-class
  <counter> begin-implement
    :noname dup <object>->new clear ; define new
    :noname 0 swap my-counter ! ; define clear
    :noname 1 swap my-counter +! ; define increment
    :noname dup my-counter @ swap clear ; define counter>
  end-implement
end-module

Here we define a class <counter> which has a private method clear and a private member my-counter and two public methods increment and counter>. While subclasses may override clear and it may be called through early binding, the private member my-counter is entirely hidden, even from subclasses.