Structures and Inheritance
In order to understand the full definitions of Functor
, Applicative
, and Monad
, another Lean feature is necessary: structure inheritance.
Structure inheritance allows one structure type to provide the interface of another, along with additional fields.
This can be useful when modeling concepts that have a clear taxonomic relationship.
For example, take a model of mythical creatures.
Some of them are large, and some are small:
structure MythicalCreature where
large : Bool
deriving Repr
Behind the scenes, defining the MythicalCreature
structure creates an inductive type with a single constructor called mk
:
#check MythicalCreature.mk
MythicalCreature.mk (large : Bool) : MythicalCreature
Similarly, a function MythicalCreature.large
is created that actually extracts the field from the constructor:
#check MythicalCreature.large
MythicalCreature.large (self : MythicalCreature) : Bool
In most old stories, each monster can be defeated in some way. A description of a monster should include this information, along with whether it is large:
structure Monster extends MythicalCreature where
vulnerability : String
deriving Repr
The extends MythicalCreature
in the heading states that every monster is also mythical.
To define a Monster
, both the fields from MythicalCreature
and the fields from Monster
should be provided.
A troll is a large monster that is vulnerable to sunlight:
def troll : Monster where
large := true
vulnerability := "sunlight"
Behind the scenes, inheritance is implemented using composition.
The constructor Monster.mk
takes a MythicalCreature
as its argument:
#check Monster.mk
Monster.mk (toMythicalCreature : MythicalCreature) (vulnerability : String) : Monster
In addition to defining functions to extract the value of each new field, a function Monster.toMythicalCreature
is defined with type Monster → MythicalCreature
.
This can be used to extract the underlying creature.
Moving up the inheritance hierarchy in Lean is not the same thing as upcasting in object-oriented languages.
An upcast operator causes a value from a derived class to be treated as an instance of the parent class, but the value retains its identity and structure.
In Lean, however, moving up the inheritance hierarchy actually erases the underlying information.
To see this in action, consider the result of evaluating troll.toMythicalCreature
:
#eval troll.toMythicalCreature
{ large := true }
Only the fields of MythicalCreature
remain.
Just like the where
syntax, curly-brace notation with field names also works with structure inheritance:
def troll : Monster := {large := true, vulnerability := "sunlight"}
However, the anonymous angle-bracket notation that delegates to the underlying constructor reveals the internal details:
def troll : Monster := ⟨true, "sunlight"⟩
application type mismatch
Monster.mk true
argument
true
has type
Bool : Type
but is expected to have type
MythicalCreature : Type
An extra set of angle brackets is required, which invokes MythicalCreature.mk
on true
:
def troll : Monster := ⟨⟨true⟩, "sunlight"⟩
Lean's dot notation is capable of taking inheritance into account.
In other words, the existing MythicalCreature.large
can be used with a Monster
, and Lean automatically inserts the call to Monster.toMythicalCreature
before the call to MythicalCreature.large
.
However, this only occurs when using dot notation, and applying the field lookup function using normal function call syntax results in a type error:
#eval MythicalCreature.large troll
application type mismatch
troll.large
argument
troll
has type
Monster : Type
but is expected to have type
MythicalCreature : Type
Dot notation can also take inheritance into account for user-defined functions. A small creature is one that is not large:
def MythicalCreature.small (c : MythicalCreature) : Bool := !c.large
Evaluating troll.small
yields false
, while attempting to evaluate MythicalCreature.small troll
results in:
application type mismatch
MythicalCreature.small troll
argument
troll
has type
Monster : Type
but is expected to have type
MythicalCreature : Type
Multiple Inheritance
A helper is a mythical creature that can provide assistance when given the correct payment:
structure Helper extends MythicalCreature where
assistance : String
payment : String
deriving Repr
For example, a nisse is a kind of small elf that's known to help around the house when provided with tasty porridge:
def nisse : Helper where
large := false
assistance := "household tasks"
payment := "porridge"
If domesticated, trolls make excellent helpers. They are strong enough to plow a whole field in a single night, though they require model goats to keep them satisfied with their lot in life. A monstrous assistant is a monster that is also a helper:
structure MonstrousAssistant extends Monster, Helper where
deriving Repr
A value of this structure type must fill in all of the fields from both parent structures:
def domesticatedTroll : MonstrousAssistant where
large := false
assistance := "heavy labor"
payment := "toy goats"
vulnerability := "sunlight"
Both of the parent structure types extend MythicalCreature
.
If multiple inheritance were implemented naïvely, then this could lead to a "diamond problem", where it would be unclear which path to large
should be taken from a given MonstrousAssistant
.
Should it take large
from the contained Monster
or from the contained Helper
?
In Lean, the answer is that the first specified path to the grandparent structure is taken, and the additional parent structures' fields are copied rather than having the new structure include both parents directly.
This can be seen by examining the signature of the constructor for MonstrousAssistant
:
#check MonstrousAssistant.mk
MonstrousAssistant.mk (toMonster : Monster) (assistance payment : String) : MonstrousAssistant
It takes a Monster
as an argument, along with the two fields that Helper
introduces on top of MythicalCreature
.
Similarly, while MonstrousAssistant.toMonster
merely extracts the Monster
from the constructor, MonstrousAssistant.toHelper
has no Helper
to extract.
The #print
command exposes its implementation:
#print MonstrousAssistant.toHelper
@[reducible] def MonstrousAssistant.toHelper : MonstrousAssistant → Helper :=
fun self =>
{ toMythicalCreature := self.toMonster.toMythicalCreature, assistance := self.assistance, payment := self.payment }
This function constructs a Helper
from the fields of MonstrousAssistant
.
The @[reducible]
attribute has the same effect as writing abbrev
.
Default Declarations
When one structure inherits from another, default field definitions can be used to instantiate the parent structure's fields based on the child structure's fields.
If more size specificity is required than whether a creature is large or not, a dedicated datatype describing sizes can be used together with inheritance, yielding a structure in which the large
field is computed from the contents of the size
field:
inductive Size where
| small
| medium
| large
deriving BEq
structure SizedCreature extends MythicalCreature where
size : Size
large := size == Size.large
This default definition is only a default definition, however.
Unlike property inheritance in a language like C# or Scala, the definitions in the child structure are only used when no specific value for large
is provided, and nonsensical results can occur:
def nonsenseCreature : SizedCreature where
large := false
size := .large
If the child structure should not deviate from the parent structure, there are a few options:
- Documenting the relationship, as is done for
BEq
andHashable
- Defining a proposition that the fields are related appropriately, and designing the API to require evidence that the proposition is true where it matters
- Not using inheritance at all
The second option could look like this:
abbrev SizesMatch (sc : SizedCreature) : Prop :=
sc.large = (sc.size == Size.large)
Note that a single equality sign is used to indicate the equality proposition, while a double equality sign is used to indicate a function that checks equality and returns a Bool
.
SizesMatch
is defined as an abbrev
because it should automatically be unfolded in proofs, so that simp
can see the equality that should be proven.
A huldre is a medium-sized mythical creature—in fact, they are the same size as humans.
The two sized fields on huldre
match one another:
def huldre : SizedCreature where
size := .medium
example : SizesMatch huldre := by
simp
Type Class Inheritance
Behind the scenes, type classes are structures. Defining a new type class defines a new structure, and defining an instance creates a value of that structure type. They are then added to internal tables in Lean that allow it to find the instances upon request. A consequence of this is that type classes may inherit from other type classes.
Because it uses precisely the same language features, type class inheritance supports all the features of structure inheritance, including multiple inheritance, default implementations of parent types' methods, and automatic collapsing of diamonds. This is useful in many of the same situations that multiple interface inheritance is useful in languages like Java, C# and Kotlin. By carefully designing type class inheritance hierarchies, programmers can get the best of both worlds: a fine-grained collection of independently-implementable abstractions, and automatic construction of these specific abstractions from larger, more general abstractions.