Introduction
Inherit.jl is used to inherit fields and interface definitions from a supertype. It supports programming with an object-oriented flavor in Julia, whenever this is more appropriate than developing under traditional Julia patterns.
Fields defined in a supertype are automatically inherited by each subtype, and method declarations are checked for each subtype's implementation. An inheritance hierachy across multiple modules is supported. To accomplish this, macro processing is used to construct native Julia types, which allows the the full range of Julia syntax to be used in most situations.
Quick Start
Use @abstractbase
to declare an abstract supertype, and use @implement
to inherit from such a type. Standard struct
syntax is used.
using Inherit
"
Base type of Fruity objects.
Creates a julia native type with
`abstract type Fruit end`
"
@abstractbase struct Fruit
weight::Float64
"declares an interface which must be implemented"
function cost(fruit::Fruit, unitprice::Float64) end
end
"
Concrete type which represents an apple, inheriting from Fruit.
Creates a julia native type with
`struct Apple <: Fruit weight::Float64; coresize::Int end`
"
@implement struct Apple <: Fruit
coresize::Int
end
"
Implements supertype's interface declaration `cost` for the type `Apple`
"
function cost(apple::Apple, unitprice::Float64)
apple.weight * unitprice * (apple.coresize < 5 ? 2.0 : 1.0)
end
println(cost(Apple(3.0, 4), 1.0))
# output
6.0
Note that the definition of cost
function inside of Fruit
is interpreted as an interface declaration; it does not result in a method being defined.
What this declaration means is that when invoking the cost
function, passing an object which is a subtype of Fruit
(declared with the @implement
macro) to the fruit::Fruit
parameter must be able to dispatch to some method instance. This is verified when a module is first loaded.
Interaction with modules
An object oriented programming style can be useful to applications that span across multiple modules. Even though Inherit.jl can be used inside of scripts, its true usefulness is to assert common interfaces shared by different data types from different modules. Verification of method declarations take place in the __init__()
function of the module which the implementing type belongs to (i.e. where the @implement
macro is used).
The module __init__()
function
The specially named module-level (i.e. top-level) function __init__()
is called after the module has been fully loaded by Julia. If an interface definition has not been met, an exception will be thrown.
module M1
using Inherit
@abstractbase struct Fruit
weight::Float64
function cost(fruit::Fruit, unitprice::Float64) end
end
@implement struct Apple <: Fruit end
@implement struct Orange <: Fruit end
@implement struct Kiwi <: Fruit end
function cost(fruit::Union{Apple, Kiwi}, unitprice::Float64)
1.0
end
end
# output
ERROR: InitError: ImplementError: subtype M1.Orange missing Tuple{typeof(M1.cost), M1.Orange, Float64} declared as:
function cost(fruit::Fruit, unitprice::Float64)
[...]
Upon loading module M1
, Inherit.jl throws an ImplementError
from the __init__()
function, telling you that it's looking for a method signature that can dispatch cost(::M1.Orange, ::Float64)
. It makes no complaints about Apple
and Kiwi
because their dispatch can be satisfied.
The @postinit
macro
The presence of an @abstractbase
or @implement
macro causes Inherit.jl to generate and overwrite the module's __init__()
function. To execute your own module initialization code, the @postinit
macro is available. It accepts a function as argument and registers that function to be executed after __init__()
. Multiple occurrences of @postinit
will result in each function being called successively.
Putting it all together
Let's demonstrate @postinit
as well as other features in a more extended example.
module M1
using Inherit
@abstractbase struct Fruit
weight::Float64
"docstrings of method declarations are appended at the end of method docstrings"
function cost(fruit::Fruit, unitprice::Float64) end
end
"this implementation satisfies the interface declaration for all subtypes of Fruit"
function cost(item::Fruit, unitprice::Real)
unitprice * item.weight
end
end
module M2
using Inherit
import ..M1
@abstractbase struct Berry <: M1.Fruit
"
In a declaration, the supertype can appear in a variety of positions.
A supertype argument can be matched with itself or a __narrower__ type.
Supertypes inside containers must be matched with itself or a __broader__ type.
"
function pack(time::Int, ::Berry, bunch::Vector{Berry}) end
"
However, if you prefix the supertype with `<:`, it becomes a ranged parameter. You can match it with a ranged subtype parameter.
"
function move(::Vector{<:Berry}, location) end
end
@implement struct BlueBerry <: Berry end
"
The implementing method's argument types can be broader than the interface's argument types.
Note that `AbstractVector{<:BlueBerry}` will not work in the 3rd argument, because a `Vector{Berry}` argument will have no dispatch.
"
function pack(time::Number, berry::BlueBerry, bunch::AbstractVector{<:M1.Fruit})
println("packing things worth \$$(cost(first(bunch), 1.5) + cost(berry, 1.5))")
end
"
The subtype `BlueBerry` can be used in a container, because it's a ranged parameter. Make sure nested containers are all ranged parameters; otherwise, the interface cannot be satisfied.
"
function move(bunch::Vector{<:BlueBerry}, location)
println("moving $(length(bunch)) blueberries to $location")
end
@postinit function myinit()
println("docstring of imported `cost` function:\n", @doc cost)
pack(0, BlueBerry(1.0), [BlueBerry(2.0)])
move([BlueBerry(1.0), BlueBerry(2.0)], "the truck")
end
end
nothing
# output
[ Info: Inherit.jl: processed M1 with 1 supertype having 1 method requirement. 0 subtypes were checked with 0 missing methods.
[ Info: Inherit.jl: processed M2 with 1 supertype having 3 method requirements. 1 subtype was checked with 0 missing methods.
docstring of imported `cost` function:
this implementation satisfies the interface declaration for all subtypes of Fruit
docstrings of method declarations are appended at the end of method docstrings
packing things worth $4.5
moving 2 blueberries to the truck
We can make a few observations regarding the above example:
- A summary message is printed after each module is loaded, showing Inherit.jl is active.
- Multiple levels of inheritance is possible across multiple modules.
- Method definitions are quite flexible. In a method declaration, you can name a supertype anywhere that's valid in Julia, and it will be checked for proper dispatch of subtypes.
- The function
M1.cost
was automatically imported into moduleM2
. The function still lives in moduleM1
together with its method instances, but it is available inM2
through the symbolcost
.- While not shown in this example, you can extend
M1.cost
by writingfunction cost(...) ... end
in moduleM2
- While not shown in this example, you can extend
- Docstrings are preserved. Docstring for method declarations are added to the end of any method docstrings.
When implementing a method declaration, supertypes inside of containers like (e.g. Pair
, Vector
, Dict
) may not be substituted with a subtype, because Julia's type parameters are invariant. However, a ranged supertype parameter (prefixed with <:
) can be substituted with a ranged subtype.
Changing the reporting level
To have module __init__()
log an error message instead of throwing an exception, add setreportlevel(ShowMessage)
near the front of the module. You can also disable interface checking altogether with setreportlevel(SkipInitCheck)
By default, module __init__()
writes its summary message at the Info
log level. You can change this by setting ENV["INHERIT_JL_SUMMARY_LEVEL"]
to one of ["debug", "info", "warn", "error", "none"]
.
Limitations
Parametric types are currently not supported. Basic support for parametric concrete types is being planned.
Methods are examined only for their positional arguments. Inherit.jl has no special knowledge of keyword arguments, but this may improve in the future.
Inherit.jl has no special knowledge about constructors (inner or otherwise). They're treated like normal functions. As a result, constructor inheritance is not available.
Short form function definitions such as f() = nothing
are not supported for method declaration; use the long form function f() end
instead. Using short form for method implementation can be problematic as well (e.g. when the function is imported from another module); it's generally safer to use long form.
Multiple inheritance
Multiple inheritance is currently not supported, but is being planned. It will have the following syntax:
@abstractbase struct Fruit
weight::Float64
function cost(fruit::Fruit, unitprice::Float64) end
end
@trait struct SweetFood
sugartype::Symbol
"
Subtype must define:
function sugarlevel(obj::T) end
where T<--SweetFood
"
function sugarlevel(obj<--SweetFood) end
end
@implement struct Apple <: Fruit _ <-- SweetFood
coresize::Int
end
function sugarlevel(apple::Apple) "depends on "*join(fieldnames(Apple),", ") end
"depends on weight, sugartype, coresize"
API
environment variable | value | description |
---|---|---|
JULIA_DEBUG | "Inherit" | Enables printing of more detailed Debug level messsages. Default is "" which only prints Info level messages |
INHERIT_JL_SUMMARY_LEVEL | "debug", "info", "warn", "error", or "none" | logs the per-module summary message at the chosen level, or none at all. Default is "info". |
Inherit.setglobalreportlevel
Inherit.setreportlevel
Inherit.@abstractbase
Inherit.@implement
Inherit.@postinit
Inherit.@abstractbase
— MacroCreates a Julia abstract type, while allowing field and method declarations to be inherited by subtypes created with the @implement
macro.
Requires a single expression of one of following forms:
struct T ... end
mutable struct T ... end
struct T <: S ... end
mutable struct T <: S ... end
Supertype S can be any valid Julia abstract type. In addition, if S was created with @abstractbase
, all its fields and method declarations will be prepended to T's own definitions, and they will be inherited by any subtype of T.
Mutability must be the same as the supertype's mutability.
Inherit.@implement
— MacroCreates a Julia struct
or mutable struct
type which contains all the fields of its supertype. Method interfaces declared (and inherited) by the supertype are required to be implemented.
Requires a single expression of one of following forms:
struct T <: S ... end
mutable struct T <: S ... end
Mutability must be the same as the supertype's mutability.
Method declarations may be from a foreign module, in which case method implementations must be added to the foreign module's function. If there is no name clash, the foreign modules's function is automatically imported into the implementing module (i.e. your current module). If there is a name clash, you must qualify the function name with the foreign module's name.
Inherit.@postinit
— MacroRequires a single function definition expression.
The function will be executed after Inherit.jl verfies interfaces. You may have any number of @postinit blocks; they will execute in the order in which they were defined.
The function name must be different from __init__
, or it will overwrite Inherit.jl interface verification code. Furthermore, you module must not contain any function named __init__
. Initialization code must use this macro with a changed name, or with an anonymous function. For example,
@postinit function __myinit__() ... end
@postinit () -> begin ... end
Inherit.setreportlevel
— FunctionSets the level of reporting for the given module. Takes precedence over global report level.
ThrowError
: Checks for interface requirements in the module __init__()
function, throwing an exception if requirements are not met. Creates __init__()
upon first use of @abstractbase or @implement.
ShowMessage
: Same as above, but an Error
level log message is produced rather than throwing exception.
SkipInitCheck
: Still creates __init__()
function (which sets up datastructures that may be needed by other modules) but won't verfiy interfaces. Cannot be set if __init__()
has already been created
Inherit.setglobalreportlevel
— FunctionSets the default reporting level of modules; it does not change the setting for modules that already loaded an Inherit.jl macro.