Proposal: Adding Optional Static Interface/Traits Checking to Julia

Adding Optional Static Interface Checking to Julia

TL;DR

Proposing a system for optional static interface checking in Julia (similar to Python’s mypy) that would:

  • Add compile-time verification of behavioral contracts for abstract types and traits
  • Have zero runtime overhead (purely static analysis tool)
  • Improve code discoverability and IDE support
  • Make it safer to work with large codebases and extend external libraries
  • Be completely opt-in, preserving Julia’s dynamic nature

The goal is to improve Julia’s usability in large-scale projects without compromising its performance or flexibility.

Why The Lack of Behavioral Contracts Is A Big Deal

I’m relatively new to Julia and have been experimenting with its features for a couple of days. While learning the language, I’ve found that the combination of Julia’s dynamic dispatch and abstract types offers incredible flexibility. However, this same flexibility, combined with the lack of interface enforcement, creates significant challenges that I believe are holding back Julia’s adoption in larger projects.

The current situation presents several critical issues:

  1. Development Speed: Without static interface checking, developers often only discover missing method implementations at runtime. This leads to a slower development cycle, which is already challenged by Julia’s time-to-first-X issue.

  2. Code Reliability: Every unexplored execution path remains liable to errors. This makes it particularly difficult to confidently refactor or modify code in large systems.

  3. Poor Discoverability: Behavioral contracts are implicit, making it harder to understand what methods need to be implemented when extending types or using libraries. This significantly impacts readability and collaboration in larger teams.

  4. Safety in Library Extension: When extending external libraries’ types, there’s no clear way to know if you’ve implemented all necessary methods until runtime errors occur.

These aren’t just theoretical concerns - they’re practical issues that affect Julia’s adoption in production environments. While Julia’s core design is brilliant and its performance characteristics are outstanding, the lack of behavioral contracts makes it challenging to recommend for large-scale software development where type safety and interface clarity are crucial.

Having basic type safety guarantees and discoverability is something that I think no modern language can do without, especially in the context of a large codebase with many different people working on the same code. While some might argue that Julia’s dynamic nature is incompatible with such guarantees, Python’s successful adoption of mypy shows that optional static checking can provide these benefits without compromising language flexibility.

A Possible Approach

I’d like to propose an idea for adding optional static interface checking to Julia, similar to how mypy provides optional static type checking for Python. This would be purely a development and CI tool, with no runtime impact or performance overhead.

The core idea is to add some syntax for expressing behavioral contracts that could be verified by static analysis tools. Here’s what this might look like:

Abstract Type Contracts

abstract type GenericA end
abstract type GenericB end 
struct C end

abstract function foo(a::Abstract{GenericA}, b::GenericB, c::C)

struct A <: GenericA end

function foo(a::A, b::GenericB, c::C)
    # concrete implementation
end

The abstract function syntax would signal that any subtype of GenericA must implement this function. This could be verified statically without affecting runtime behavior.

Traits (Or Interfaces)

trait Friend
   function dap(x::Friend)::String end
   function talk(x::Friend)::String end
end

trait Huggable
   function hug(x::Huggable)::String end
end

function heart_warming_friend_reunion(x::T) where {T is {Friend,Huggable}}
   print(dap(x))
   print(talk(x))
   print(hug(x))
end

Again, this would be purely for static analysis and documentation, with no runtime overhead.

Behavioral Tests

We could even include interface-specific tests:

trait Iterable
   function length(x::Iterable)::Int
   function toArray(x::Iterable)::Array{Iterable}
end

Iterable.@tests begin
   function test_length(x::T) where {T is Iterable}
      @assert length(x) == length(toArray(x))
   end
end

Why This Approach?

The success of mypy in the Python ecosystem shows how valuable optional static checking can be. It provides:

  • Better tooling support and code navigation
  • Earlier error detection
  • Clearer interface documentation
  • Safer library extension

Most importantly, it does this while maintaining all the benefits of a dynamic language:

  • No runtime overhead
  • No compromise on flexibility
  • Optional usage - use it where it helps, ignore it where it doesn’t
  • Gradual adoption possible

Implementation Details

