What is the difference between traits in Rust and typeclasses in Haskell?
Asked Answered
A

3

211

Traits in Rust seem at least superficially similar to typeclasses in Haskell, however I've seen people write that there are some differences between them. I was wondering exactly what these differences are.

Araldo answered 24/1, 2015 at 7:50 Comment(7)
I don't know much about Rust. But common stumbling blocks for similar technologies in other languages are higher kinds (e.g. can traits range over parameterized types, but not their parameters?) and return-type polymorphism (e.g. can a trait type appear in the result of a function, but not anywhere in the arguments?). An example of the former in Haskell is class Functor f where fmap :: (a -> b) -> (f a -> f b); an example of the latter is class Bounded a where maxBound :: a.Gurolinick
GHC also supports multi-parameter type classes (that is, traits involving several types) and functional dependencies, though this is not a part of the official Haskell specification. Judging from the Rust syntax suggested at your link, it can only support traits ranging over one type at a time, though that judgment is again not based on deep experience.Gurolinick
@DanielWagner Return-type polymorphism exists (e.g. std::default), and multiparameter traits sort-of work (including an analogue of functional dependencies), though AFAIK one needs to work around the first parameter being privileged. No HKT however. They're on the far-future wishlist but not on the horizon yet.Shagreen
another difference is the treatment of orphan instances. Rust tries to have stricter coherence rules on where a new impl for a trait can be written. See this discussion for more detail (in particular here )Pinprick
Rust supports associated types and equality constraints now, though they aren't as powerful as Haskell's type families. It also has existential types via trait objects.Indicative
@DanielWagner: I think parameterized traits qualify as something similar to multi-parameter type classes. Self is just another “input parameter” that gets a bit of special treatment and is implicit.Mattins
I don't know enough about Haskell type classes to compare, but here's the Rust reference on traits doc.rust-lang.org/reference.html#traits (it's currently out of date) and the accepted + partially implemented RFC to add associated items to traits github.com/rust-lang/rfcs/blob/master/text/…Atlantean
A
83

At the basic level, there's not much difference, but they're still there.

Haskell describes functions or values defined in a typeclass as 'methods', just as traits describe OOP methods in the objects they enclose. However, Haskell deals with these differently, treating them as individual values rather than pinning them to an object as OOP would lead one to do. This is about the most obvious surface-level difference there is.

The one thing that Rust could not do for a while was higher-order typed traits, such as the infamous Functor and Monad typeclasses.

This means that Rust traits could only describe what's often called a 'concrete type', in other words, one without a generic argument. Haskell from the start could make higher-order typeclasses which use types similar to how higher-order functions use other functions: using one to describe another. For a period of time this was not possible in Rust, but since associated items have been implemented, such traits have become commonplace and idiomatic.

So if we ignore extensions, they are not exactly the same, but each can approximate what the other can do.

It is also mentionable, as said in the comments, that GHC (Haskell's principal compiler) supports further options for typeclasses, including multi-parameter (i.e. many types involved) typeclasses, and functional dependencies, a lovely option that allows for type-level computations, and leads on to type families. To my knowledge, Rust has neither funDeps or type families, though it may in the future.†

All in all, traits and typeclasses have fundamental differences, which due to the way they interact, make them act and seem quite similar in the end.


† A nice article on Haskell's typeclasses (including higher-typed ones) can be found here, and the Rust by Example chapter on traits may be found here

Azotobacter answered 18/9, 2015 at 19:50 Comment(5)
Rust still does not have any form of higher kinded types. "Infamous" needs justification. Functor are incredibly pervasive and useful as a concept. Type families are the same as associated types. Functional dependencies are essentially redundant with associated types (including in Haskell). The thing Rust lacks wrt. fundeps is injectivity annotations. You have it backwards, Rust's traits and Haskell's type classes are different on the surface but many differences evaporate when you look beneath. Differences that remain are mostly inherent to the different domains the languages operate in.Sagittarius
Associated items are now considered idomatic in many circumstances, right?Spindle
@Spindle You're right—This answer should be updated a bit. Editing now.Azotobacter
This answer is still mistaken about higher kinded types, what you've called higher order traits; we still can't make Functor or Monad traits in rust, it's not just a historic limitation.Autocorrelation
@ZoeyHewll of course we can make a Functor and Monad. Rust supports higher kinds through generic associated types. github.com/mtomassoli/HKTsChoirboy
I
32

I think the current answers overlook the most fundamental differences between Rust traits and Haskell type classes. These differences have to do with the way traits are related to object oriented language constructs. For information about this, see the Rust book.

  1. A trait declaration creates a trait type. This means that you can declare variables of such a type (or rather, references of the type). You can also use trait types as parameters on function, struct fields and type parameter instantiations.

    A trait reference variable can at runtime contain objects of different types, as long as the runtime type of the referenced object implements the trait.

    // The shape variable might contain a Square or a Circle, 
    // we don't know until runtime
    let shape: &Shape = get_unknown_shape();
    
    // Might contain different kinds of shapes at the same time
    let shapes: Vec<&Shape> = get_shapes();
    

    This is not how type classes work. Type classes create no types, so you can't declare variables with the class name. Type classes act as bounds on type parameters, but the type parameters must be instantiated with a concrete type, not the type class itself.

    You can not have a list of different things of different types which implement the same type class. (Instead, existential types are used in Haskell to express a similar thing.) Note 1

  2. Trait methods can be dynamically dispatched. This is strongly related to the things that are described in the section above.

    Dynamic dispatch means that the runtime type of the object a reference points is used to determine which method that is called though the reference.

    let shape: &Shape = get_unknown_shape();
    
    // This calls a method, which might be Square.area or
    // Circle.area depending on the runtime type of shape
    print!("Area: {}", shape.area());
    

    Again, existential types are used for this in Haskell.

In Conclusion

It seems to me like traits are in many aspects the same concept as type classes. It addition, they have the functionality of object oriented interfaces.

On the other hand Haskell's type classes are more advanced. Haskell has for example higher-kinded types and extension like multi-parameter type classes.


Note 1: Recent versions of Rust have an update to differentiate the usage of trait names as types and the usage of trait names as bounds. In a trait type the name is prefixed by the dyn keyword. See for example this answer for more information.

Iotacism answered 8/8, 2017 at 19:2 Comment(2)
"Type classes create no types" -- I think it is best to understand dyn Trait as a form of existential typing as they relate to traits/type-classes. We can consider dyn an operator on bounds projecting them to types, i.e. dyn : List Bound -> Type. Taking this idea to Haskell, and with regards to "so you can't declare variables with the class name.", we can do this indirectly in Haskell: data Dyn (c :: * -> Constraint) = forall (t :: Type). c t => D t. Having defined this, we may work with [D True, D "abc", D 42] :: [D Show].Sagittarius
@Iotacism so traits are type classes that just also happen to implicitly define an existential type? That's not a big difference.Complaint
Y
10

Rust's “traits” are analogous to Haskell's type classes.

The main difference with Haskell is that traits only intervene for expressions with dot notation, i.e. of the form a.foo(b).

Haskell type classes extend to higher-order types. Rust traits only don't support higher order types because they are missing from the whole language, i.e. it's not a philosophical difference between traits and type classes

Yarborough answered 1/4, 2015 at 10:35 Comment(1)
Traits in Rust do not "only intervene for expressions with dot notation". For example, consider the Default trait which has no methods, only non-method associated functions.Sagittarius

© 2022 - 2024 — McMap. All rights reserved.