InheritableFields.jl provides a convenient, robust way to define abstract types with fields that are ultimately inherited by concrete subtypes. (Similar functionality is offered by many other packages, including Classes.jl, ConcreteAbstractions.jl, OOPMacro.jl, ObjectOriented.jl, and Inherit.jl; see also Mixers.jl and ReusePatterns.jl.)
This package does not aim to broadly recreate an object-oriented programming style in Julia, nor does it aim to provide a way to define and/or enforce interfaces.
Use the macro @abstract to define an abstract type with associated fields:
@abstract A{T<:Number} begin
s::String
x::T
end
If the type has no fields, the begin end block may be omitted from the definition. Abstract types can be subtyped:
@abstract B{T} <: A{T} begin
i::Int
end
Use @mutable or @immutable to create mutable or immutable concrete type (struct) that inherits the fields of all its @abstract supertypes and can introduce additional fields:
@immutable C{T} <: B{T} begin
b::Bool
end
In this example, instances of type C{T} will have fields s::String, x::T, i::Int, and b::bool, in that order. An instance of a @mutable or @immutable type can be constructed in the usual way, by calling the type with a value for each field in order. However, construction using keywords (described below) is more flexible and powerful.
Note that only abstract types can be inherited, in accordance with Julia's fundamental design. To achieve what ammounts to a hierarchy of concrete types, say Person >: Employee :> Salaried, one can use the following approach: First. create a hierarchy of @abstract types, e.g. AbstractPerson :> AbstractEmployee >: AbstractSalaried. Second, define methods that dispatch on these abstract types. Finally, define a concrete type for each abstract type in the hierarachy (e.g. @mutable Person <: AbstractPerson, @mutable Employee <: AbstractEmployee, etc.).
Construction of an object with fields defined in separate places can be tricky.
To facilitate this, the macros @mutable and @immutable automatically define several constructors:
- An inner constructor that resembles a default constructor, but allows each type in the hierarchy to validate its input arguments.
- A keyword-based outer constructor in which values may be assigned to named fields in any order, and omitted fields take default values provided in the type definitions.
- A keyword-based outer constructor that uses an existing object to provide default values. If the object has type parameters, two versions of each constructor are created: one with explicit type parameters, and one in which the type parameters are inferred from the arguments.
A copy method for the type is also created.
Within the body of an @abstract, @mutable, or @immutable type definition, one can implement a special method to validate construction values for the fields introduced by that type. For example, suppose type A requires x to be nonnegative. This can be enforced as follows:
@abstract A{T<:Number} begin
s::String
x::T
function validate(s, x)
x >= zero(T) || error("x must be non-negative")
return (s, x)
end
end
Suppose types B and C are defined as above. Attempting to construct an instance of C (a subtype of A) with a negative value for x will produce an error:
c = C(; i = -6, b = true, x = -1.2, s = "hello")
ERROR: x must be non-negative
A validate method defined in an @abstract, @mutable, or @immutable definition is called whenever an instance based on that type is constructed. It is passed candidate values for the type's fields as if it were the default inner constructor. The method should either return a tuple of field values or throw an exception.
If no validation method is provided, a fallback method that simply returns the input arguments is used.
In addition to the standard constructor, a keyword-based constructor is defined for each @mutable or @immutable type. This allows field values to be specified in any order using the field names as keywords:
c = C(; i = -6, x = 1.2, b = true, s = "hello") # == C{Float64}("hello", 1.2, -6, true)
Besides providing increased readibility and robustness to field order, the keyword constructor allows the use of default values (explained next).
Any field declaration may optionally include a default value by expressing it as an assignment:
@abstract A{T<:Number} begin
s::String = "goodbye"
x::T
end
If a default value is provided, the field's type may be omitted (but see the Limitations below); in this case the field's type is taken to be that of the provided value. The default value of a field is used when the keyword constructor is invoked and no value is provided for that field:
c = C(; i = -6; b = true; x = 1.2) # == C{Float64}("goodbye", 1.2, -6, true)
Default values can also be obtained from an existing instance:
new_c = C(c; s = "goodbye")
In this case, values for the unspecified fields are copied from the provided instance.
Additional fields or validators can be added to an @abstract type after its initial definition.
(This can be useful to avoid circular type definitions.) For example,
@abstract A{T<:Number} begin
s::String
end
@addtotype A{X} begin
x::X
end
is equivalent to the very first example above. Note that when using @addtotype, it is not possible to introduce additional type parameters or change the bounds on existing type parameters (since the type has already been defined). Also note that only concrete subtypes defined after @addtotype will acquire the added fields.
When a @mutable or @immutable type is defined, formal and literal type parameters are propagated up the chain of type definitions so that inherited fields are expressed in terms of the correct type parameters.
Similarly, all symbols appearing in a type definition are implicitly qualified by the module in which the definition occurs, so that they will be resolved correctly even when subtypes are defined in different modules.
Within a type definition, if the default value given for a field depends on a type parameter, for example x = zero(T), the field's type cannot be automatically inferred. In this case the type must be explicitly provided, e.g. x::T = zero(T).
Vararg type parameters have not been tested may not work correctly.
The @abstract macro:
- Defines the specified type as
abstract type. - Defines the
validatemethod for the type (if provided). - Stores the definition of the type's fields for later retrieval.
The @mutable or @immutable macro:
- Defines the specified type as
structormutable struct.- Looks up the inherited fields of all
@abstractsupertypes and includes them. - Creates an inner constructor that calls the
validatemethod for each (super)type the corresponding arguments.
- Looks up the inherited fields of all
- Defines the
validatemethod for the type (if provided). - Defines keyword-based outer constructors.
- Defines a copy method for the type.
validate methods are actually defined slightly differently than how the appear in the type definition: They have an additional argument at the front, namely, the type for which the method is defined. This allows validate to dispatch to the appropriate method.