Ficl is not the first Forth to include Object Oriented extensions. Ficl's OO syntax owes a debt to the work of John Hayes and Dick Pountain, among others. OO Ficl is different from other OO Forths in a few ways, though (some things never change). First, unlike several implementations, the syntax is documented (below) beyond the source code. In Ficl's spirit of working with C code, the OO syntax provides means to adapt existing data structures. I've tried to make Ficl's OO model simple and safe by unifying classes and objects, providing late binding by default, and separating namespaces so that methods and regular Forth words are not easily confused.
This tutorial works best if you follow along by pasting the examples into ficlWin, the Win32 version of Ficl included with the release sources (or some other build that includes the OO part of softcore.c).
You need to use this incantation to make Ficl's OOP words visible to the interpreter:
ONLY ( Reset to default search order ) ALSO OOP DEFINITIONS ( Add OOP wordlist to search order )
ORDER
after each word. You can repeat the sequence above if you like. All of the above words are described in the
Standard
To start, we'll work with the base class OBJECT
.
Try this:
objectFiclWin's stack viewer shows that the stack now holds two values - the one on top is the address of
class
and the other is the address of a special class called METACLASS
that describes the behavior of all classes (including itself). If you're using someother version of Ficl, you can type .s
to view the contents of the stack non-destructively.
Now try:
object --> methodsThe line above contains three words. The first is the name of a class, so it pushes its signature on the stack. The next word (
-->
) finds a method in the context of class object
and executes it. In this case, the name of the method is methods
.
Its job is to list all the methods that a class knows. What you get when
you execute this line is a list of all the class methods Ficl provides.
Now let's create a class that can do something interesting with the simulated hardware that FiclWin provides...
object --> sub c-led
Causes base-class OBJECT
to derive from itself a new class called
c-led
.
!oreg ( value -- )
. To test it, try 0xff !oreg
or 0 !oreg
.Now we'll add some instance variables and methods to the new class...
c-byte obj: .state \ an instance variable : init { 2:this -- } 0 dup !oreg this --> .state --> set ." initializing an instance of " this --> class --> id type cr ; : on { led# 2:this -- } this --> .state --> get 1 led# lshift or dup !oreg this --> .state --> set ; : off { led# 2:this -- } this --> .state --> get 1 led# lshift invert and dup !oreg this --> .state --> set ; end-classThe first line adds an instance variable called
.state
to the
class. This particular instance variable is an object - it will be an instance
of c-byte
, one of ficl's stock classes (the source for which can be found in the distribution in sorftowrds/classes.fr). Try this...
c-led --> see .stateThe
see
method (inherited from object
) decompiles methods so that you can see exactly what they do. There's also a debug
method that lets you step through a method.
Next we've defined a method called init
. This line also declares
a local variable called this
(the 2 in front tells Ficl that this is a double-cell local). All methods
by convention expect the address of the class and instance on top of the
stack when called. The next four lines define init
's behavior.
It first clears the simulated LED bar and .state
. The rest displays some text and causes the instance to print its class name (this --> class --> id
).
The init
method is special for Ficl objects: whenever
you create an initialized instance using new
or new-array
,
Ficl calls the class's init
method for you on that instance. The
default init
method supplied by object
sets the instance variables to zero (see the source code in ficl/softwords/oo.fr).
The ON
and OFF
methods defined above hide the details
of turning LEDs on and off. The interface to FiclWin's simulated hardware
is handled by !OREG
. The class keeps the LED state in a shadow
variable (.STATE
) so that ON
and OFF
can work
in terms of LED number rather than a bitmask.
Now make an instance of the new class:
c-led --> new ledAnd try a few things...
led --> methods led --> pedigree 1 led --> on 1 led --> offOr you could type this with the same effect:
led 2dup --> methods --> pedigreeNotice (from the output of
methods
) that we've overridden the
init method supplied by object, and added two more methods for the member
variables. If you type WORDS
, you'll see that these methods are
not visible outside the context of the class that contains them. The method
finder -->
uses the class to look up methods. You can use
this word in a definition, as we did in init
, and it performs
late binding, meaning that the mapping from message (method name) to method
(the code) is deferred until run-time. To see this, you can decompile the
init method like this:
c-led --> see initor
led --> class --> see init
cell: cells: char:
and chars:
) just push the address of the corresponding instance
variable when invoked on an instance of the class. It's up to you to remember
the size of the instance variable and manipulate it with the usual Forth
words for fetching and storing.
As advertised earlier, Ficl provides ways to objectify existing data structures without changing them. Instead, you can create a Ficl class that models the structure, and instantiate a ref from this class, supplying the address of the structure. After that, the ref instance behaves as a Ficl object, but its instance variables take on the values in the existing structure. Example (from ficlclass.fr):
object subclass c-wordlist c-wordlist ref: .parent c-ptr obj: .name c-cell obj: .size c-word ref: .hash : ? 2drop ." ficl wordlist " cr ; : push drop >search ; : pop 2drop previous ; : set-current drop set-current ; : words --> push words previous ; end-classIn this case,
c-wordlist
describes Ficl's wordlist structure;
named-wid creates a wordlist and binds it to a ref instance of c-wordlist
.
The fancy footwork with POSTPONE
and early binding is required
because classes are immediate. An equivalent way to define named-wid with
late binding is:
: named-wid ( "name" -- ) wordlist postpone c-wordlist --> ref ;To do the same thing at run-time (and call it my-wordlist):
wordlist c-wordlist --> ref my-wordlistNow you can deal with the wordlist through the ref instance:
my-wordlist --> push my-wordlist --> set-current orderFicl can also model linked lists and other structures that contain pointers to structures of the same or different types. The class constructor word
ref:
makes an aggregate reference to a particular class. See the instance
variable glossary for an example.
Ficl can make arrays of instances, and aggregate arrays into class descripions.
The class methods array
and new-array
create uninitialized and initialized arrays, respectively, of a class.
In order to initialize an array, the class must define (or inherit) a reasonable
init
method. New-array
invokes it on each member of the array
in sequence from lowest to highest. Array instances and array members use
the object methods index
, next
, and prev
to navigate. Aggregate a member array of objects using array:
.
The objects are not automatically initialized in this case - your class
initializer has to call array-init
explicitly if you want
this behavior.
For further examples of OOP in Ficl, please see the source file ficl/softwords/ficlclass.fr
.
This file wraps several Ficl internal data structures in objects and gives
use examples.
c-string
(ficl 2.04 and later) is a reasonably useful dynamic string class. Source code for the class is located in ficl/softwords/string.fr
. Features: dynamic creation and resizing; deletion, concatenation, output,
comparison; creation from quoted string constant (s"
).
Examples of use:
c-string --> new homer s" In this house, " homer --> set s" we obey the laws of thermodynamics!" homer --> cat homer --> type
object --> sub c1 : m1 { 2:this -- } ." c1's m1" cr ; : m2 { 2:this -- } ." Running " this my=> m1 ; ( early ) : m3 { 2:this -- } ." Running " this --> m1 ( late ) end-class c1 --> sub c2 : m1 { 2:this -- } ." c2's m1" cr ; end-class c2 --> new i2 i2 --> m1 ( runs the m1 defined in c2 ) i2 --> m2 ( is this what you wanted? ) i2 --> m3 { runs the overridden m1)Even though we overrode method m1 in class c2, the definition of m2 with early binding forced the use of m1 as defined in c1. If that's what you want, great, but more often you'll want the flexibility of overriding parent class behaviors appropriately.
my=>
binds early to a method in the class being defined,
as in the example above.
my=[ ]
binds a sequence of methods in the current class.
Useful when the class has object members. Lines like this --> state
--> set
in the definition of c-led above can be replaced with
this my=[ state set ]
to get early binding.
=>
(dangerous) pops a class off the stack and compiles
the method in that class. Since you have to specify the class explicitly,
there is a real danger that this will be out of sync with the class you
really wanted. I recommend the my=
operations.
=>
is dangerous because it partially
defeats the data-to-code matching mechanism object oriented languages were
created to provide, but it does increase run-time speed by binding the
method at compile time. In many cases, such as the init
method,
you can be reasonably certain of the class of thing you're working on.
This is also true when invoking class methods, since all classes are instances
of metaclass
. Here's an example from the definition of metaclass
in oo.fr (don't paste this into ficlWin - it's already there):
: new \ ( class metaclass "name" -- ) metaclass => instance --> init ;Try this...
metaclass --> see new
Decompiling the method with SEE
shows the difference between the
two strategies. The early bound method is compiled inline, while the late-binding
operator compiles the method name and code to find and execute it in the
context of whatever class is supplied on the stack at run-time.
Notice that the primitive early-binding operator =>
requires
a class at compile time. For this reason, classes are IMMEDIATE
,
meaning that they push their signature at compile time or run time. I'd
recommend that you avoid early binding until you're very comfortable with
Forth, object-oriented programming, and Ficl's OOP syntax.
OBJECT,
as shown in the figure below. All classes are instances
of METACLASS
. This means that classes
are objects, too. METACLASS
implements the methods for messages
sent to classes. Class methods create instances and subclasses, and give
information about the class. Each class is represented by a data stucture
of three elements:
.CLASS
) of a parent class, or zero if it's
a base class (only OBJECT
and METACLASS
have this property).SIZE
) in address units of an instance of the
class.WID
) for the methods of the classMETACLASS
and OBJECT
are real system-supplied
classes. The others are contrived to illustrate the relationships among
derived classes, instances, and the two system base classes. The dashed
line with an arrow at the end indicates that the object/class at the arrow
end is an instance of the class at the other end. The vertical line with
a triangle denotes inheritance.
Note for the curious: METACLASS
behaves like a class - it responds
to class messages and has the same properties as any other class. If you
want to twist your brain in knots, you can think of METACLASS
as an instance of itself.
A Ficl object associates a class with an instance (the storage for one set of instance variables). This is done explicitly on Ficl's stack: any Ficl object is represented by a cell pair:
( instance-addr class-addr )The
instance-addr
is the address of the object's storage, and the class-addr
is the address of its class. Whenever a named Ficl object executes (eg. when you type its name and press enter at the Ficl prompt), it leaves this "signature". All methods by convention expect a class and instance on the
stack when they execute, too. In many other OO languages, including C++,
instances contain information about their classes (a vtable
pointer, for example). By making this pairing explicit, Ficl can be OO about chunks of data that don't realize that they are objects, without sacrificing any robustness for native objects. That means that you can use Ficl to write object wrappers for data structures created in C, C++, or even assembly language, as long as you can determine how they're laid out in memory.
Classes are special kinds of objects that store the methods of their
instances, the size of an instance's payload, and a parent class pointer.
Classes themselves are instances of a special base class called METACLASS
,
and all classes inherit from class OBJECT
. While confusing at
first, this gives Ficl a very simple syntax for constructing
and using objects. Class methods include subclassing (SUB
), creating
initialized and uninitialized instances (NEW
and INSTANCE
),
and creating reference instances (REF
), described later. Classes
also have methods for disassembling their methods (SEE
), identifying
themselves (ID
), and listing their pedigree (PEDIGREE
).
All objects inherit (from OBJECT
) methods for initializing instances
and arrays of instances, for performing array operations, and for getting
information about themselves.
-->
that sends messages
to objects at run-time, and an early-binding operator =>
that compiles a specific class's method. These operators are the only supported
way to invoke methods. Regular Forth words are not visible to the method-binding
operators, so there's no chance of confusing a message with a regular
word of the same name.
softwords/oo.fr
.
CATCH
: looks up and CATCH
es the given
method in the context of the class on top of the stack, pushes zero or
exception code upon return.--> sub
.. end-class
class definition.--> sub
.. end-class
class definition.metaclass --> sub
.
Compiles .do-instance
in the context of a class; .do-instance
implements the does>
part of a named instance.
sub
method on object
or any
class derived from it (not metaclass
). Source code for
Ficl OOP is in ficl/softwords/oo.fr.
object subclass c-example cell: .cell0 c-4byte obj: .nCells 4 c-4byte array: .quad char: .length 79 chars: .name end-clasThis class only defines instance variables, and it inherits some methods from
object
. Each untyped instance variable (.cell0, .length,
.name) pushes its address when executed. Each object instance variable
pushes the address and class of the aggregate object. Similar to C, an
array instance variable leaves its base address (and its class) when executed.
The word subclass
is shorthand for --> sub
class
as a member variable
of the class under construction.
object subclass c-4list c-4list ref: .link c-4byte obj: .payload end-class; address-of-existing-list c-4list --> ref mylist
The last line binds the existing structure to an instance of the class we just created. The link method pushes the link value and the class c_4list, so that the link looks like an object to Ficl and like a struct to C (it doesn't carry any extra baggage for the object model - the Ficl methods alone take care of storing the class information).
Note: Since a ref: aggregate can only support one class, it's good for modeling static structures, but not appropriate for polymorphism. If you want polymorphism, aggregate a c_ref (see classes.fr for source) into your class - it has methods to set and get an object.
By the way, it is also possible to construct a pair of classes that contain aggregate pointers to each other. Here's an example:
object subclass akbar suspend-class \ put akbar on hold while we define jeff object subclass jeff akbar ref: .significant-other ( your additional methods here ) end-class \ done with jeff akbar --> resume-class \ resume defining akbar jeff ref: .significant-other ( your additional methods here ) end-class \ done with akbar
metaclass
. They define the manipulations
that can be performed on classes. Methods include various kinds of instantiation,
programming tools, and access to member variables of classes. Source is
in softwords/oo.fr
.
c_ref --> instance uninit-ref 2drop
c_4byte --> array 40-raw-bytes 2drop drop
array
class
from the heap (using a call
to ficlMalloc() to get the memory). Leaves the payload and class addresses
on the stack. Usage example:
c-ref --> alloc 2constant instance-of-ref
init
method
class
from the dictionary. Leaves
the payload and class addresses on the stack. Usage example:
c-ref --> allot 2constant instance-of-ref
init
method
subclass
. Examples:
c_4byte --> sub c_special4byte ( your new methods and instance variables here ) end-class ( --OR-- ) c_4byte subclass c_special4byte ( your new methods and instance variables here ) end-class
: get-size metaclass => .size @ ;
: get-wid metaclass => .wid @ ;
: get-super metaclass => .super @ ;
metaclass --> offset-of .wid
SEE
, from the
TOOLS
wordset.
DEBUG
.
object
. The methods include default initialization, array manipulations, aliases of class methods, upcasting, and programming tools.
new
or new-array
. Zero-fills the instance. You do not normally need to invoke init
explicitly.init
to an array of objects created by new-array
.
Note that array:
does not cause aggregate arrays to be initialized
automatically. You do not normally need to invoke array-init
explicitly.alloc
or alloc-array
. Note - this method is not presently protected
against accidentally deleting something from the dictionary. If you do
this, Bad Things are likely to happen. Be careful for the moment to apply
free only to instances created with alloc
or alloc-array
.
object
is zero. Useful for invoking an overridden parent class method.get-size
0 my-obj --> index \ is equivalent to my-obj
-ROT
for
help in dealing with indices on the stack.get ( inst class -- ref-inst ref-class ) set ( ref-inst ref-class inst class -- ) .instance \ cell member that holds the instance ( inst class -- a-addr ) .class \ cell member that holds the class ( inst class -- a-addr )
object
, with a 1-byte payload. Set
and get methods perform correct width fetch and store. Methods and members:
get ( inst class -- c ) set ( c inst class -- ) .payload \ member holds instance's value ( inst class -- addr )
object
, with a 2-byte payload. Set
and get methods perform correct width fetch and store. Methods and members:
get ( inst class -- 2byte ) set ( 2byte inst class -- ) .payload \ member holds instance's value ( inst class -- addr )
object
, with a 4-byte payload. Set
and get methods perform correct width fetch and store. Methods and members:get ( inst class -- x ) set ( x inst class -- ) .payload \ member holds instance's value ( inst class -- addr )
object
, with a cell payload (equivalent
to c-4byte in 32 bit implementations, 64 bits wide on Alpha). Set and get
methods perform correct width fetch and store. Methods and members:
get ( inst class -- x ) set ( x inst class -- ) .payload \ member holds instance's value ( inst class -- addr )
object
for pointers to non-object types.
This class is not complete by itself: several methods depend on a derived
class definition of @size
. Methods and members:.addr ( inst class -- a-addr ) \ member variable - holds the pointer address get-ptr ( inst class -- ptr ) set-ptr ( ptr inst class -- ) inc-ptr ( inst class -- ) \ Adds @size to pointer address dec-ptr ( inst class -- ) \ Subtracts @size from pointer address index-ptr ( i inst class -- ) \ Adds i*@size to pointer address
@size ( inst class -- size ) \ Push size of the pointed-to thing get ( inst class -- c ) \ Fetch the pointer's referent byte set ( c inst class -- ) \ Store c at the pointer address
@size ( inst class -- size ) \ Push size of the pointed-to thing get ( inst class -- n ) \ Fetch the pointer's referent c-2byte set ( n inst class -- ) \ Store n at the pointer address
@size ( inst class -- size ) \ Push size of the pointed-to thing (a c-4byte) get ( inst class -- n ) \ Fetch the pointer's referent c-4byte set ( n inst class -- ) \ Store n at the pointer address
@size ( inst class -- size ) \ Push size of the pointed-to thing (a cell) get ( inst class -- x ) \ Fetch the pointer's referent cell set ( x inst class -- ) \ Store n at the pointer address
set ( c-addr u 2:this -- ) \ Initialize buffer to the specified string get ( 2:this -- c-addr u ) \ Return buffer contents as counted string cat ( c-addr u 2:this -- ) \ Append given string to end of buffer compare ( 2string 2:this -- n ) \ Return result of lexical compare type ( 2:this -- ) \ Print buffer to the output stream hashcode ( 2:this -- x ) \ Return hashcode of string (as in dictionary) free ( 2:this -- ) \ Release internal buffer