The actual syntax shown here is just a rough proposal - the Julia team would certainly have better ideas about how to make this feel more idiomatic. However, the technical implementation could be relatively straightforward:

Abstract Functions

The abstract function syntax could be resolved into standard Julia code:

function foo(a::GenericA, b::GenericB, c::C)
   throw(MethodError("Required method foo not implemented for type $(typeof(a))"))
end

This makes all abstract behaviors immediately discoverable using methodswith(GenericA) without any runtime overhead in actual implementations. The static checker would simply verify that concrete types provide their own implementations.

Traits Implementation

Traits could be implemented by translating them to Julia’s existing type system using the holy traits pattern. For example, a trait declaration like:

trait Friend
   function dap(x::Friend)::String end
   function talk(x::Friend)::String end
end

struct Dog <: Animal is Friend
   # struct implementation
end

Would be translated under the hood to:

abstract type FriendTrait end
struct hasFriendTrait <: FriendTrait end
struct hasNotFriendTrait <: FriendTrait end
FriendTrait(x::T) where {T} = hasNotFriendTrait
has_trait(x, :Friend) = FriendTrait(typeof(x)) == hasFriendTrait

struct Dog <: Animal
    # struct implementation
end

FriendTrait(x::Dog) = hasFriendTrait

The beauty of this approach is that:

  • The actual runtime would still use Julia’s normal duck typing for function dispatch
  • Traits provide static verification without imposing runtime constraints
  • The implementation maps cleanly to existing Julia patterns
  • You can optionally use the traits at runtime via the holy traits pattern if desired
  • Works seamlessly with Julia’s union types for more complex behaviors

The static analyzer would verify that types claiming to implement a trait provide all required methods, while the runtime would continue to work exactly as Julia does now. This means we get the best of both worlds: static verification for catching errors early and maintaining Julia’s dynamic flexibility at runtime.

For example, a trait declaration like:

trait Friend
   function dap(x::Friend)::String end
   function talk(x::Friend)::String end
end

Could be checked statically but wouldn’t need any runtime representation beyond normal Julia methods. This means:

  • No performance impact
  • No changes to Julia’s core dispatch system
  • Full compatibility with existing code
  • Natural integration with documentation tools

Static Analysis Integration

The static checking would work similarly to mypy:

  1. Parse the Julia AST to identify trait declarations and requirements
  2. Build a graph of type relationships and required methods
  3. Verify that all required implementations exist
  4. Provide clear error messages for missing implementations

This could be implemented as a separate tool that runs during development or CI, without requiring any changes to the Julia runtime or affecting production code performance.

Behavioral Tests

The test macros would expand to normal Julia test code, but the static analyzer would ensure that all required test implementations exist. This provides:

  • Compile-time verification of interface completeness
  • Runtime testing of actual implementations
  • No overhead in production code

The key insight is that we can achieve all these benefits through static analysis and existing Julia features, without needing to modify the language’s runtime behavior or compromise its performance characteristics.

Questions and Limitations

As someone still learning Julia, I’m sure there are aspects I haven’t considered or limitations I haven’t spotted. I would particularly appreciate feedback on:

  1. How this would interact with Julia’s multiple dispatch system
  2. Whether the proposed approach fits with Julia’s design philosophy
  3. What challenges this might present for tooling implementation
  4. Whether there are better ways to achieve similar goals

Looking for Feedback

I really appreciate the thoughtful design of Julia and the amazing work done by the language maintainers. This proposal is offered in the spirit of constructive discussion, and I’m very interested in hearing the community’s thoughts, concerns, and suggestions.

While parts of this system could potentially be prototyped using macros, I believe the real value would come from proper language integration with IDE support and tooling. A macro-based approach would be limited in several ways:

  • Inability to provide proper static analysis
  • Limited IDE integration capabilities
  • Less intuitive error messages
  • No cross-module analysis
  • Harder to maintain and evolve

Nevertheless, if anyone is interested in exploring a macro-based prototype to test some of these ideas, I’d be very interested in collaborating. This could help validate some concepts while we discuss potential language-level integration.

What aspects of this proposal make sense? What problems do you see? Are there alternative approaches that would work better within Julia’s design philosophy?

15 Likes

