Every value has a type, which is a collection of properties and methods aggregated (via inheritance) from one or more objects, classes or mixins:
- Objects support prototypical inheritance (similar to Javascript or Lua).
- Classes support classical inheritance (similar to Ruby, Smalltalk, Java, or C++), which segregates the class properties and methods (e.g., constructors) from the instance properties and methods .
- Mixins enable multiple inheritance (much like Ruby), which allows objects or classes to be dynamically augmented with a mixin's methods and properties.
Objects, classes, and mixins are mutable, first-class values. Object, Class, Mixin and various collection types are metaclasses, used to create and define the properties and methods for a new type. Like other collections, the property and method content of an object, class or mixin is typically built using a 'this' block.
Objects
Objects offer the simplest structure for inheritable properties and methods. This simple structure makes them very convenient to use. Despite their structural simplicity, objects are quite versatile.
Creating a new Object
Let's start by creating a basic "singleton", an object which has its own distinct state (properties) and behavior (methods). To build a new object, use +Object with a 'this' block. In the block, specify the object's named properties (using ':') and methods (using ':='):
# Create a new object thing = +Object # Specify and initialize its properties position: +Xyz(0.0, 0.0, 0.0) velocity: +Xyz(1.0, 0.0, 0.0) # Define method that calculates new position after dt seconds Move:= [dt] .position = .position + .velocity * dt
Having now created 'thing' and described its two properties and method, we may now call its defined .Move method on itself, thereby altering its position:
thing.Move(0.1) # calculates new .position to be (0.1, 0.0, 0.0)
Properties and Methods
An earlier chapter described how to use a value's properties and methods. Here we talk about their names and content.
Property and Method Names
An object has a single namespace for uniquely naming each of its properties and methods. These helpful (but not enforced) conventions apply to property and method names:
- If it ends with '?' (e.g., '.empty?'), it returns a true or false/null value.
- If it starts capitalized (e.g., '.New') or using punctuation (e.g., '*'), it is a method called to perform a specific action.
- If it starts with '$' (e.g., '$cache'), it is a class ("global") property shared in common across all its many instances.
- If it starts with a lower-case letter (e.g., '.size'), it is a property whose value might be retrieved or changed. This applies to computed properties as well. It also applies to constants, even if they are implemented as read-only using a closure.
- If it starts with a '_' (e.g., '._cache'), it is intended to be private, a part of the plumbing that may change from implementation to implementation. The following letter is capitalized for methods and lowercase for properties.
These distinctions can have practical consequences. One might want to use 'each' to iterate over a type to see only the current state of its properties. One might want to 'freeze' a type, so that only its properties can be altered, but not its methods (even those that underly computed properties).
Specifying the Value of an Object's Property or Method
An object has only one namespace. Any name in any object can hold any type of value: null, true, a number, string, symbol, collection, method, closure, yielder, class, etc. As a helpful convention, we often label a certain object's name a method when it stores any kind of method, closure or yielder, and a property otherwise.
Part of the reason we do this is because this distinction matters to the '.' and ':' operators, which access a name's stored value when it is a property, but call it when it is some kind of method. Because of this behavior:
- A named property's value should be set using the ':' operator. This ensures that the new value is processed by any methods defined by a computed property (see below).
- A named method should be specified using the ':=' operator. This ensures the new method value is not accidentally processed by trying to set the new value using the name's current 'set' method (which we want to change, not use).
Computed Properties
Computed properties are part-property and part-method. They look and behave like simple properties, However, under the covers they make use of a method or get/set closure to retrieve or change its value. The following example defines 'radians' as a normal property and 'degrees' as a computed property:
angle = +Object radians: 0. degrees:= +[] [] {.radians * 180. / Float.Pi} [a] {.radians = a * Float.Pi / 180.}
Notice that we used the ':=' operator for 'degrees', since we are storing a get/set closure into it. However, if we later want to assign its property value to some number (in degrees), we would then use ':' or '.', which will automatically make use of this get/set closure to retrieve or alter its value.
The get/set closure used by the 'degrees' computed property links the computed value accessible via 'degrees' to the float value held in 'radians'. As a result, angle's single value can now be accessed in either radians or degrees:
# Set the angle using degrees... angle.degrees = 45. # ... retrieve it in radians angle.radians # 0.785398
Computed properties are useful and convenient in many situations:
- Permitting methods to look and act like properties, sparing the code that uses them of having to know how they are implemented and having to needlessly supply empty parentheses.
- Allowing properties to begin life as stored values and later graduate through re-factoring into Closure use, without necessarily having to alter any of the existing code that accessed those properties.
- Performing automatic validation to ensure that a property only ever holds a valid value.
- Overriding an inherited method or property and augmenting its use (without needing to know anything about the original behavior).
- Monitoring requests or changes to a property's value and triggering appropriate action on certain conditions. 2-way binding is a great example of this, where a part can subscribe to be notified whenever the value of a watched variable or property changes.
- Providing aliases or portals to other properties, performing any on-the-fly conversions, extractions or alterations needed to the master value. For example, a 'position' property might actually retrieve or change the x,y.z position found within a transformation matrix.
Several properties in the core and World type libraries (e.g., the 'size' property for collections) are implemented using computed properties.
Closure and Yielder Iterators
An object may define closure or yielder iterators as named methods, as illustrated by the Each method provided for collections and Range. With an iterator as an object method, we want it to be re-usable, which stateful iterators are not. To address this, define a named iterator method as an factory that returns a new iterator each time it is called.
Yielder methods already work this way:
mathlib = +Object Fibonacci:= *[start=1, inc=0] while true yield true,start start,inc = start+inc, start # using mathlib.s Fibonacci iterator factory each n in mathlib.Fibonacci ...
To make a closure factory, define a method that returns a newly created closure whose closure state is built using parameters passed to the factory method:
mathlib Odds:= [start=1] +[start] [] local s,start = start, start+1 s, s*2-1 each n in mathlib.Odds ...
Object Prototypes
Objects are valuable for more than singletons; they are commonly used to create several objects that share common behavior and properties. To do this, begin by creating a prototype object that describes the methods, constants, default properties, and "static" properties that those objects share in common. A prototype is created exactly the same way as a singleton:
# Define a prototype called 'Ball' Ball = +Object # Specify the properties for a ball position: +Xyz(0.0, 0.0, 0.0) velocity: +Xyz(0.0, 0.0, 0.0) # Define method that calculates new position after dt seconds Move:= [dt] .position = .position + .velocity * dt
We can now use this defined prototype to create two new balls. Notice we use +Ball for this, instead of +Object:
fastball = +Ball position: +Xyz(0.0, 0.0, 0.0) velocity: +Xyz(100.0, 0.0, 0.0) balloon = +Ball position: +Xyz(0.0, 50.0, 0.0) velocity: +Xyz(0.0, 10.0, 0.0)
balloon and fastball are initialized with their own property values, which represents the distinct state for each ball. Since they were created by Ball, balloon and fastball automatically inherit its Move method. So:
fastball.Move(0.1) # position is now +Xyz(10.0, 0.0, 0.0) balloon.Move(0.1) # position is now +Xyz(0.0, 51.0, 0.0)
Prototypical Inheritance
The way that objects inherit from their prototype is very straightforward. If you try to use an object's named property or method:
- It looks first to see if the property or method is defined within the object itself (e.g., fastball).
- If the property or method is not found there, it then looks for it in that object's prototype (e.g., Ball).
- If it fails to find it in the prototype, it looks within the prototype's prototype (which is Object).
It follows the ancestry back up the creation chain (in reverse order) until it gets to Object, which is the end of the line. This is how balloon was able to use .Move (found in Ball). It also explains how we were able to create a new Ball. +Ball means Ball.New, which works because Ball inherits the .New method from Object.
It is a very simple mechanism. All prototypes are objects and any object may become a prototype for a new object it creates. This means we could, if we want, use +balloon to create a new balloon called redballoon.
With prototypical inheritance, an object automatically inherits every property, computed property and method from every prototype all the way up its inheritance chain. An object may add its own properties and methods or override those it inherits.
Let's explore the design implications for inheritable properties and methods.
Property Initialization and Inheritance
In general, properties should be given values on the object they apply to. Most of the time, this is the lowest level object (rather than specify them on any prototype). However, there are times when it makes sense to define properties on a prototype:
- Constants. For example: Float.pi
- Multi-object state. These properties apply across many objects, rather than one object in particular. For example: Resource.extensions
- Object defaults. Defining property defaults at the prototype level ensures that if that
property is not defined at the object level, it still has a valid (default) value.
As long as the property's value is atomic or replaced using assignment, there is no danger.
Be very careful, however, when a property's default value is a mutable collection (e.g., the Xyz position of a Ball). If a method were to alter something within the default property's collection, rather than replacing the collection with a new one at the object level, that change would ripple out to all other objects that inherit that default value.
An object can easily override the default value it inherits from its prototype by assigning it a new value at the object level (as the balloon example demonstrates). Assignments always act at the object level, so doing so leaves the prototype's default value unchanged. Although an object cannot explicitly "delete" an inherited property assigning the object's property to 'null' accomplishes the same thing. From a property use perspective, there is no difference between a property that does not exist and one that exists, but is assigned to null. The difference is only noticeable during property reflection.
It is important to establish how a newly created object's properties are initialized. Several distinct approaches are possible:
- Within a 'this' block, where 'this' represents the just-created object. The balloon example above demonstrates this simple, straightforward technique.
- Within the object's constructor (typically the prototype's .New method). Later, we will describe how to define a prototype's .New method to do this. Using a constructor to initialize an object's properties is preferable if property initialization is repetitive or procedural, or if there is a danger that the 'this' block initialization will not define all needed properties.
- In both places. The constructor would initialize properties to a default state. The 'this' block can then be used to alter or add to the object's properties.
Method Definition and Inheritance
In contrast to properties, methods are more typically populated at the prototype level. To be reusable by any object, prototype methods should typically be stateless. For this reason, a prototype that wants to offer (stateful) closures or yielders as methods will do so using closure or iterator factories. As with properties, there may be situations where it makes sense for a prototype to use a stateful method, particularly when it applies across multiple objects (rather than to one specifically).
On the flip side, a key benefit of prototypical inheritance is that it is very easy for any object to define its own specific behavior. This can be very useful for games and other models that make use of one-of-a-kind entities (such as a boss monster). Non-prototype objects may freely use stateful methods (closures and yielders) along with its stateful properties.
In this example, superball is given the Stop capability that no other ball has.
superball = +Ball Stop:= [] velocity: +Xyz(0.,0.,0.)
As with properties, an newly created object's methods can be defined within a 'this' block or as part of an object's constructor (e.g., .New).
Method Overriding
A prototype (or singleton) may specify a new method that replaces an existing same-named method it inherits from a parent prototype. This is called overriding. If the new method wants to make use of the inherited method as part of its logic, it must either:
- Refer to the inherited method using its properly qualified name (e.g., parent.:method).
- Capture the inherited method as part of a closure that defines the new method
Let's demonstrate the latter approach by showing a very common scenario: defining a prototype that has its own .New constructor (which must override and make use of the .New method it inherits):
Mover = +Object # Override .New with a closure which binds # 'super' to the prior (inherited) .New method (Object.New) New:= +[super=.:New] [pos=0.0, velocity=0.0] # Use super to create and return a new object super() # equivalent to: self.(Object.:New)() # Finish initialization of new object using parameter info pos: pos velocity: velocity accel: 0.0 # Create a new Mover obj1 = +Mover(12., 10.)
A similar technique may also be applied to computed properties.
This technique may even be used more than once on the same name within a prototype, much like the use of decorators in other languages (e.g., Python or Javascript). This approach enables a core capability to be wrapped by parasitical pre- or post-logic that might log, validate, handle subscription notifications, etc.
Classes
Classes offer a similar capability to prototypes: they are used to create values that share similar behavior and state structure. Their key advantage over prototypes is that a class-created "instance" value need not have the same hashed-key collection structure nor share the same namespace as its creator. This is why most of Acorn's core types (e.g., List, Text, and Float) are implemented using Classes.
Every class defines two namespaces:
- The class namespace (e.g., List), which specifies the constructor(s) (e.g., List.New) and all other methods and properties that are not specific to a class-created instance value.
- The traits mixin, which specifies all methods inherited by any instance value created by the class. The traits mixin is accessed using the .traits property of the named class (e.g., List.traits).
This dual structure makes classes somewhat more complex to define and use than prototypes (which combine both roles into a single namespace). Also note that class instances cannot be singletons.
Other than these differences, the rest of what has been said about objects/prototypes applies to classes.
Class Creation
Rectangle = +Class New: +[super=.:New] [len, wid] super() length: len width: wid .traits area: [] ::length * ::width # Instantiate the class using the new operator rect = +Rectangle(2,6) rect.area # 12
Class Creation via Composition
Like subclassing, composition takes advantage of a base class's methods. However, rather than inheriting methods from the base class, it simply makes use of them. In effect, it is a wrapper around a generic collection type, presenting a new interface for values of its type and hiding the base class's methods from view.
This example creates a Complex number class that is wrapped around a two-value List:
Complex = +Class New:= [r,i] self.(List.:New)(r,i) if r.float? and i.float? traits = +Mixin r := self.(List.traits.:'[]')(0) i := self.(List.traits.:'[]')(1) '+' := [v] +Complex(self.r+v.r, self.i+v.i)
Using +Class and +Mixin ensures Complex builds from two clean namespaces for its methods. The methods defined for Complex use List's methods to store and access the real and imaginary components for a complex number:
+Complex(0.5, 1.3) + +Complex(1.2, 0.2) # +Complex(1.7, 1.5)
The base class used for the value returned by .New must be created by a subclassable type, such as List, Index or Text.
Class Creation via Sub-classing
In Acorn, a new class is typically defined by subclassing an existing collection class, such as List, Index, Text or Mixin. The new class (and its traits) inherits properties and methods from the specified base class.
This simple example defines a new Set class that subclasses Index:
Set = Index.Subclass # Set's class methods & properties go here # Set inherits Index.New .traits # a set's inherited methods go here Add:= [mbr] self[mbr] = true Find:= [mbr] self[mbr]
Notice the use of the .Subclass constructor method on Index. It handles the creation of Set's new class and the traits collections and sets up them up to inherit from Index and Index.traits respectively.
With Set defined, we can now use it:
suits = +Set .Add('Hearts') .Add('Clubs') suits.Find('Clubs') # true suits.Find('Diamonds') # null
Not all classes support subclassing. In particular, it is not possible to subclass the core library's atomic types, such as Null, Bool, Integer, Float, and Method. It is also not possible to subclass Object.
Class Creation using the C-API
The Acorn C-API makes it possible to define and make use of new classes and methods that are coded in C. This is very useful for defining high-performance classes that manipulate specialized data structures or provide access to system capabilities via external libraries (e.g., OpenGL or database access).
Mixins
Prototypical and classical inheritance are single-inheritance mechanisms by default. An object inherits through a single line of ancestry: its prototype (or class), its prototype's prototype, etc. Most of the time this is sufficient. However, single inheritance can be needlessly inflexible when we want two objects from radically different prototypes/classes to share common behavior. To address the inflexibility of single-inheritance, some professionals favor composition over inheritance, a design approach that many game developers implement using an Entity-Component System.
Another way to overcome the inflexibility of single-inheritance is multiple-inheritance, which Acorn supports via the use of mixins. A mixin, like an object, is simply a hashed-key collection of methods (and sometimes properties). However, unlike an object, a mixin does not create new values, inherits nothing from Mixin (its creator), and makes no use of its own methods. A mixin's sole purpose is to make additional methods inheritable by certain values.
This example defines a trivial Mixin named Square:
Square = +Mixin Square:= [] self * self
Using the .Mixin method makes a mixin's methods inheritable by an object, prototype, class, or mixin. For example, let's add the Square mixin to Integer and Float and then use it:
Integer.traits .Mixin Square 3.Square # 9 Float.traits .Mixin Square 4.1.Square #16.81
As this example demonstrates, a mixin may access methods (such as '*') or properties that are not defined by the mixin, but which can be reasonably expected to be implemented somewhere else in the value's inheritance chain.
Multiple Inheritance Search Order
How does Acorn handle the multiple inheritance "diamond problem", wherein an object inherits from two mixins that use the same name for different methods? It uses the one defined by the more recently added mixin. In essence, the last one specified is the first one checked.
For example, imagine an object that makes use of two mixins:
beachball = +Ball .Mixin DiamondSkin .Mixin Bouncer
When we call beachball.Throw, Acorn will look for .Throw first in the Bouncer mixin, and use that one if found. If not found in Bouncer, it looks in DiamondSkin. Failing that, it looks for Throw in Ball (the prototype).
Although multiple inheritance offers great flexibility, it carries both a performance and complexity cost when overused. The more parents in the inheritance tree, the more places that have to be checked for any value not found (which returns 'null'). Complex multiple inheritance trees are possible. Acorn will faithfully search them in a disciplined depth-first order, but they can become too complex for other people to easily follow.
Mixins and State
Since, mixins are, for the most part, injected into prototypes and classes, they should contain just stateless methods (and computed properties) and not instance-specific, mutable state. If a mixin's methods provide an orthogonal extension which makes use of properties (state) not defined by the primary inheritance chain, how do those properties get initialized for any new instance that uses the mixin?
There are three ways to initialize the properties referenced by a mixin:
- Individually initialize every property within every newly-created instance's 'this' block. Every new instance would initialize all properties referenced by every mixin, prototype and class it inherits from.
- Use the mixin's initializer method within the newly-created instance's 'this' block. The mixin's initializer method would initialize the unique properties it makes use of. Typically, this initializer method is given the same name as the mixin.
- Have an injected mixin adjust (override) .New to automatically initialize its selected properties as well. This techniques takes advantage of the fact that whenever .Mixin is used to inject a mixin into a prototype or class, it automatically calls the mixin's .Init method (if it exists). Any parameters passed to .Mixin are routed to this .Init method. The .Init method can, if desired, be used to override the .New method, wrapping around it code that ensures any new instance also initializes the mixin's properties in a certain way.
Type-checking a Value
Determining the type for any value is deceptively easy. Just use the .type property on it:
# The type of an object is its prototype +Object.type # === Object # The type of an instance is its class's traits 1.type # === Integer.traits # The type of an object with injected mixin is a List m = +Object .Mixin Amixin m.type # == +List(Amixin, Object)
As these examples demonstrate, an unfortunate side-effect of Acorn's inheritance flexibility is that what you get back as the type for a value varies depending on whether that type uses prototypical, classical or multiple inheritance. And, when mixins are injected, they are structurally captured using ordered Lists.
Fortunately, most of the time we are not really interested in what the value's type is. We just want to know whether the value implements a specific method or makes use of a certain type's methods. Acorn's duck typing makes this possible.
The .uses? method (implemented by All) may be used to check if a value implements a specific property name. This provides valuable information about we can do with a value. For example:
val.uses?('<=>') # true if the value is comparable val.uses?('[]') # true if the value is indexable val.uses?('Each') # true if the value's members can be iterated
Determining whether a value inherits from a certain type is accomplished using pattern matching:
+Object ~~ Object # true 1 ~~ Integer # true m ~~ Amixin # true m ~~ Object # true
Additionally, some classes provide specific methods for this purpose:
1.integer? # true
Metaclasses and Reflection
Thus far, this chapter has introduced the most commonly used Type features in Acorn: how to define prototypes, classes and mixins and then use them to create new values that properly inherit the appropriate methods and properties. Let us turn to other type capabilities offered by Acorn: altering a type's definition dynamically at any time and interrogating the properties or methods of by a type (reflection).
All these capabilities are possible because objects, classes and mixins are mutable first-class values. Being values, they inherit the method capabilities provided by their metaclasses: Object, Class and Mixin.
Mutable Classes
Since objects, classes and mixins are just mutable values, their properties and methods may be dynamically altered at any time. Even the built-in classes provided by the core type library may be changed.
For example, the following code adds the Square method to Integer, thereby making it available to all integers:
Integer.traits Square:= [] self * self 3.Square # 9
Notice that we added the new method to Integer.traits, rather than Integer, in order to make the method work properly on instances created by Integer.
Type Reflection
How Inheritance Works
Inheritance is controlled by three properties:
- .type (all values). This can be a type or a list of types. It is the entryway to all properties the value inherits.
- .inheritype (only types). This can be a type or a list of types. Never used by the type itself, this a gateway to additional properties inherited by a value who has inherited this type's properties. In other orders, inheritance for a value begins with .type, but uses .inheritype for all further hops up the parent tree.
- .prototype (only types). True if the type uses its own properties.
Acorn is able to efficiently support many inheritance techniques by having the different varieties of types use these properties differently:
- Prototypes and classes keep .type and .inheritype to the same value and set .prototype to true
- Mixins have different values for .type (the methods it uses for itself) and .inheritype (the properties it delegates to other values). .prototype is false.
- Type has .type and .inheritype set to null and .prototype set to true.
The inheritance search starts with self, if .inheritype is true. Failing that, it proceeds depth-first through the parent tree starting with .type. When it encounters a list of types, it looks into each in order. When it encounters null, it backs up. If the inheritance search fails to find a match for the desired property, the "All" type is checked last. If it fails too, 'null' is returned.
Composition vs. Inheritance
Although this chapter focused entirely on dynamic, multi-level, multi-parent inheritance, Acorn supports just as well a design where inheritance has been flattened to one level, accomplished by statically copying in the inheritable properties down into every type that uses them. Such as strategy might improve performance. However, it would be problematic if the world is in the habit of dynamically modifying its types after it is loaded up and running.