There are LOTS of things you can do with type classes, and they are super sexy. Let me try to enumerate them.
One commenter already noted higher-kinded type classes, which is the basis of Haskell's libraries and do-notation for monads. That's HUGE.
But besides higher-kinded type classes, there are others:
* Factories and constants in your interfaces. Type classes allow you to put a type variable in the return argument of a method. This lets you put factories and constants in your type classes. For example, if you look at the monoid type class:
class Monoid a where
mappend: a -> a -> a
mzero: a
And I can implement this
instance Monoid String where
mappend x y = AppendStrings x y
mzero = ""
instance Monoid Integer where
mappend x y = x + y
mzero = 0
So now I can write generic code where I can get the "zero constant" of a type without knowing what type that is. I could just have a genericMonoid : T where T is a type variable, and all I know about it is that it belongs to class Monoid (maybe it's an integer, maybe it's a string, maybe it's something else). I can now get the zero value of whatever that type is. In fact, my method doesn't have to be dispatched on the type of some input argument. I can dispatch on the return value. Something like
multiplyByZero : [Monoid T] int -> T
multiplyByZero 0 = mzero
multiplyByZero n = mplus mzero (multiplyByZero (n-1))
(Forgive my crappy Haskell pseudo syntax, I haven't written it in a while). Not that multiply by zero is useful.
* Operators of greater arity than 1. So I hinted at this above. With "method syntax" arity is always 1. I have one "this". I can operate on it. For all other arguments to my method, I cannot actually constrain them to have the same type as "this". At best, with OOP, my method belongs to some base class, and I can insist that the non-this arguments are of base class type, but I cannot constrain them to have "this" type. This is illustrated in the monoid example above, or in the Eq example I gave, where it can be guaranteed that the two operands to == are always of the same type. In Java, the equals method has to take one argument of type "Object", while the "this" argument has the class's type.
* Multiparameter type classes. (This is behind a flag in the Haskell compiler). You can do something like this:
class Eating E F where
eat :: E -> F -> Meal
# E is short for Eater
# F is short for Food
so now I can type check to make sure that eaters eat the right type of food.
instance Carnivore Meat where
eat = # specialized implementation for eating meat
instance Carnivore Vegetables where
eat = # specialized implementation for eating vegetables
instance Vegeterian Vegetables where
eat = # specialized implementation for vegeterian food
AFAIK, >1-arity polymorphism in OOP languages is generally achieved with double dispatch. But it's not as nice (or as easy to understand IMO) as in ad hoc polymorphism.
Rust does not have HKT or multi-param traits though.
One commenter already noted higher-kinded type classes, which is the basis of Haskell's libraries and do-notation for monads. That's HUGE.
But besides higher-kinded type classes, there are others:
* Factories and constants in your interfaces. Type classes allow you to put a type variable in the return argument of a method. This lets you put factories and constants in your type classes. For example, if you look at the monoid type class:
And I can implement this So now I can write generic code where I can get the "zero constant" of a type without knowing what type that is. I could just have a genericMonoid : T where T is a type variable, and all I know about it is that it belongs to class Monoid (maybe it's an integer, maybe it's a string, maybe it's something else). I can now get the zero value of whatever that type is. In fact, my method doesn't have to be dispatched on the type of some input argument. I can dispatch on the return value. Something like (Forgive my crappy Haskell pseudo syntax, I haven't written it in a while). Not that multiply by zero is useful.* Operators of greater arity than 1. So I hinted at this above. With "method syntax" arity is always 1. I have one "this". I can operate on it. For all other arguments to my method, I cannot actually constrain them to have the same type as "this". At best, with OOP, my method belongs to some base class, and I can insist that the non-this arguments are of base class type, but I cannot constrain them to have "this" type. This is illustrated in the monoid example above, or in the Eq example I gave, where it can be guaranteed that the two operands to == are always of the same type. In Java, the equals method has to take one argument of type "Object", while the "this" argument has the class's type.
* Multiparameter type classes. (This is behind a flag in the Haskell compiler). You can do something like this:
so now I can type check to make sure that eaters eat the right type of food.