I really need this one. Whenever I switch to C++, I’m envious of its static interface checking. Although I don’t have the background of knowledge to contribute directly to the project, I can donate.

3 Likes

see also

both are in the experimentation phase, but you are not alone in wanting something like this.

1 Like

Some kind of official interface might be coming soon, according to a recent talk by Jeff. I can’t find the Interfaces draft proposal document that he mentioned in this talk, so it might be non-public for now.

4 Likes

Possibly this one? A roadmap for interfaces - HackMD

7 Likes

That seems like it, thanks!

  • While you admit that the syntax is rough, that’s what makes it unclear how this can be implemented. Without more focus or details, you can’t address the possible pitfalls. Julia’s multimethods are susceptible to method ambiguity, for example.
  • You need to consider method implementation more. For example, the “Traits Implementation” translation omits the methods that would be written. Holy traits involve adding positional arguments so that is important to address.
  • There is a tendency toward attaching interfaces or traits to one typed argument at a time e.g. Dog is Friend. That is a single-dispatch pattern. A Holy trait can be derived from several typed arguments, see the Holy trait-based SimpleTraits.jl for an example. Julia’s informal interfaces may also be polymorphic over multiple typed arguments; consider a Predator-Prey interface where different species types can be one or the other, resulting in mirrored implementations like eat(::Shark, ::Human) and eat(::Human, ::Shark). Note that it’s just as possible we could only implement single-dispatch formally, and the rest has to be done informally.
  • One reason interfaces are informal is many interface methods aren’t involved in functionality. Sometimes it’s just optional; the useful caller methods only need a fraction of the interface methods. Sometimes it’s impossible; immutable AbstractArrays can’t implement setindex!. Limiting formal interfaces and traits to only mandatory methods leaves MANY useful methods in the informal side, and splitting traits or types to organize all the methods formally is a deep problem. How this is statically checked or discovered should also be addressed.

Like others have said, formalizing this has been a big want, and the more people working this out, the better.

1 Like

Thanks for addressing some weak points in my proposal. The original post is already quite long, and in an attempt to shorten it I have given many things for granted. I will respond point by point, but by reading your response I think there is a major misunderstanding of my proposal: my traits don’t change dispatch behavior, just like python type hints have NO effect on runtime

The ambiguity concern isn’t relevant here because traits are purely for static verification, not dispatch. They have zero impact on Julia’s method ambiguity because they don’t affect runtime behavior at all. The syntax is intentionally rough as this is meant to spark discussion, not provide a complete spec, although I admit until no concrete spec is given, these discussion don’t amount to much.

The translation to holy traits was only included as an optional implementation detail for runtime type checking. The core proposal is about static verification - the methods wouldn’t need to be written because traits aren’t used for dispatch. I included the holy traits example only to show how runtime checking could work if desired, but it’s not core to the proposal. But if needed, some syntax sugar could be provided by specifying traits with a single colon. For example

def eat(predator: Predator, prey: Prey, other_value:: Integer)
# implementation
end

would be translated to:

def eat(predator:: T, prey:: S) where {T,S} = eat(predator, PredatorTrait(T), prey, PreyTrait(S))

def eat(predator:: T, ::HasPredatorTrait, prey::S, ::HasPreyTrait)
# method implementation
end

Even though there would be absolutely no advantage to this as far as I can see, since you can just rely on duck typing (which also makes multiple dispatch faster anyway!)

This criticism stems from thinking about traits as dispatch mechanisms. The proposal already handles multiple behaviors clearly:

Since traits are just static promises of behavior, there’s no dispatch ambiguity to resolve.

As a mathematician, this argument deeply concerns me. It’s equivalent to saying “this theorem requires an abelian group, but let’s just state it for any group and let users figure out they need commutativity when the proof fails.” This approach leads to exactly the kind of confusion and bugs we’re trying to prevent.

Instead, we should use trait composition to handle optional behaviors:

trait Container{T}
    get(x::Container{T}, i::Integer)::T 
end

trait ContainerIsMutable{T}
    set!(x::ContainerIsMutable{T}, i::Integer, el::T)  # Optional extension
end

trait MutableContainer{T} = Container{T} & ContainerIsMutable{T}

