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.
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
validate
method 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
struct
ormutable struct
.- Looks up the inherited fields of all
@abstract
supertypes and includes them. - Creates an inner constructor that calls the
validate
method for each (super)type the corresponding arguments.
- Looks up the inherited fields of all
- Defines the
validate
method 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.