I’ve been working through an idea for a package, and I’d love your feedback.
Tl;Dr: This package allows you to create method dispatches based on the methods defined for a type, not the place the type has in the type lattice.
Here’s a little snippet to get the idea:
@duck_type struct FiniteIterable{T}
# the signatures that must be defined to make a type a `FiniteIterable`
function Base.iterate(::This)::Union{Nothing, Tuple{T, <:Any}} end
function Base.iterate(::This, ::Any)::Union{Nothing, Tuple{T, <:Any}} end
function Base.length(::This)::Int end
# Calculate the paramater for `FiniteIterable` for a given type
@narrow InputT -> FiniteIterable{eltype(InputT )}
end
@duck_dispatch function my_collect(arg1::FiniteIterable{T}) where {T}
return T[x for x in arg1]
end
ch = Channel{Int}() do ch
for i in 1:2
put!(ch, i)
end
end
my_collect((1,2)) # [1,2]
my_collect([1,2]) # [1,2]
# When a type does not have the methods required, an error is thrown
# currently, this error is a generic ExceptionError, but will soon be a proper
# MethodError
my_collect(ch) # error -- no method `length`
In the background, it wraps any DuckType args in a Guise{<:DuckType, T}. When it gets to a method call listed in the DuckType’s interface, it unwraps the data.
DuckType’s can be composed together so you can build complex DuckTypes without cluttering the method table with tons of new methods. Composition looks like this:
@duck_type struct Indexible{T}
function getindex(::This, <:Any)::T end
@narrow InputT -> Indexible{eltype(InputT)}
end
@duck_type struct Container{T} <: Union{FiniteIterable{T}, Indexible{T}}
@narrow InputT -> Container{eltype(InputT )}
end
Now, a Container has behaviors for iterate, length, and getindex.
Status
All the dispatch work happens in the type domain, so it completely compiles away in my testing. Since a lot of the composition logic requires recursion, I don’t know how the inference performs in deeply nested DuckType definitions. That said, I see this kind of flexibility as most useful at the top level of a script. I don’t think it makes sense to use this inside a hot loop in general.
I will soon add a @check function along with @narrow to allow custom checking of input types beyond just “does it have these methods.” This is because a lot of types have default methods that don’t make sense. For example, iterate and eltype are defined for Int, but you probably want to exclude Int from an Iterable DuckType.
All my error messages are generic and unhelpful at the moment. One of the perks of owning the dispatching infrastructure is that I can design very nice error messages. That’s on my roadmap.
Edit: After a couple rounds of optimization, there is no overhead for many types!