This makes requirements explicit and verifiable rather than implicit and error-prone. Optional methods in traits defeat the entire purpose of having behavioral contracts - they just recreate the current problems with slightly better documentation.

1 Like

I wouldn’t be too sure that method ambiguity isn’t a problem just because you’re only aiming for static verification. Other AOT-compiled languages let you define traits and interfaces that conflict, and the compiler will stop you if you don’t disambiguate properly. In our case, the JIT compiler interactively kicks in per call, so something more or else may have to do the work earlier.

Big problem there is Holy traits are typically used for compile-time dispatch, here’s an old book excerpt with an example. Minor things, you would make one trait, not two, and it’s function, not def.

Which is what I was referring to by “splitting traits or types”. Problem is, how confident are you that you can split an informal interface into a reasonable hierarchy of subsets and unions? When you start naming a 20th trait like MutableSliceableReversableHashedContainer, you run into a practical problem if not a mathematical one. As for a simpler mathematical one, you didn’t distinguish required from secondary (optional or impossible). We’re able to implement ContainerIsMutable alone, a set! with no get. What happens if we implement the 2 traits but not the combined one?

isn’t the point of traits that this would be 5 separate traits? and a type can implement any all or none

although trait != interface

1 Like

That would indeed be a move in the right direction, more typical in other languages too.

No offense, but it seems like you’re focusing too much on the finger pointing to the moon, especially by pointing out stuff like:

I don’t understand your critique, I think I am not straying at all from the excerpt except for minor syntactical differences that are irrelevant to the core discussion:

# From the excerpt
abstract type LiquidityStyle end
struct IsLiquid <: LiquidityStyle end
struct IsIlliquid <: LiquidityStyle end
LiquidityStyle(::Type{<:Cash}) = IsLiquid()

#mine
abstract type FriendTrait end
struct hasFriendTrait <: FriendTrait end
struct hasNotFriendTrait <: FriendTrait end
FriendTrait(x::T) where {T} = hasNotFriendTrait

The point is that multiple dispatch is expensive anyway! Duck typing is generally faster in Julia. While the holy trait pattern is elegant, there’s no real performance advantage versus duck typing. It’s faster than runtime boolean traits, but that’s entirely orthogonal to my proposal which is about static verification, not runtime behavior!

If someone designs a poor abstraction hierarchy, that’s a design problem, not a language problem. No programming language can prevent poor abstractions - they can only provide tools for expressing good ones clearly. The solution isn’t to avoid formal interfaces; it’s to design better abstractions, just as mathematics has done successfully for centuries.

In mathematics, we don’t create poor abstractions like VSetOverFieldWithSumOperationAndDotProductInterface - we create meaningful abstractions like “Hilbert space” that capture exactly the properties we need. The ability to compose traits doesn’t mean we should create unwieldy combinations - it means we should think carefully about our abstractions, just like mathematicians do.

Yes, we could call them interfaces if preferred, but I think you’re raising a crucial point about behavioral complexity. Even a simple array requires a dozen of functions to be described as such. I suppose you can fallback to the type system, but then you’re back to square one in a sense.

Interface composition is good enough, and a good static checker could actually help here - it could analyze the code and suggest simplifying interfaces where possible, though this would need to be done judiciously

Coming from Python’s experience with type hints and interfaces, I think we’re letting perfect be the enemy of good here. Are interfaces a perfect solution to behavioral contracts? No. But they’re a proven system that would add zero runtime overhead and wouldn’t restrict Julia’s multiple dispatch in any way. We’re currently choosing to have no compile-time verification tools at all because we can’t design a “perfect” system that handles every edge case. Python made the pragmatic choice to add type hints and interfaces despite similar concerns, and the result has been dramatically improved code quality and developer experience. Sometimes good enough is, well, good enough.

This statement and distinction are strange. What do you think multiple dispatch and duck typing are in Julia, exactly?

I agree, which is why more thought has to be put into the design. We’ve never had proofs by handwaving, and language design gets pretty formal.

1 Like

@Solmantos it may be useful information to you that interfaces, traits, and the desire for each have been discussed a lot throughout history on this forum and across various github issues

