Trait Dispatch
TypeContracts supports the Holy Trait pattern: dispatch on whether a type satisfies an interface contract, without requiring the type to declare anything upfront.
For juliac --trim compatibility of interface_trait and the proactive/reactive trim tools, see Trim Compatibility.
interface_trait(I, T) — the dispatch key
interface_trait(::Type{I}, ::Type{T}) -> Implemented{I} | NotImplemented{I}Returns Implemented{I}() if T satisfies all mandatory methods of the contract registered for I, or NotImplemented{I}() otherwise. Only method existence (hasmethod) is checked — return-type inference is not run, making this safe for runtime use including in juliac --trim compiled binaries.
abstract type AbstractShape end
@contract AbstractShape begin
area(::Self) :: Float64
perimeter(::Self) :: Float64
end
struct Circle <: AbstractShape; r::Float64 end
area(c::Circle)::Float64 = π * c.r^2
perimeter(c::Circle)::Float64 = 2π * c.r
interface_trait(AbstractShape, Circle) # Implemented{AbstractShape}()
interface_trait(AbstractShape, Int) # NotImplemented{AbstractShape}()verified_trait(I, T) — return-type-checked dispatch
interface_trait deliberately checks method existence only — return types are never inspected, because Base.return_types cannot safely run inside a @generated function's generator (Julia forbids reflection there; it would mean recursive type inference). verified_trait closes that gap a different way: instead of checking anything itself, it reflects whatever @verify/@verify_all/@delegate already verified — including return types — at the moment verification succeeded.
verified_trait(::Type{I}, ::Type{T}) -> Implemented{I} | NotImplemented{I}@verify T runs check_contract(T) (method existence and declared return types, via Julia's type inferencer) and, on success, seals in a concrete verified_trait(::Type{I}, ::Type{T}) = Implemented{I}() method for every interface T's supertype chain registers. A type that was never @verify'd — even one that would satisfy the contract structurally — gets NotImplemented{I}() from the generic fallback.
You write @verify; you never write verified_trait. @verify T is the only thing you add to your code — it plays the same role @contract plays for interface_trait: a one-time declaration that makes a method exist. verified_trait itself is a read, called at a dispatch site exactly like interface_trait is — never something you define. The two are not parallel steps you maintain; @verify is the write, verified_trait is the query against what it wrote:
struct Square <: AbstractShape; side::Float64 end
area(s::Square)::Float64 = s.side^2
perimeter(s::Square)::Float64 = 4 * s.side
interface_trait(AbstractShape, Square) # Implemented{AbstractShape}() — methods exist
verified_trait(AbstractShape, Square) # NotImplemented{AbstractShape}() — never @verify'd
@verify Square # ← the only line you add
verified_trait(AbstractShape, Square) # Implemented{AbstractShape}() — now reads ImplementedUsed in the same two-method dispatch pattern as interface_trait above — @verify is the setup, verified_trait is the same kind of call interface_trait already was:
_render(::Implemented{AbstractShape}, x) = "shape: area=$(round(area(x); digits=2))"
_render(::NotImplemented{AbstractShape}, x) = "not a verified shape: $(typeof(x))"
render(x) = _render(verified_trait(AbstractShape, typeof(x)), x)
@verify Square # run once, wherever Square is defined — not per call site
render(Square(3.0)) # "shape: area=9.0"This is a nominal, opt-in guarantee — the same shape as Rust's impl Trait for T or Go's var _ I = T{} assertion — not a structural one. Skipping @verify means verified_trait reads NotImplemented even for a type that's actually fine; TC cannot force every implementer through a check the way a compiler's coherence rules can.
Sealing is a plain method definition emitted at verification time (module load / precompile), strictly more specific than the generic fallback, so dispatch resolves it statically — same zero-allocation, juliac --trim-safe shape as interface_trait, with no new runtime cost.
When to use which:
interface_trait— "does a method with this signature exist," always available, no setup required.verified_trait— "has this exact(interface, type)pair been fully verified, including return types" — requires@verifyonce per type, then it's called at dispatch sites exactly likeinterface_traitis.
Revise caveat. Redefining an implementation method after @verify leaves the sealed verified_trait method in place until T is re-verified — the Revise integration warns on the resulting contract violation but does not automatically unseal it. In an interactive session under active edits, prefer interface_trait or re-run @verify after changes.
Implemented{I} and NotImplemented{I} {#Implemented{I}-and-NotImplemented{I}}
Plain singleton structs with no fields, exported by TypeContracts:
struct Implemented{I} end
struct NotImplemented{I} endUse them as method argument types, type parameters, or anywhere a type is expected. Julia specializes dispatch on them statically when the implementing type is known at compile time, so the overhead is zero.
Basic dispatch pattern
Define two internal methods — one per trait — and a public entry point:
_render(::Implemented{AbstractShape}, x) = "shape: area=$(round(area(x); digits=2))"
_render(::NotImplemented{AbstractShape}, x) = "not a shape: $(typeof(x))"
render(x) = _render(interface_trait(AbstractShape, typeof(x)), x)render(Circle(3.0)) # "shape: area=28.27"
render(42) # "not a shape: Int64"
render("hello") # "not a shape: String"When the concrete type of x is known at the call site Julia resolves the dispatch statically — no dynamic dispatch overhead.
Graceful fallback instead of error
Not every NotImplemented case needs to error. Return a sentinel, log, or silently skip:
abstract type Summarizable end
@contract Summarizable begin
summary(::Self) :: String
end
_summarize(::Implemented{Summarizable}, x) = summary(x)
_summarize(::NotImplemented{Summarizable}, x) = "(no summary available)"
summarize(x) = _summarize(interface_trait(Summarizable, typeof(x)), x)struct Report; title::String end
summary(r::Report)::String = "Report: $(r.title)"
@verify Report
summarize(Report("Q1")) # "Report: Q1"
summarize(42) # "(no summary available)"Progressive enhancement with multiple interfaces
Types can implement as many interfaces as they like, independently. Each interface produces its own trait. Compose them to express which combinations of capabilities are required:
abstract type Printable end
abstract type Persistable end
@contract Printable begin
Base.show(::IO, ::Self)
end
@contract Persistable begin
serialize(::Self) :: Vector{UInt8}
deserialize(::Type{Self}, ::Vector{UInt8}) :: Self
end
# Dispatch on all combinations
function store_and_display(x)
pt = interface_trait(Printable, typeof(x))
st = interface_trait(Persistable, typeof(x))
_store_and_display(pt, st, x)
end
_store_and_display(::Implemented{Printable}, ::Implemented{Persistable}, x) =
println("storing: ", x) # show via Base.show; serialize for storage
_store_and_display(::Implemented{Printable}, ::NotImplemented{Persistable}, x) =
println("display only: ", x) # can show but not persist
_store_and_display(::NotImplemented{Printable}, ::Any, x) =
error("$(typeof(x)) must be Printable")Only method combinations that make sense need definitions — Julia's ordinary method dispatch handles the fallthrough for any combination you don't define.
Library extension point
Trait dispatch lets library code work with user-defined types without inheritance. The library defines the interface and the dispatch; users add their own types later:
# --- In a library -----------------------------------------------------------
module ShapeLib
using TypeContracts
abstract type AbstractShape end
function area end
function color end
@contract AbstractShape begin
area(::Self) :: Float64
:optional
color(::Self) :: Symbol
end
_area_str(::Implemented{AbstractShape}, x) = string(round(area(x); digits=3))
_area_str(::NotImplemented{AbstractShape}, x) = "N/A"
_color_str(::Implemented{AbstractShape}, x) =
hasproperty(x, :_has_color) ? string(color(x)) : "default"
_color_str(::NotImplemented{AbstractShape}, x) = "unknown"
function describe_shape(x)
t = interface_trait(AbstractShape, typeof(x))
"$(typeof(x)): area=$(_area_str(t, x))"
end
export AbstractShape, area, color, describe_shape
end # module
# --- In user code -----------------------------------------------------------
using ShapeLib
struct Hexagon <: AbstractShape; side::Float64 end
area(h::Hexagon)::Float64 = 3√3/2 * h.side^2
color(h::Hexagon)::Symbol = :blue
@verify Hexagon
describe_shape(Hexagon(2.0)) # "Hexagon: area=10.392"The library never needs to know about Hexagon. New shapes added by users automatically get the correct dispatch behavior.
Dispatch on parametric types
interface_trait works with parametric contracts. The type parameter resolves at specialization time:
abstract type AbstractContainer{T} end
function cget end
function cset! end
@contract AbstractContainer{T} begin
cget(::Self, ::Int) :: T
cset!(::Self, ::T, ::Int)
end
struct Stack{T} <: AbstractContainer{T}
data::Vector{T}
end
cget(s::Stack{T}, i::Int) where T = s.data[i]
cset!(s::Stack{T}, v::T, i::Int) where T = (s.data[i] = v)
@verify Stack{Float64}
interface_trait(AbstractContainer{Float64}, Stack{Float64}) # Implemented{...}()
interface_trait(AbstractContainer{Float64}, Stack{Int}) # NotImplemented{...}()Checking the trait at the type level
interface_trait takes types, not values. When you have a value, pass typeof:
x = Circle(1.0)
interface_trait(AbstractShape, typeof(x)) # Implemented{AbstractShape}()
# Or directly with a concrete type:
interface_trait(AbstractShape, Circle) # Implemented{AbstractShape}()To branch at the value level without a helper method:
function maybe_render(x)
if interface_trait(AbstractShape, typeof(x)) isa Implemented
return area(x)
else
return nothing
end
endThe isa Implemented check is resolved at compile time when x has a known concrete type, so this branch is as efficient as the two-method pattern.