Plinth has several different sorts of user-defined type: classes, interfaces, compound types, and (eventually) enums. In this post, I will talk about how each of them works.
Classes
Classes act much as they do in Java and C♯, with single inheritance of classes, but multiple inheritance of interfaces. They are allocated on the heap, and must be created with the new keyword, as follows:
class Foo extends Test implements Runnable { uint value; static uint lastValue; this(uint value) { this.value = value; lastValue = value; } void run() { stdout::println(toString()); } } // ...in a method somewhere... Foo foo = new Foo(12);
Classes can have all of the different types of members: initialisers, constructors, fields, methods, and properties. All of these (except constructors) can optionally be static, which means they do not require an instance of the class to be used. Members of an instance of a class can be accessed using the .
operator, but static members of a class must be accessed with the ::
operator on the class’s type. For example:
foo.value = 8; Foo::lastValue = 97; foo.run();
Classes also inherit instance members from their super-classes and super-interfaces. All classes (and in fact, all types) get default implementations of certain methods, such as toString()
and equals()
.
Something that all classes must implement is the set of abstract methods and properties which their super-types declare but do not define. One point that is worth noting here is that abstract methods may have implementations which are higher up in the class hierarchy than the abstract method itself (e.g. something may override toString() and make it abstract). In this case, any subclasses would have to provide their own implementation of that method (or use one from an interface – see the next section). This allows interfaces to specify that their implementations do not just use object.toString()
or object.equals(...)
.
Interfaces
Interfaces are the feature I am currently working on. They will work quite differently from those in Java and C♯, in that they will be able to contain implementations of methods. This makes them very similar to mixins (thank you to William Berg for pointing me towards them).
All of their members will be abstract by default, unless an implementation of a given method is provided. They will not be able to contain any data (unless it is static), however they will be able to specify abstract properties that must be implemented by a subclass.
Interfaces will have full multiple inheritance, which is something that can be difficult to implement well. One reason for this is the well-known diamond problem, which is to decide where to look first for a method on an object of type D in the following class hierarchy (remember the method could be implemented on both B and C):
A / \ B C \ / D
This problem is fairly easy to solve by starting with the leftmost super-type and checking each parent from left to right in the list, giving the method resolution order of D, B, C, A.
A much more difficult problem is what happens in the following example:
A B | \ /| | /\| | / |\ |/ |/ C D \ / E
It may not be clear from the ASCII art, but C extends A before B, whereas D extends B before A. Individually, C and D are well defined, but their subtype E is not. Suppose A and B both implement some method foo()
, and something in C depends on inheriting A.foo()
, but something else in D depends on inheriting B.foo()
. In this case, whichever way around E inherits foo()
, it will break something in either C or D.
To combat inconsistencies like this, I am using C3 linearisation to decide on a consistent order for inheritance, or fail in cases where the class hierarchy is inconsistent. This is the same system used by Python, and it also has the side effect that all super-classes of a class are searched before any of its inherited interfaces.
Compound Types
This is a type which I have still not finalised the name of, although its behaviour has been decided. They are very similar to value types, but I don’t want to make ‘value’ a keyword, so I will probably change it from ‘compound’ to ‘value_type’ at some point.
Their main property is that they are stored on the stack (or directly embedded in another object when stored as a field), and because of this, their values are routinely copied into new variables, rather than passed into a function by reference. The only exception to this is when they are the value which the function is being called on, in which case they must be passed by reference in order to allow the function to modify the value.
Due to their fixed size and object layout, they cannot have any form of inheritance (even interfaces). However, they can have all of the normal members that are allowed in classes.
Enums
Enums are a well known concept, but they can be implemented in very different ways. In C, C++, and C♯, they are syntactic sugar for integers, whereas in Java, they are a constant set of Objects. In plinth, they will be more similar to Java’s representation, which allows enum constants to store data.
However, there is a problem: each enum constant must be created statically, which means that there must be an order in which the enum constants are created. What happens if the constructor for the first enum constant tries to use the value for the second, before it has been created? This is a case where using C-style enums would be much easier. (In Java, the JRE ignores the problem and just allows the constants to be null before they are created – something I would like to avoid in plinth.)
The system I am currently considering for plinth is to disallow enums from storing any data which would need to be initialised (similarly to how static variables are already handled). This would mean that the enum constants could be allocated early, and if necessary they could be used safely before any of their constructors are called.
This scheme would make an enum extending a class impossible, because classes need to guarantee that they are initialised before use. However, interface inheritance (including mixins) would be possible.
Next week’s post will be about the rest of the type system, and how it all interacts.
Pingback: Plinth’s Type System « Anthony's Blog