Interfaces
Macros for defining the required behaviours of Julia interfaces, and stating that an object implements them.
The goal is to get as much as possible out of defining an interface, specifically:
- Traits: All
@implements
declarations produce compile-time traits that can be checked by other packages - for the whole interface and all of it's optional components. - Tests:
@implements
declarations should be tested in package tests. - Docs: interface documentation can be inserted into trait documentation.
Note: the syntax here is likely to change over 2022 as we work out the best ways to define interfaces
Example
See the IterationInterface
in BaseInterfaces.jl (a subpackage of this package)
for examples of @interface
and @implements
.
But heres an examples using Animals, and the implementation of a Duck.
First we define the interface methods, and a list of mandatory and
optional properties of the interface, with conditions, using the @interface
macro.
The @interface
macro takes two argumens
- The name of the interface, which should usully end with "Interface"
- The
mandatory
andoptional
components of the interface written as aNamedTuple
, with functions or tuple of functions that test them.
module Animals
using Interfaces
# Define the methods the interface uses
function age end
function walk end
function talk end
function dig end
# Define the interface conditions
@interface AnimalInterface (
mandatory = (;
age = (
x -> age(x) isa Real,
x -> age(x) >= 0,
)
),
optional = (;
walk = x -> walk(x) isa String,
talk = x -> talk(x) isa Symbol,
dig = x -> dig(x) isa String,
),
)
end
Now we can implement the AnimalInterface, for a Duck.
The @implements
macro takes three arguments.
- The interface type, with a tuple of optional components in its first type parameter.
- The the type of the object implementing the interface
- Some code that defines an instance of that type that can be used in tests.
using Interfaces
# Define our Duck object
struct Duck
age::Int
end
# And extend Animals methods for it
Animals.age(duck::Duck) = duck.age
Animals.walk(::Duck) = "waddle"
Animals.talk(::Duck) = :quack
# And define the interface
@implements Animals.AnimalInterface{(:walk, :talk)} Duck [Duck(1), Duck(2)]
Now we have some methods we can use as traits, and test the interface with:
julia> Interfaces.implements(Animals.AnimalInterface{:walk}, Duck)
true
julia> Interfaces.implements(Animals.AnimalInterface{:dig}, Duck)
false
# We can test the interface
julia> Interfaces.test(Animals.AnimalInterface, Duck)
true
# Or components of it:
julia> Interfaces.test(Animals.AnimalInterface{(:walk,:talk)}, Duck)
true
# Test another object
struct Chicken end
julia> Interfaces.implements(Animals.AnimalInterface, Chicken())
false
If you think it should behave differently or there is better syntax, please make an issue.