I’ve recently come across how Zig handles interfaces. The gist is that Zig doesn’t have syntax sugar for interfaces and instead opts for manually managing the vtable.
I got curious and started playing with the idea, and I think it translates to julia quite well:
# An interface is simply a struct that contains a list of function pointers
writer(x) = error("$(typeof(x)) does not implement the Writer interface.")
struct Writer{T}
impl::T
writeAll::Function
writeSome::Union{Nothing,Function}
about::Function
end
# The signature of these functions is encoded in these definitions
writeAll(w::Writer, data::String) = w.writeAll(w.impl, data)
writeSome(w::Writer, data::String) = w.writeSome(w.impl, data)
about(w::Writer) = println("\nI'm a writer! of subtype $(typeof(w.impl))\n")
# we can write custom Initializers to include functions always available to an interface:
Writer(x, writeAll, writeSome = nothing) = Writer(x, writeAll, writeSome, about)
# We can define functionality that assumes a method is available
# And this is enforced by the fact that a Writer object must be
# initialized with the needed functions.
complex_functions(x, data) = complex_functions(writer(x), data)
function complex_functions(w::Writer, data)
about(w)
# w will always have a writeAll function
writeAll(w, data)
# we can also check if this particular implementation has
# defined the optional writeSome.
isnothing(w.writeSome) || writeSome(w, data)
return nothing
end
# Then we can implement this interface on different types
struct File
fd::Int
end
writeAll(f::File, data) = println("value: ", f.fd, " msg: ", data)
# we could also pass existing functions that act on File,
# not necessarily the writeAll, as long as it accept the correct inputs
writer(f::File) = Writer(f, writeAll)
# or another implementer:
struct StrangeFile
fd::Float64
end
writeAll(f::StrangeFile, data) = println("value: ", f.fd, " msg: -VERY STRANGE-", data)
writeSome(f::StrangeFile, data) = println("value: ", f.fd, " msg: -OPTIONAL METHOD-", data)
writer(f::StrangeFile) = Writer(f, writeAll, writeSome)
# or yet another, simpler one
writeAll(i::Int, data) = println("value: ", i, " msg: -I'm an Int-", data)
writer(i::Int) = Writer(i, writeAll)
# And it just works(TM) :D
f = File(1)
sf = StrangeFile(1.0)
f = File(1)
sf = StrangeFile(1.0)
complex_functions(f, "hello interfaces")
# I'm a writer! of subtype File
#
# value: 1 msg: hello interfaces
complex_functions(sf, "hello interfaces")
# I'm a writer! of subtype StrangeFile
#
# value: 1.0 msg: -VERY STRANGE-hello interfaces
# value: 1.0 msg: -OPTIONAL METHOD-hello interfaces
complex_functions(10, "hello interfaces")
# I'm a writer! of subtype Int64
#
# value: 10 msg: -I'm an Int-hello interfaces
complex_functions(1.0, "Not supported")
# ERROR: Float64 does not implement the Writer interface.
# Stacktrace:
# ...
I don’t know if this is something that is already well known, and if it is I apologise for the noise, but I thought this was an interesting design pattern, reminiscent of holy traits.
You could argue, “congratulation for figuring out polymorphism”, but I think that the valuable part is having to fill all the gaps when instantiating the interface object, which gives you the extra assurance that a method exists when dispatching on it.
Also from my limited testing, ti seems most of the machinery is compiled away.
However, I am not knowledgeable enough about the internals to figure out why this design pattern could be a bad idea, or where it could break, or what are the edgecases. So I aim to leverage Cunningham’s Law with this post and leech some knowledge .