As the discussion above shows, we’re still figuring this out. Many would prefer to have some kind of interface check through some kind of static analysis. Honestly, we have made the interface checks too complicated thus far, while I also think the MethodError
approach is a bit too passive.
Rather than passively wait for MethodErrors
to arise, we should preemptively and simply test the interface. I’m starting to think we can use the dynamic features of Julia to test.
My proposal for interface testing is that one should provide a function to test the interface when declaring such an interface. Focus on testAbstractPerformerInterface
below.
module Performers
using Test
export AbstractPerformer, rejoice
abstract type AbstractPerformer end
function smile end
function say end
function rejoice(ap::AbstractPerformer)
smile(ap)
say(ap, "Oh joy!")
end
function testAbstractPerformerInterface(ap::AP) where AP <: AbstractPerformer
@testset "Testing if $AP implements AbstractPerformerInterface" begin
@test hasmethod(smile, Tuple{AP})
@test hasmethod(say, Tuple{AP, String})
@test isnothing(smile(ap))
@test isnothing(say(ap, "Oh joy!"))
end
end
end
# ... insert macro here ...
end # module Performers
An implementing type could do the following.
using Performers
struct MyPerformer <: AbstractPerformer end
Performers.testAbstractPerformerInterface(MyPerformer())
...
Test Summary: | Fail Error Total Time
Testing if MyPerformer implements AbstractPerformerInterface | 2 2 4 2.7s
ERROR: Some tests did not pass: 0 passed, 2 failed, 2 errored, 0 broken.
We could even provide some macro and preferences support so the tests do not have to run every time.
module Performers
# ... see above
# macro support
using Preferences
const abstract_performer_tests = @load_preference("abstract_performer_tests", false)
macro implementsAbstractPerformer(ex, T)
T = esc(T)
quote
$(esc(ex))
if abstract_performer_tests
testAbstractPerformerInterface($T)
end
end
end
end # module Performers
An implementing type could the be declared as follows.
using Performers
Performers.@implementsAbstractPerformer begin
struct MyPerformer <: AbstractPerformer end
Performers.smile(::MyPerformer) = println("*smiles*")
Performers.say(::MyPerformer, s::String) = println(s)
end MyPerformer()
The above will do nothing extra by default. However, during testing or development we could turn the tests on.
julia> using Preferences
julia> Preferences.set_preferences!("Performers", "run_abstract_performer_tests" => true, force=true)
julia> using Performers
julia> Performers.@implementsAbstractPerformer begin
struct MyPerformer <: AbstractPerformer end
Performers.smile(::MyPerformer) = println("*smiles*")
Performers.say(::MyPerformer, s::String) = println(s)
end MyPerformer();
*smiles*
Oh joy!
Test Summary: | Pass Total Time
Testing if MyPerformer implements AbstractPerformerInterface | 4 4 0.0s
For an incorrect implementation, we would get the following.
julia> Performers.@implementsAbstractPerformer begin
struct BadPerformer <: AbstractPerformer end
Performers.smile(::BadPerformer) = println("*smiles*")
Performers.say(::BadPerformer) = "Hello World!"
end BadPerformer();
Testing if BadPerformer implements AbstractPerformerInterface: Test Failed at /home/mkitti/blah/Performers.jl/src/Performers.jl:20
Expression: hasmethod(say, Tuple{AP, String})
Stacktrace:
...
*smiles*
Testing if BadPerformer implements AbstractPerformerInterface: Error During Test at /home/mkitti/blah/Performers.jl/src/Performers.jl:22
Test threw exception
Expression: isnothing(say(ap, "Oh joy!"))
MethodError: no method matching say(::BadPerformer, ::String)
Closest candidates are:
say(::BadPerformer)
@ Main REPL[4]:4
Stacktrace:
...
Test Summary: | Pass Fail Error Total Time
Testing if BadPerformer implements AbstractPerformerInterface | 2 1 1 4 0.2s
ERROR: Some tests did not pass: 2 passed, 1 failed, 1 errored, 0 broken.
One aspect that I like about the above is that we’re actively declaring what we think implements the interface.
In summary, if you want someone to implement a particular interface, provide a test function to check if they actually did implement that interface.