that’s not to say that you have done anything wrong by bringing it up again; I only mean to give you context as to why you may be getting the sense that the general response is “give code with details or stop talking,” because I think there is broad agreement that these are useful language features in the abstract, but what prevents them from being added into Julia at this point IS the collection of details and implementation challenges that have not been decided upon or solved yet.

FWIW, there are packages that do provide tools like what you’re talking about — although I’m not sure how maintained all of them are — but you may be interested to look at packages like WhereTraits.jl and Supposition.jl (as well as the ones I sent in my first comment earlier). There is just no agreement as to which of these approaches if any should become standardized and none have seen wide community adoption yet.

6 Likes

Well, I’ve been reading that is generally bad to do stuff like this if you can avoid it:

module MultipleDispatch
function foo(x::Int32)::Int32
    return x + 2
end

function foo(x::Int64)::Int64
    return x + 2
end

function foo(x::Float64)::Float64
    return x + 2
end
end

module DuckTyping
function foo(x)
    return x + 2
end

and I would generally agree. Performance wise I believed what I read, but I haven’t bothered checking it out. I don’t know how Julia works under the hood.

Well, the problem there is redundancy, specifically the unnecessary definition of several methods with the exact same body. Multiple dispatch isn’t the problem; in fact, the DuckTyping module is doing multiple dispatch too. There isn’t a sign of performance loss in either module either. I think you’re failing to see the relevance of some of our comments because you still don’t have a firm grasp of Julia on the language level, not just its implementation.

1 Like

The fact that these discussions date back to 2014 (as far as I could find) is precisely what concerns me. As I emphasized in my original post’s section “Why The Lack of Behavioral Contracts Is A Big Deal”, the absence of clear behavioral contracts in a programming language isn’t just an inconvenience - it’s a fundamental design flaw.

Suggesting that people should rely on macro-based solutions for what is fundamentally a language design issue is problematic, especially in a community primarily focused on academia and research. While these packages are creative solutions, they’re band-aids that can’t provide the comprehensive tooling and IDE support that proper language integration would enable.

Poor developer experience kills languages in industry. Julia’s limited industry adoption despite its excellent technical merits is evidence of this. The bar isn’t “better than MATLAB” anymore - we’re competing with languages that have excellent tooling, clear interfaces, and strong static analysis support.

No professional development team is going to seriously invest in a language where fundamental issues remain unresolved for over a decade. My proposal doesn’t need to be a complete language specification to be valuable for discussion. I’m demonstrating a possible solution - one that’s proven successful in Python with mypy. The pushback I’m receiving seems focused on implementation details rather than fundamental flaws, which raises the question: if there aren’t major conceptual obstacles, why hasn’t this been solved?

I’m not alone in these concerns - numerous well-known blog posts about Julia raise similar issues. At some point, we need to acknowledge that perfect is becoming the enemy of good, and that waiting for an ideal solution is worse than implementing a practical one.

1 Like

I would suggest to stop looking at fingers because you have consistently missed the point in most of the conversation. Here you go, happy now?

module MultipleDispatch
function foo(x::Int32)::Int32
    return x + 2
end

function foo(x::Int64)::Int64
    return x + 3
end

function foo(x::Float64)::Float64
    return x + 4
end
end

module DuckTyping
function foo(x)
    return x + 2
end

You’re confusing design with implementation, the latter of which was not discussed at all. Both would be necessary for a working feature, but there were already holes and mistakes in your design to address first.

You haven’t demonstrated anything as rigorous as Mypy for Julia here. Also worth pointing out that Mypy started development in 2012, 18 years after Python v1 released. Python’s support for type hints came in 2014, a full 2 decades after. People didn’t exactly wait for this to start serious investment.

I am enjoying an album at the moment, but my mood is irrelevant to the conversation, I don’t know why you’d ask. In turn, I’d suggest you learn more about Julia and dispel your misconceptions about multiple dispatch and duck-typing, which are heavily influencing your proposal. I’m sure a mathematician like yourself would agree that sufficient foundational knowledge is needed for meaningful contributions.

2 Likes

There is nothing wrong with this module. If you, the writer, wants to have different behavior of foo depending on the type of input, then this exactly the way to do it.

There is no performance hit in this case because the compiler will compile the correct path before running it on the input. “Duck typing” does not lead to a run-time performance loss in Julia.