Object Orientation in zeptoforth - tabemann/zeptoforth GitHub Wiki
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.