Inheritance
Motivation
In the section Fuzion Choice Type, one of Fuzion's mechanisms to store data of different types has been presented. Choice types are ideal for situations in which the number of possible choices is known and fixed from the beginning, while new operations on that data might be added later with little effort. Any new operation has to provide code to handle all the choices that a type provides. The compiler will check if the code is complete.
However, in situations where the kind of operations on data are known and fixed, but the actual kind of data might change and new data might be added, the choice types provide little help.
One classic example is a graphical editor that supports an extensible set of graphical objects such as boxes, circles, text blocks, polygons, images, etc. A small set of operations must be performed on these objects. E.g., drawing onto the screen, scaling the object.
For this purpose, object-oriented techniques such as classes with inheritance and dynamic binding are ideal. However, these concepts require additional data for type information and additional code execution for call resolution. This is why many purely object-oriented languages suffer from a performance penalty.
Fuzion is a pure object oriented language in the sense that every feature defines a type that can be used for inheritance and dynamic binding. However, fuzion does not require type information or dynamic binding in cases that are not polymorphic. Furthermore, the compiler specializes code to avoid this overhead in many of the remaining cases.
Example code
drawable(x, y i32) abstract is draw(g graphics) abstract is move(dx, dy i32) is x := x + dx y := y + dy
circle(x, y, radius i32) : drawable x y is draw(g graphics) is g.draw_circle x y radius
square(x, y, side i32) : drawable x y is draw(g graphics) is g.draw_rect x y x+side y+side
Multiple Inheritance
Fuzion supports multiple inheritance, such that different aspects of a
feature can be expressed via inheritance. An example is a numeric value, that
defines a total order and that can be converted to a string. Consequently, the
feature defining the numeric value should inherit from features like
comparable
and printable
.
Inheritance Conflicts
The presence of multiple inheritance leads to several possible conflicts, that need to be resolved:
Name Conflicts
A conflicting inheritance occurs when a feature inherits from two different
parents features that happen to have the same name. Say a feature
boat
has a feature max_speed
, while a car
also defines max_speed
. If you wanted to define a feature
amphibious_vehicle
you would get two different features
max_speed
with different meanings.
Repeated Inheritance
Repeated inheritance happens when the same feature is inherited through different parents. This is no problem if the feature is not redefined on the way. The original feature will then appear only once in the heir.
However, if a feature is redefined on one or several of the inheritance paths, we end up with conflicting implementations that need to be resolved somehow.
Conflicting Generics
Through inheritance, a feature could inherit repeatedly from a generic parent giving different actual generic arguments.
Invisible Conflict
A conflict might occur between features that are not visible by the heir class. For example, a module might export a feature that redefines a feature that is visible only locally within the module. Repeated inheritance in another module could then result in conflicts that cannot be predicted by the developer of the other module.
Resolving Conflicts
Renaming
A mechanism to rename features that are inherited helps to solve name
conflicts. If conflicting features can be renamed, using the example from above, we
could have two features max_speed_on_land
and
max_speed_on_water
.
TBD: In case one feature f
defined in feature A
is
inherited repeatedly through B
and C
and renamed
as f1
and f2
in the heir class D
, which
version should be called in a call a.f
with the target having
static type A
? Possible options: explicit selecting one renamed
version or just using the first inheritance clause.
Redefinition
In case of conflicting implementations, the heir class could provide a new implementation that meets the requirements of both implementations that were inherited.
Handling Generics
A simple solution for conflicting generics would be to just forbid repeated inheritance unless the generic arguments are equal.
Alternatively, generic classes with different actual generic arguments could be treated like completely different classes. Then, the repeated inheritance results in name conflicts that could be resolved by renaming.
Visibility
One solution to avoid invisible conflicts would be to forbid inheriting from features and redefining inherited features that have a visibility that is more restrictive than the heir class' visibility. I.e., a feature exported from a module may only inherit from features that are also exported from the module and may only redefine features that are also exported.
One consequence this has is that inheritance cannot be hidden, inheritance is not an implementation detail. Consequently, inheritance should not be used as a means of implementation of a feature unless this implementation should be revealed to the public.
As an example, say you implement a module that provides basic data types like
stack
based on an internal datatype my_fancy_list
.
Since you do not want to expose the internal datatype, stack
must
not inherit from my_fancy_list
. Instead, stack
could
contain a field of type my_fancy_list
.
Late Inheritance
It should be possible to add inheritance relations to existing features from other modules as long as these new inheritance relations are invisible to that other module. This is similar to adding the implementation of a trait to a type in Rust.
This could result in name conflicts and repeated inheritance. Name conflicts can be solved by renaming within the heir module.
To solve repeated inheritance, redefining external features is not an option: apart from being very error-prone it would result in new conflicts when there are conflicting redefinitions in different modules. Instead, the only permissible way to solve repeated inheritance would be renaming of the feature that was added later.