Why use subtypes instead of traits and duck typing?

So perhaps the way to think about this is that an abstract supertype is the “most essential trait” of a type? It is the trait that we think is the most important part of the type, so we make it easy to dispatch on the type by this trait and much less convenient for others.

8 Likes

The concept of a single “most essential trait” seems bad. An object will have multiple equally essential traits. Which trait is relevant at any moment depends on which part of the program you’re in.

That could be implemented as traits or multiple inheritance.

2 Likes

I agree. I was trying to explain what the subtyping means (beyond merely what it does), so while it may have flaws as the type system I think it is an accurate interpretation of the type system (based on what has been said so far). For complex types that have no essential traits, it would appear to make more sense to not give it any supertypes and use traits instead.

2 Likes

So, maybe Julia’s type system not being able to express everything is a feature and not a bug.

That’s the real interesting part of the issue. It’s easy to come up with paper-exercises about how a type system could express this or that. It’s much harder to figure out how that will actually impact code once you get down to writing it. After all, much of the exercise of writing code is to figure out what you want to express, not simply to express a fully baked idea. And there are important aspects of code writing that’s harder to put into words: It’s easy to show how one design is able to correctly express a concept, it’s harder to show if implementations of that design becomes unwieldly, slow to write, hard to learn, etc. After all, if people only cared about correctness, people would not use Julia in the first place, but use Ada or Rust.

I’ve found Julia’s abstract types surprisingly useless, for several reasons.

  • First, it’s hard to know if what you’re writing applies exactly to the type you’re thinking of (restricting to AbstractArray and accidentally excluding Tuples are a classic).
  • Second, it’s hard to know if you are missing any intermediate abstract types, and you can’t add more abstract types in the middle of the tree except by refactoring the whole code.
  • Third, the large majority of actual abstract types in Julia are undocumented - or so vaguely documented it doesn’t actually present an interface. For example, what does AbstractChar promise? Since the literal only function of abstract types are to specify a behaviour interface, that’s a pretty big problem.
  • Let’s not even get into the nightmare of having to wrap types in order to change its abstract type.

There’s probably more. On the other hand, I’m not sure purely relying on a trait-based system would work for Julia. There’s the whole dispatch and specialization system - the major selling point of Julia - how would that work?

Furthermore, we don’t have good static analysis. With a purely trait-based system you’ll quickly end up with functions that directly or indirectly depends on tens of traits. How can you make sure you’ve not missed the implementation of a trait? At least with subtyping, it’s pretty easy to say “this is an AbstractSet{UInt8} - so I know pretty much what it promises”. Even if we could get static analysis, would we really want a system that is hard to use without analysis? Or would that make Julia less expressive and easy to write?

9 Likes

Thinking about it again, the way I see trait in Julia (inspired by Holy traits of course) they have nothing to do with a type system. They are just functions on types that are (in principle) fully inferrable and can be compiled out by the compiler.

If anything they seem to rather be a low overhead extension of duck typing.

How can you make sure you’ve not missed the implementation of a trait?

Agree on the convention that traits should not have general fallback methods so that it raise a method error in that case, just like it would with duck typing? Silent failure with trait system is definitively something we need to be very careful about.

3 Likes

I think there’s a key fact missing here. Traits can express more specific things only by adding more functions. You can’t override a function defined for a more general trait when it deals with a more specific trait. You need subtyping for that.

(With that said, Holy traits have the nice property that they can express negation, unlike typeclasses in Hindley Milner type systems where the typechecker is prolog-like and can’t express negation. So you can still define a new function with a specific method to the trait and a fallback that uses the method from the more general trait. But existing code written for the general trait can’t make use of your specialization)

3 Likes

For me the answer is definitely yes. We have powerful analytical tools, starting with JET.jl, and likely more in the future. I can do a lot more with tools than I can without them, and I want to take advantage of that, not restrict myself to the least common denominator.

1 Like

Just wanted to add that there’s a really promising trait package in https://github.com/andyferris/Traitor.jl

Needs some work though to get it working for 1.x. @Mason already started on that path.

If it works well, perhaps it can be included in base some day

“useless” ? They are used heavily throughout core of Julia, and the ecosystem. People have made a lot of use of them . Perhaps you mean that they are not as expressive as you’d like?

2 Likes

Traits can express more specific things only by adding more functions

This is not entirely true, you can use abstract types as trait return values too, best of both worlds :wink:

4 Likes

Thinking about my last comment… it does seem to mean there essentially no difference between a concrete type inheriting from and abstract type, and traits. Subtyping inheritance for abstract types is just a nice general way of defining a tree structure to specify traits.

Then subtyping inheritance for a concrete type is like an unnamed, default trait, with syntactic sugar.

But you can have multiple subtyping trees of arbitrary specificity for dispatch:

somfunction(object::AbstractType, trait1::Val{<:AbstractTrait1}, trait2::Val{<:AbstractTrait2}) = ...

And the first is not special.

Sure, but then you are back to relying on subtyping (combined with first class types). You could technically have a language where concrete types cannot inherit from abstract types of course, but you would still need trait subtyping to do specialization.

But youre not really back to regular subtyping, because you can define new subtyping traits after the object is defined, like any other trait.

1 Like