API design for dispatching on type of action

Which is prefered design of an API - dispatching on Type or dispatching on type of argument?

Consider the simple code below

abstract type Ab end
struct B <: Ab end
struct C <: Ab end

f(x::Int,::Type{B}) = "I am B and got $x"
f(x::Int,::Type{C}) = "I am C and got $x"
f_main(x_plus2::Int,T::Type{U}) where U <: Ab = f(x_plus2-2,T)

g(x::Int,::B) = "I am B and got $x"
g(x::Int,::C) = "I am C and got $x"
g_main(x_plus2::Int,t::U) where U <: Ab = g(x_plus2-2,t)

@assert f_main(8,B) == g_main(8, B())
@assert f_main(8,C) == g_main(8, C())

Which API you would consider better - f or g?

One advantage of g is that additional parameters could be added via B and C constructors (after adding fields them to struct) and hence is quite popular around.

On the other hand f requires less typing by the user of the API and sometimes you know that that one will not need to use gs functionality.

Which feels better?

I usually go for values (g) unless there is a compelling reason to use types.

Typing that extra () is usually well worth the simplicity and extensibility.

I mostly use types for methods that construct something of that type, eg see floor(::Type{T}, ...) and friends in Base.

2 Likes