Objects
One of MiniD's goals is to create a simple-to-use object-oriented method of programming. It accomplishes this through prototype-based objects, which are simpler and more basic than classes, but which still provide a lot of power.
The syntax for object declarations is as follows:
ObjectDeclaration:
['local' | 'global'] 'object' Identifier [':' Expression] '{' {ObjectMember} '}'
ObjectMember:
[AttributeTable] SimpleFunctionDeclaration
Identifier ['=' Expression] StatementTerminator
Prototype-Based OO
Many of you are probably familiar with class-based OO, found in most popular languages including C++, D, and Java. You declare a class type, which has fields and methods, and maybe inherits capabilities from another class. Classes then form a hierarchy of types. Polymorphism is achieved by overloading methods of base classes so that when a method is called on a base class, the derived class methods are determined and called at runtime.
This works well for a statically-typed language, and can be implemented fairly efficiently for natively-compiled languages as well. However it's not necessarily the best fit for dynamically-typed interpreted languages. Dynamically-typed languages are not as concerned with the type hierarchy of objects and tend to use "duck typing" (that is, as long as an object supports a certain set of operations, it doesn't matter what type it is) rather than expecting an object to be of a certain type. Class-based OO also usually assumes that the objects will not change. So, if you end up making class-based OO more flexible to match the flexibility of the language, you end up with class types and instance types which have almost all of the same operations but with a relatively arbitrary bifurcation between the two.
This is the motivation for prototype-based OO. In prototype-based OO, you do not have classes and instances, you just have "objects." The distinction between subclassing and instantiation disappears, and is replaced with a single "clone" operation.
Each object has a prototype, which you can think of both as its superclass and as the class from which it was instantiated. When you look up a field in an object, it looks in the object, and if not found, follows the chain of prototypes, looking up the field in each object until it either finds it or it reaches the root of the prototype hierarchy.
When you clone an object, no new fields are created in the new object. In fact the namespace for its fields is not even created until it's needed. The namespace is created when you add a field into an object, or when you attempt to get the field namespace of the object. This means that MiniD uses a delegation-based approach to prototype OO, sometimes known as "differential inheritance." This is in contrast to concatenative prototype OO, in which fields are copied from the source object into the new object. This makes objects very lightweight.
Object Declarations
Object declarations follow the same basic syntax as class declarations in C++ and D. An object is given a name, optionally followed by a colon with the base object's name, and then a block containing the members of the object.
Where the object actually ends up is dependent upon whether it's preceded by a 'local' keyword, a 'global' keyword, or neither. If it's declared 'local', a new local is created and the class is placed in it. If it's declared 'global', a new global is created and the class is placed in it. If it has neither keyword, it is global at module scope, and local everywhere else.
Fields
Fields are the members of the object. Their declaration is very similar to that of table fields, and their initializers can be any expression (not just constant expressions as in D).
object A { mX = 0 mY = 0 mZ }
Object A has three fields, named mX, mY, and mZ. mX and mY will be initialized to 0, and mZ to null.
When you clone A (by calling the clone method, which is actually declared in Object; or by using an object literal with A as the prototype), the new object will not have any fields, but getting mX, mY, and mZ will get the values held in A. Only when you set one of these fields in the new object will they actually be created.
Remember that the member initializers are only run at the point of the object definition, however, and not whenever the object is cloned. This is important to remember when dealing with members which need to be instances of new reference types for each instance, for example.
Objects are dynamically modifiable. Object fields can be added and their values changed at any time:
object A { mX = 0 mY = 0 mZ } local a = A.clone() // the following writes "0 0 null" since those values are in A. // note that there is no data in a writefln("{} {} {}", a.mX, a.mY, a.mZ) A.mX = 10 // the following writes "10 10" -- remember that a has no members, so when you change // a value in A, that change is reflected in anything that was cloned from it writefln("{} {}", A.mX, a.mX) a.mX = 3 // now this prints "10 3". we've actually added a field to a now, and so changes made to // A.mX are no longer reflected in a writefln("{} {}", A.mX, a.mX) A.mW = 5 // prints 5, even though there was no mW in A when we cloned a from it, since field lookup // is performed dynamically writefln("{}", a.mW)
You may not define a field more than once in an object declaration.
Member Functions
Declaring functions within an object declaration is really just syntactic sugar. All kinds of fields, functions included, are kept in the same namespace within an object.
Note that unlike many statically-typed languages like D and Java, you cannot implicitly access other fields and methods of an object from within a method.
object O { x = 5 function foo() writeln(x) // error, undefined global 'x' } O.foo()
The above, executed in isolation, will give an error in foo about not being able to find a global named 'x'. In order to access the member 'x' from the current object, you must access it through the 'this' parameter, like "writeln(this.x)", or more tersely, "writeln(:x)". The latter is sugar for the former.
Object Construction
As mentioned before, there is no distinction between subclassing and instantiation, and that both are performed with a "clone" operation. Really, when you declare an object, you're just performing a clone of an existing object (the prototype). If you don't specify an object to clone from, it defaults to "Object". You must clone an existing object; you are not allowed to create an object from nothing.
By default there is the Object object at global scope from which all other objects must ultimately derive. It is the only object that is allowed to have no prototype. It has a clone method. Object could be defined as so:
object Object : null { function clone() = object : this {} }
Except that you can't clone an object from null like Object does.
There are then two ways to create an object -- by doing it manually with an object expression (like "object : Object{}") or by calling the clone method of an object. The nice thing about the latter is that it can be overridden. You can use the clone method to take the place of constructors in class-based object systems.
object O { x function clone(x) = object : this { x = x } } local o = O.clone(5) writeln(o.x) // writes 5
You can also override the clone method to do some more interesting things. For example, if you wanted to implement a free list, you could do the allocation step in the clone method and then write a free method to add that object to the free list. You can also make an object a singleton by having the clone method just return "this":
object O { function clone() = this }
Now whenever you call "O.clone()", you just get O. Note that there's nothing preventing you from writing "object : O {}", however.
possible: have Object.clone call an 'init' method if it exists? Then you only have to override 'init' and not worry about cloning the object in overridden 'clone'?
The Prototype
Almost every MiniD object has a prototype. If you declare an object without a prototype, it defaults to "Object". Object is the only object that has no prototype. You can retrieve the prototype of any object by using ".super" on it. "Object.super" gives null.
object A {} object B : A {} writeln(A.super) // prints 'object Object' writeln(B.super) // prints 'object A' writeln(Object.super) // prints 'null' local b = B.clone() writeln(b.super) // prints 'object B'
A similar-looking but slightly different expression is the supercall expression. This is how you call a prototype's implementation of a method. Remember that clone is just another method, so there's no difference between calling it and any other method.
object A { x function clone(x) { local ret = super.clone() // calls Object.clone ret.x = x return ret } } object B : A { y function clone(x, y) { local ret = super.clone(x) // calls A.clone ret.y = y return ret } } local b = B.clone(2, 3) writefln("{} {} {}", b.x, b.y, b.super) // prints "2 3 object B"
The last thing, 'b.super' is notable for one reason. Remember that Object.clone returns an object cloned from 'this'. When you call a super method, the 'this' parameter is passed along, meaning that when B.clone calls A.clone, it is passed B as 'this', and when A.clone subsequently calls Object.clone, B is still passed as 'this'.
Supercalls are special -- they cause the interpreter to keep some information about the object in which the currently-executing method was called, so that it can continue method search if another supercall is made. Supercalls can only be performed in functions that were called as methods. Furthermore, something like "a.super.f()" or ":super.f()" does not perform a super call; rather, it gets the super of the object to the left of 'super', and then calls 'f' on that object.
