Note: This website is archived. For up-to-date information about D projects and development, please visit wiki.dlang.org.

Cookbook: Classes

Here you'll find help on doing interesting things with MiniD's object-oriented facilities.

Contents

  1. How do you implement static fields and methods?
  2. How do you implement properties?
  3. Can you do multiple inheritance?

How do you implement static fields and methods?

MiniD doesn't explicitly support static fields and methods, but since class fields are freely modifiable, you can implement them without much trouble.

To make a static field, you don't have to do anything special. The only difference comes when accessing the field. Assuming we had a class like this:

class A
{
	x = 0
}

We want to treat x as a static field. This is easy - just always use A.x.

A.x = 4
writeln(A.x)

So how about a static method? This is also easy. You just make a method and always call it on the class.

class A
{
	x = 0

	function incX()
		:x++
}

A.incX()
writeln(A.x) // prints 1

But wait, these just look like normal class fields and methods. What's different? Nothing, really. It's just it how you access it.

Another tip on writing static methods: if you want to ensure that the method is only ever called on the class, you can use a parameter type constraint on the this parameter:

class A
{
	x = 0

	function incX(this: class)
		:x++
}

A.incX()
writeln(A.x) // prints 1

local a = A()
a.incX() // error, `this` must be a class, not an instance

How do you implement properties?

You could just use a field for simpler properties, since there are no field protection mechanisms. But for nontrivial setters and getters, the opField metamethod can be used to implement properties.

The opField mechanism is flexible enough that you can use all sorts of interesting mechanisms for implementing properties. You could go overboard and have decorators on functions and have property objects with set() and get() functions and do autoinheriting of properties, or you could do something as simple as a table that you keep the properties in. Whatever, it's up to you!

Here's a simple implementation of properties that just keeps them in a table field named _props. When you try to access a field which doesn't exist in the class, it'll fall back to the opField[Assign] methods that the properties decorator defines, which will look up the field name in the _props field, and call the appropriate setter or getter. You define setters and getters by declaring member functions named "get_name" and "set_name" in the class that you're decorating.

module test

// Decorator function which sets up the _props field.
// Takes a list of property names as varargs
function properties(c: class, vararg)
{
	// Check that all the property names are strings
	local propNames = [vararg]

	if(!propNames.all(isString))
		throw "All property names must be strings"

	// Set up the props table
	local props = {[name] = {} for name in propNames}
	local fields = fieldsOf(c)

	foreach(name, prop; props)
	{
		// Look for a getter
		local getter = "get_" ~ name

		if(getter in fields)
			prop.get = fields.(getter)

		// Look for a setter
		local setter = "set_" ~ name

		if(setter in fields)
			prop.set = fields.(setter)

		// Did we find either a getter or a setter?
		if(#prop == 0)
			throw format("No setter or getter defined for property '{}'", name)
	}

	// Actually add the _props field to the class
	c._props = props

	// Add the opField function.  Note that we don't check if the class already defines one;
	// if we wanted this to play nice with other opFields, we'd check for it and make a
	// chained call or somesuch.
	c.opField = function opField(name)
	{
		// We do some checks here to give nice error messages.  Not completely necessary;
		// if we wanted speed over clarity, we could just write:
		// return :_props[name].get(with this)
		if(local p = :_props[name])
		{
			if(local g = p.get)
				return g(with this)
			else
				throw format("Attempting to get write-only property '{}' of instance of '{}'", name, nameOf(:super))
		}
		else
			throw format("No property '{}' for instance of {}", name, nameOf(:super))
	}

	// opFieldAssign.
	c.opFieldAssign = function opFieldAssign(name, value)
	{
		if(local p = :_props[name])
		{
			if(local s = p.set)
				return s(with this, value)
			else
				throw format("Attempting to set read-only property '{}' of instance of '{}'", name, nameOf(:super))
		}
		else
			throw format("No property '{}' for instance of {}", name, nameOf(:super))
	}

	// Don't forget to return the class from the decorator!
	return c
}

// Decorate the class with "x" and "y" properties
@properties("x", "y")
class A
{
	mX = 0

	// notice the get_ and set_ names of the functions that correspond to the property names
	function get_x() = :mX
	function set_x(v)
	{
		writefln("setting x to {}", v)
		:mX = v
	}

	function get_y() = 10
}

local a = A()
// Whee!
a.x = 3
writeln("a.x = ", a.x)
writeln("a.y = ", a.y)

// Error - read-only property
a.y = 3

Can you do multiple inheritance?

The MiniD object model by default supports only single inheritance. This isn't all that restrictive in a dynamic language, since the class hierarchy isn't nearly as important when you have duck typing. As long as an object supports a set of methods, you don't have to care what type it is.

However it's sometimes nice to inherit functionality from multiple base objects. This is not all that hard to do. The basic idea is that you just .. well, copy the functionality from multiple bases into the derived class. Here is a simple implementation:

module test

function derive(c: class, vararg)
{
	local bases = [vararg]
	
	if(!bases.all(isClass))
		throw format("All bases must be classes")
		
	// Just do an exhaustive copy.
	foreach(base; bases)
		foreach(name, field; fieldsOf(base))
			c.(name) = field

	return c
}

class Base1
{
	x = 3
	function foo() writeln(:x)
}

class Base2
{
	y = 5
	function bar() writeln(:y)
}

@derive(Base1, Base2)
class Derived
{
	z = 10
	function baz() writeln(:z)
}

local d = Derived()
d.foo() // prints 3
d.bar() // prints 5
d.baz() // prints 10

This implementation is simple, but there's one shortcoming: if, for some reason, you modify one of the "fake" base classes, the modifications won't show up in the derived class, since the fields were copied before the modifications were made. For that, we'd need a more robust solution. We can use opField[Assign] and opMethod in this case. The idea is to keep track of the base classes in an array (a "static member" of the class). Then, when we call a method or access a field out of the derived class which isn't found, we do the method or field lookup manually in all of the bases, until we find one that has it or we exhaust all possibilities.

The following example only handles method inheritance. Field inheritance is done with opField and opFieldAssign and would look almost exactly the same, which is why I won't bother implementing it.

module test

function multiDerive(c: class, vararg)
{
	local bases = [vararg]

	if(!bases.all(isClass))
		throw format("All bases must be classes")

	// This time we just keep the list of bases as a member of the class.
	c._bases = bases
	
	c.opMethod = function opMethod(name, vararg)
	{
		// At this point we know we've called a method on the instance that doesn't
		// exist in it normally.  So we have to look for a method in one of the bases.
		
		foreach(base; :_bases)
			if(local owner = findField(base, name))
				return owner.(name)(with this, vararg)
				
		// That's all we have to do.  There was no method found if we get here, so error.
		throw format("No method named '{}' found for instance of class '{}'", name, nameOf(:super))
	}

	// If we wanted to support field lookup as well, we'd put opField and opFieldAssign
	// overloads here, which would look almost the same.  Note that when you assign a field
	// into an instance for the first time, you should add it to 'this' to be consistent with
	// the way field lookup works normally.

	return c
}

class Base1
{
	function foo() writeln("foo!")
}

class Base2
{
	function bar() writeln("bar!")
}

@multiDerive(Base1, Base2)
class Derived
{
	function baz() writeln("baz!")
}

local d = Derived()
d.foo() // prints foo!
d.bar() // prints bar!
d.baz() // prints baz!

You might also want to come up with functions to see if a class "derives" from another class by doing a depth-first search on its _bases member.