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.

Info

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 module M2. The function still lives in module M1 together with its method instances, but it is available in M2 through the symbol cost.
    • While not shown in this example, you can extend M1.cost by writing function cost(...) ... end in module M2
  • Docstrings are preserved. Docstring for method declarations are added to the end of any method docstrings.
Info

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 variablevaluedescription
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.@abstractbaseMacro

Creates 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.

source
Inherit.@implementMacro

Creates 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.

source
Inherit.@postinitMacro

Requires 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
source
Inherit.setreportlevelFunction

Sets 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

source
Inherit.setglobalreportlevelFunction

Sets the default reporting level of modules; it does not change the setting for modules that already loaded an Inherit.jl macro.

source