Hi, I’m pleased to share TraitInterfaces.jl. It provides an @interface
macro to declare interfaces and an @instance
macro to declare implementations of those interfaces. These implementations are identified with Julia values, which we think of as traits. I write a lot more in the README, but I’ll excerpt a small piece here. I’ll start by showing the use of these macros in the simplest of cases and then show what the macros expand to.
struct Sheep
naked::Bool
name::String
end
joe = Sheep(true, "Joe")
@interface AnimalInterface′ begin
Species::TYPE # 'abstract type'
@import String::TYPE # 'concrete type'
name(s::Species)::String
noise(s::Species)::String
end
struct SheepImplsAnimalTrait end
trait = SheepImplsAnimalTrait()
@instance AnimalInterface′{Species=Sheep} [model::SheepImplsAnimalTrait] begin
name(s::Sheep)::String = s.name
noise(s::Sheep)::String = s.naked ? "baaaaah?" : "baaaaah!"
end
@test name[trait](joe) == "Joe" # equivalent to name(Trait(trait), joe)
@test noise[trait](joe) == "baaaaah?"
AnimalInterface′.Meta.@wrapper Animal # type which abstracts implementation details
joe_animal = Animal(joe)
# error because there is no `@instance Animal [model::Int] ...`
@test_throws MethodError Animal(100)
@test name(joe_animal) == "Joe" # equivalent to name[joe]()
The @interface
command first expands to putting the abstract types and operations into the namespace where @interface
is being declared. Then these are imported into a newly created module:
function name end
function noise end
function Species end
module AnimalInterface′
export name, noise, Species
import ..Foo: name, noise, Species # if the ambient module is Foo
module Meta
struct T end # A special type associated with the interface
# Copy of the Julia data structure that stores the content of the interface
const theory = Interface(:AnimalInterface′, Judgment[...])
macro wrapper(n)
... # to be explained below
end
end
end
For convenience, we add getindex
methods so that we can call my_operation[implementation](args...)
to avoid requiring explicit Trait()
wrapping. E.g.:
Base.getindex(::typeof(name), m::Any) = (args...; kw...) -> name(Trait(m), args...; kw...)
Base.getindex(::typeof(noise), m::Any) = (args...; kw...) -> noise(Trait(m), args...; kw...)
The @implements
code generates the following methods:
function AnimalInterface′.name(m::Trait{<:SheepImplsAnimalTrait}, s::Sheep)::String
let model = m.value
s.name # code that was explicitly written by user appears here
end
end
function AnimalInterface′.noise(m::Trait{<:SheepImplsAnimalTrait}, s::Sheep)::String
let model = (m).value
s.naked ? "baaaaah?" : "baaaaah!"
end
end
It then generates code to check whether the interface has been fully implemented:
if !(hasmethod(AnimalInterface′.noise, Tuple{Trait{SheepImplsAnimalTrait}, Sheep}))
throw(MissingMethodImplementation(...))
end
# likewise for `noise`
Lastly it stores the information of how this implementation assigned concrete types to the abstract type of the interface:
impl_type(::SheepImplsAnimalTrait, ::typeof(AnimalInterface′.Species)) = Sheep
The @wrapper
macro generates the following code:
@struct_hash_equal struct Animal
val::Any
types::Dict{Symbol, Type}
function Animal(x::Any)
types = try
Dict(:Species => impl_type(x, AnimalInterface′.Species))
catch _
error("Invalid $AnimalInterface′ model: $x")
end
new(x, types)
end
end
Base.get(x::Animal) = x.val
impl_type(x::Animal, o::Symbol) = x.types[o]
AnimalInterface′.noise(x::Animal, args...; kw...) =
AnimalInterface′.noise(Trait(x.val), args...; kw...)
AnimalInterface′.name(x::Animal, args...; kw...) =
AnimalInterface′.name(Trait(x.val), args...; kw...)
There are lots of more features, such as aliases, default methods, extending interfaces, multiple inheritance (e.g. ThRing
combines ThAbelianGroup
and ThMonoid
), declaring types which depend on types and depend on terms, traits which have nontrivial data (e.g. struct ModuloArith n::Int end
, rather than traits being zero-field structs), traits which have type parameters (e.g. implementing an interface with Matrix{T} where T
).
I’m not an expert in the other interface / trait libraries in Julia, so it will take some time to draw detailed comparisons. I also noticed just before posting that MultipleInterfaces.jl got announced recently!