Lily allows classes to inherit from another class. By default, the child class cannot redefine methods of the parent class. This can become an issue if a child class wants to refine the behavior of a method in the parent.
Suppose there is a Point2D class that defines a method to view the
coordinates. If it's inherited by Point3D, then the method should show all
three coordinates instead of just two.
class Point2D(public var @x: Integer,
public var @y: Integer)
{
public virtual define show: String
{
return "Point2D({}, {})".format(@x, @y)
}
}
class Point3D(x: Integer,
y: Integer,
public var @z: Integer) < Point2D(x, y)
{
public virtual define show: String
{
return "Point3D({}, {}, {})".format(@x, @y, @z)
}
}
var p2 = Point2D(10, 20)
print(p2.show()) # Point2D(10, 20)
p2 = Point3D(5, 10, 15)
print(p2.show()) # Point3D(5, 10, 15)
If a class makes use of virtual method, then instances of it will have a table to route those methods. In the above example, the virtual table (vtable) routes the request according to the underlying type.
What if Point3D wanted to use a virtual method from Point2D?
class Point2D(public var @x: Integer,
public var @y: Integer)
{
public virtual define move(by: Integer)
{
@x += by
@y += by
}
}
class Point3D(x: Integer,
y: Integer,
public var @z: Integer) < Point2D(x, y)
{
public virtual define move(by: Integer)
{
Point2D.move(self, by)
@z += by
}
}
var p = Point2D(10, 20)
var p3 = Point3D(5, 10, 15)
p.move(100)
print([p.x, p.y]) # [110, 120]
p = p3
p.move(5)
print([p3.x, p3.y, p3.z]) # [10, 15, 20]
The vtable is used to route calls inside of a class the same way as they are
routed outside of a class. This can, again, be avoided by specifying
First.adjust instead of adjust.
class First
{
public var @data = 10
public virtual define adjust(x: Integer)
{
@data += x
}
public define do_something
{
adjust(10)
}
}
class Second < First
{
public virtual define adjust(x: Integer)
{
@data = 0
}
}
var v: First = Second()
v.do_something()
print(v.data) # 0
Virtual methods are permitted to use the same argument features as normal definitions (varargs, optional arguments, keyword arguments). However, virtual methods contain some limitations:
Parameter and result types must match exactly. This is for simplicity and to prevent soundness issues with vtable routing.
Virtual methods can only be overridden by another virtual method.
virtual and static are mutually exclusive to each other.
Forward classes are permitted to define forward methods. However, those forward methods cannot be virtual.
The above examples each have the base class provide a virtual method that is overriden. There are, however, situations where a placeholder would be useful.
Suppose you have an Animal class. You'd like to make sure that classes which
inherit it provide a speak method. If you supply Animal.speak, an inheriting
class may forget to implement it. You find out when you attempt to call speak
and your Animal.speak raises an error.
A forward virtual method is a placeholder that a child class must implement.
class Animal
{
forward public virtual define speak: String { ... }
}
class Cat < Animal
{
public virtual define speak: String
{
return "Meow"
}
}
class Dog < Animal {}
var c = Cat()
print(c.speak()) # Meow
# var a = Animal() # Syntax error: Animal.speak must be resolved.
In addition to the above rules for virtuals, there are some extra rules to consider with forward virtual methods.
Forward virtual methods cannot be resolved in the class they are declared in.
A class can finish with unresolved virtuals. However, attempting to construct an instance of it will result in a syntax error.
Because of the above, methods in Animal can call speak, because some child
class of Animal will have resolved speak. However, Animal.speak is a
syntax error, because Animal.speak specifically is a syntax error.
That may seem confusing on first read, but it is akin to this:
class First
{
public define do_something {}
}
class Second < First {}
# Second.do_something() # Syntax error, because it is First.do_something.