[RFC/WIP] DuckDispatch.jl

mrufsvold
Wed, 29 May 2024 17:30:52 +0000
Package Announcements

I’ve been working through an idea for a package, and I’d love your feedback.

github.com

GitHub - mrufsvold/DuckDispatch.jl: If it quacks like a duck... dispatch on it!

If it quacks like a duck... dispatch on it!

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!

23 posts - 9 participants