Advanced
Here's an example of a multi-argument interface where we implement groups. For mathematicians, a group is just a set of objects where you can perform multiplication
and inversion
, such that an element multiplied by its inverse yields a neutral
element.
This functionality is still experimental and might evolve in the future. If you have feedback about it, open an issue to help us improve it!
using Interfaces
Definition
Unlike the AnimalInterface
, this example involves functions with more than one argument. Such arguments need to be passed to the interface testing code, which means the interface definition must take them into account as well.
For technical reasons, we provide a type called Arguments
that you should use for this purpose. It behaves exactly like a NamedTuple
but enables easier dispatch.
module Group
using Interfaces
function neutral end
function multiplication end
function inversion end
@interface GroupInterface Number (
mandatory = (;
neutral_check = (
"neutral stable" => a::Arguments -> neutral(typeof(a.x)) isa typeof(a.x),
),
multiplication_check = (
"multiplication stable" => a::Arguments -> multiplication(a.x, a.y) isa typeof(a.x),
),
inversion_check = (
"inversion stable" => a::Arguments -> inversion(a.x) isa typeof(a.x),
"inversion works" => a::Arguments -> multiplication(a.x, inversion(a.x)) ≈ neutral(typeof(a.x)),
),
),
optional = (;),
) """
A group is a set of elements with a neutral element where you can perform multiplications and inversions.
The conditions checking the interface accept an `Arguments` object with two fields named `x` and `y`.
The type of the first field `x` must be the type you wish to declare as implementing `GroupInterface`.
"""
end;
Implementation
Let's try to see if our favorite number types do indeed behave like a group.
Group.neutral(::Type{N}) where {N<:Number} = one(N)
Group.multiplication(x::Number, y::Number) = x * y
Group.inversion(x::Number) = inv(x)
First, we check it for floating point numbers, giving a list of Arguments
objects with the proper fields to the test
function.
float_pairs = [Arguments(x = 2.0, y = 1.0)]
Interfaces.test(Group.GroupInterface, Float64, float_pairs)
true
We can thus declare proudly
@implements Group.GroupInterface Float64 [Arguments(x = 2.0, y = 1.0)]
Now we check it for integer numbers. The reason it fails is because for an integer x
, the inverse 1/x
is no longer an integer! Thus integer numbers are not a multiplicative group.
int_pairs = [Arguments(x = 2, y = 1)]
Interfaces.test(Group.GroupInterface, Int, int_pairs)
false
What happens if we give an input whose field types (Int
) are not coherent with the type we are testing (Float64
)?
try
Interfaces.test(Group.GroupInterface, Float64, int_pairs)
catch e
print(e.msg)
end
Each tested object must either be an instance of `Float64` or an instance of `Arguments` whose field types include at least one subtype of `Float64`. You provided a `Arguments{(:x, :y), Tuple{Int64, Int64}}` instead.
In summary, there are two things to remember:
- The anonymous functions in the interface conditions of
Interfaces.@interface
should accept a single object of typeArguments
and then work with its named fields. These fields should be listed in the docstring. - The list of objects passed to
Interface.test
must all be of typeArguments
, with the right named fields. At least one field must have the type you are testing.
This page was generated using Literate.jl.