Classes
One of MiniD's goals is to create a simple-to-use object-oriented method of programming. It accomplishes this through a simple class-based object model which is much simpler than those found in static languages but which still retains a lot of power and expressiveness.
The syntax for class declarations is as follows:
ClassDeclaration:
['local' | 'global'] 'class' Identifier [':' Expression] '{' {ClassMember} '}'
ClassMember:
[Decorators] SimpleFunctionDeclaration
[Decorators] 'this' FunctionBody
Identifier ['=' Expression] StatementTerminator
MiniD's Object Model
In larger static languages, such as D, C++, and Java, the class systems can be large and complex - public versus private inheritance; multiple inhertiance; interfaces; virtual versus nonvirtual methods; final methods; static methods; public, private, and protected members; constructors; destructors... the list goes on and on.
MiniD attempts to capture a large portion of the expressiveness of the class-based object model while stripping away some of the nonessential attributes of these larger models. MiniD also adds a large amount of dynamicity that is lacking in these static languages.
MiniD's classes use single inheritance with a singly-rooted inheritance tree rooted by Object, similar to D and Java. There are no interfaces, mostly because "duck typing" (that is, as long as an object supports a certain set of operations, it doesn't matter what type it is) is much more natural in a dynamically-typed language.
A prototype-based object model was tested during the development of MiniD. While this model does simplify some implementation details while providing a nice conceptual unity for object types, there is an impedance mismatch when attempting to interface prototype-based and class-based object models. Given that one of the design goals of MiniD is to be an embedded language that interfaces smoothly with its host, it was decided that a class-based model was, in the end, the better choice.
One aspect that many prototype-based models have that MiniD's class model has adopted is the idea of differential inheritance. This applies to both class inheritance and to instantiation of classes. What this means is that when you create a new class or instance, it initially has no data in it. It has no fields. In fact, the namespace for a class or instance's fields is not even created until it is needed. The namespace is created when you add a field into a class or instance, or when you attempt to get its field namespace. When you access a field that does not exist in a given class or instance, field lookup simply continues on to its parent (either the base class or the class from which the instance was created), continuing until either the field is found or the root of the class hierarchy is reached. This is in contrast to typical approaches to class-based OO, in which fields are copied from the source to the new class or instance upon its creation. This means that MiniD's classes and instances are very lightweight.
Classes and instances are very similar in some fundamental ways. They are both reference types which have a namespace that holds fields, and looking up a field in one may traverse a chain of inheritance of some form. The biggest difference is that classes can be instantiated, while instances cannot. Also, classes may not participate in metamethods, while instances can.
Class Declarations
Class declarations follow the same basic syntax as those in C++ and D. A class is given a name, optionally followed by a colon with the base class's name, and then a block containing the members of the class.
Where the class 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 and Methods
Fields are the members of the class. This includes both data members and class methods. There is no distinction between the two.
The declaration of fields is very similar to that of table fields, and fields' initializers can be any expression (not just constant expressions as in D). You may not define a field more than once in a class declaration.
class A { mX = 0 mY = 0 mZ }
The class A has three fields, named mX, mY, and mZ. mX and mY will be initialized to 0, and mZ to null.
When you instantiate A (by calling it as if it were a function), the new instance 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 instance will they actually be created.
Remember that the member initializers are only run at the point of the class definition, and not whenever the class is instantiated. This is important to remember when dealing with members which need to be instances of new reference types for each instance, for example.
Classes and their instances are dynamically modifiable. Class fields can be added and their values changed at any time:
class A { mX = 0 mY = 0 mZ } local a = A() // 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 instantiated a from it, since field lookup // is performed dynamically writefln("{}", a.mW)
Declaring functions within an class declaration is really just syntactic sugar. All kinds of fields, functions included, are kept in the same namespace within a class or instance.
Note that unlike many statically-typed languages like D and Java, you cannot implicitly access other fields and methods of an instance from within a method.
class 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 instance, you must access it through the 'this' parameter, like "writeln(this.x)", or more tersely, "writeln(:x)". The latter is sugar for the former.
Constructors
When you instantiate a class, you'll usually want to associate some data with that instance. As with other class-based models, MiniD uses constructors to do so. A constructor is just a member function named "constructor" which is run upon instantiation of the class. When declaring the constructor, you can either declare it as a normal member function called "constructor", or you can use the D-like "this()" syntax:
class A { mX = 0 mY = 0 function constructor(x, y) { :mX = x :mY = y } } local a = A(3, 4) writefln("{}, {}", a.mX, a.mY) // prints "3, 4" class B { this(x, y) writefln("B ctor: {}, {}", x, y) } B(2, 5) // prints "B ctor: 2, 5"
Keep in mind that the "this()" syntax is just sugar for declaring a method named "constructor", so if you want to get the constructor of a class, just use ClassName.constructor.
Calling the constructor does not instantiate the class. Rather, the constructor is called when the class is instantiated. You instantiate the class by calling it as if it were a function. When you do this, the instance is created, and any constructor is called with the parameters that were passed to the class. If a class defines no constructor, the parameters are just discarded.
Because the constructor is just another method, you can certainly call it on existing instances. Sometimes this can be useful, for example to reinitialize an already-initialized object.
The Base Class and .super
Almost every MiniD class has a base class. If you declare a class without a base class, it defaults to "Object". Object is the only class that has no base class. You can retrieve the base class of any class by using ".super" on it. "Object.super" gives null. You can also get the class that an instance was created from by using ".super" on it.
class A {} class B : A {} writeln(A.super) // prints 'class Object' writeln(B.super) // prints 'class A' writeln(Object.super) // prints 'null' local b = B() writeln(b.super) // prints 'class B'
A similar-looking but slightly different expression is the supercall expression. This is how you call a base class's implementation of a method. It looks just like it does in D: "super.methodName(params)".
The constructor is just another method, so you can supercall it. There is also a shortcut for supercalling the constructor, which looks like "super(params)", like the D equivalent.
class A { this(x) :x = x function fork() writeln("Base fork.") } class B : A { this(x, y) { super(x) // same as super.constructor(x) :y = y } function fork() { writeln("Derived fork!") super.fork() } } local b = B(2, 3) writefln("{} {} {}", b.x, b.y, b.super) // prints "2 3 class B" b.fork() // prints "Derived fork!" and then "Base fork."
If a derived class declares no constructor but its base class does, because of the way inheritance works, the derived class's constructor will simply be the same as the super class. So there is no need to create a dummy "forwarding" constructor.
class Base { this() writeln("Base ctor") } class Derived : Base { } Derived() // writes "Base ctor"
Supercalls are special -- they cause the interpreter to keep some information about the instance 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 value to the left of 'super', and then calls 'f' on that value.
One thing that should be mentioned is what happens if an instance overwrites a method and then attempts to perform a supercall.
class A { function fork() writeln("A's fork.") } class B : A { function fork() { writeln("B's fork.") super.fork() } } local a = B() a.fork() // prints "B's fork." and then "A's fork." a.fork = function fork() { writeln("instance's fork.") super.fork() } a.fork() // prints "instance's fork." and then "A's fork."
As you can see from the example, when the instance's custom fork method is called, the supercall results in A.fork being executed, with B.fork being skipped. This is because supercall lookup always works on the class level. When you call a method on an instance of B, if that method performs a supercall, it will always start looking for a super implementation in B.super and go from there.
