I want to write a piece of software that my users can extend by writing “plugins” for it. Specifically, they would provide their own types in their plugins, that the main program will then operate on. For the main program to be able to do that the provided types need to adhere to a well defined interface. I think that’s a setting many of us have encountered before.
In a language like, say, Python an abstract base class that people can inherit from would probably be the most common way to go about that. Now, in Julia, in order to at least document what that interface is I’m tempted to write
where Unit is the base type the plugin types extend, the two functions are the ones that need to be defined on each plugin type and NotImplementedError I’d defined myself in analogy to Python. Unfortunately, the expected return type can only be given as a comment AFAIK. Anyway, I find the above preferable over
function computeOutputsAfter end
function setInputs! end
– which I’ve seen mentioned in the documentation – because there I don’t even see the required number of arguments, let alone their type.
My question is: does that look like a reasonable approach or are there more common/idiomatic ways to do this in Julia.
I would say don’t define the methods for the base (“abstract”) type. That way Julia will throw an error when a given plugin does not satisfy the required interface.
Make sure to document what interface is expected though.
The thing that has bitten me over and over with this is that when you define the method generically with a NotImplementedError, you can end up confusing dispatch if you want to, say, overload the method with something more specific for unit but more general for intputs. Then Julia will say that it doesn’t know which method to use.
So I’ve just learned the hard way to just do
function computeOutputsAfter end
with a comment for the type signature.
Julia has such a great type system that you want to use it as much as possible. But really, the type system is not a module system or a correctness system; it’s a dispatch and performance system. Having those methods defined to throw NotImplementedError doesn’t help with dispatch and doesn’t help with performance, and so isn’t really an idiomatic usage of the type system.
I find the following blog post to be the most relevant and interesting resource for issues related to the need for interfaces in real-world applications:
At its core, a MethodErroronly says “there is no method matching these arguments”; it doesn’t communicate whether there should be such a method or not. This is why I disagree with the idea of just leaving the base function completely unimplemented. You don’t want to end up in a situation where a user implements your interface wrongly and tries to “fix” that by chasing MethodErrors.
I’m the maintainer of RequiredInterfaces.jl, which does exactly that. I use this package for pretty much all of my projects, where I want to provide a user-extensible API.
See also this github issue for more discussion about NotImplementedError: