10
$\begingroup$

In Rust, structs and enums may implement any number of traits, which specify certain functions. When a struct/enum implements a trait, this is where the functions are defined:

impl Foo for Thing {
    fn bar() { ... }
}

This allows one type to implement two or more traits with methods with conflicting names/signatures, since a trait needs to be imported for its methods to be used.

In comparison, many OOP languages have interfaces, which serve a similar purpose. When a class implements an interface, it must have every method the interface requires, but the method doesn't "belong" to the interface in the way that it does with Rust's traits, among other differences. Additionally, a new trait can be implemented on an existing class, whereas interfaces cannot.

What are the pros and cons of these two approaches?

$\endgroup$
4
  • 5
    $\begingroup$ There is no real hard line between traits and interfaces. In C# for example you can choose to implement a method differently with the same name for 2 different interfaces. $\endgroup$
    – mousetail
    May 18, 2023 at 15:07
  • 7
    $\begingroup$ I would say the main difference is that it's possible to implement a trait for a third-party type, but you cannot implement an interface for a third-party class, at least not in the languages I'm used to (instead you would use the adapter pattern). $\endgroup$
    – kaya3
    May 18, 2023 at 15:11
  • $\begingroup$ @kaya3 Oh this is an important one. Lemme add that to the question. $\endgroup$ May 18, 2023 at 15:11
  • 1
    $\begingroup$ Note that "traits" can mean something different. The traits in MLIR refer to properties, like how an operation is commutative. $\endgroup$ May 18, 2023 at 15:58

3 Answers 3

6
$\begingroup$

Traits:

  • Are not types. You need something like Rust's impl Trait or dyn Trait.
  • Are usually resolved implicitly. OCaml's modules is a counterexample.
  • Can have overlapping implementations. For instance, implementing Display<(Vec<T>, Vec<char>)> and Display<(Vec<char>, Vec<T>)> at the same time may cause issues.
  • Needs a prolog-style resolution, whose time complexity can be high (depending on how rich your type theory is).
  • Multiple trait impl is not a problem, can be added freely (Rust's orphan rule is not mandatory) if we don't care about clashes.
  • Can have an impl for a special case of a type. For instance, impl ToString for Vec<char>.
  • Identically named functions in different traits are possible.

Interfaces:

  • Are types. Put them directly in the signatures.
  • Requires subtyping (and probably subtyping polymorphism) to work properly. This is horrible for type theory, because $(a : A) \in \Gamma$ does not mean $a : A$ in the context $\Gamma$! It only means $a : B$ for some $B :< A$.
  • Resolution is trivial. Lookup the inheritance tree!
  • Is tied to the class system. We cannot, say, implement ToString for the type List<char>. We may only do so for the generic List<T>.
  • Implementations of methods are tied to the subclass. They must not clash.
$\endgroup$
9
  • $\begingroup$ It can be argued that List in signatures is syntax sugar for impl List in interface-based languages, as I was discussing with the OP a while back before this site existed $\endgroup$
    – Seggan
    May 18, 2023 at 16:39
  • $\begingroup$ @Seggan In Java you're free to have Vec<List> with each member of a subtype of List, but Vec<impl List> seems much harder to implement $\endgroup$
    – ice1000
    May 18, 2023 at 16:45
  • 1
    $\begingroup$ In Java List is actually closer to dyn List, and they can afford to have it be implicit, as every object is already stored and passed by reference $\endgroup$
    – abel1502
    May 18, 2023 at 19:14
  • 1
    $\begingroup$ Can you explain your notation ("(a : A) ∈ Γ", "B :< A", etc) on the "Requires subtyping..." bullet? $\endgroup$
    – Dai
    Aug 9, 2023 at 10:13
  • 1
    $\begingroup$ @Dai these are hard to explain in a few words. I found hedonisticlearning.com/posts/… which might be helpful. $\endgroup$
    – ice1000
    Aug 9, 2023 at 14:35
4
$\begingroup$

Organization

There’s a chance I misunderstand what you’re getting at, but Swift blurs the line between the two by supporting both syntaxes:

protocol P {
  var x: Int { get }
}

struct Foo: P {
  var x: Int /* stored */
}

struct Bar {}
extension Bar: P {
  var x: Int { return 3 }
}

The latter is commonly used as an organizational tool, as it segregates methods for different protocols and even allows them to live in different files. This is especially important for types like Array that conform to a laundry list of protocols.

$\endgroup$
3
  • 2
    $\begingroup$ Swift and Scala both followed the path of "start with interface but make it powerful enough to be a typeclass", and it's super interesting that they did it in such different ways. In Scala, you land on implicits and a trait keyword which is basically a super-interface. In Swift, you get extension implementations that are as powerful as typeclasses. $\endgroup$ Jul 16, 2023 at 21:28
  • $\begingroup$ @SilvioMayolo - Swift's protocols are not as powerful as typeclasses. AIUI, they do not provide a way of specifying that the type of a function argument, return value or property is the type of the implementing class, which is supported by typeclasses (and is required, for example, to describe the Monad typeclass, as if a type T<A> implements Monad then it must have a function that takes a T<A> and a function from A to B and returns a T<B>, which may not be a different Monad implementation). I don't know enough about Scala's traits to be sure whether this applies there too. $\endgroup$
    – occipita
    Sep 6, 2023 at 22:25
  • $\begingroup$ @occipita They do support Self for "the exact implementing type", but they don't support generic associatedtypes which is necessary for Monad. $\endgroup$
    – Bbrk24
    Sep 6, 2023 at 22:26
3
$\begingroup$

I think you can have a language that has both the traditional OOP interfaces and traits. Anyway, let's look at how we can use either to store a vector of animals. In Rust:

trait Animal {…}
struct Dog;
impl Animal for Dog {…}
struct Cat;
impl Animal for Cat {…}
let animals: Vec<Box<dyn Animal>> = Vec::new();

This vector is now storing two pointers for each animal it contains: one pointer points to the Dog or Cat object (which is holding the data members), the other points to the impl Animal for that struct (which thus is holding the function members).

In C++ you would probably write:

struct Animal {…};
struct Dog: public Animal {…};
struct Cat: public Animal {…};
auto animals = std::vector<std::unique_ptr<Animal>>;

That vector only holds one pointer per animal, and that pointer points to the Dog or Cat object. However, apart from the data members, those objects are also containing a pointer to a vtable.

So you could say that in C++, the impl pointer is stored in the objects themselves, whereas in Rust it's not in the objects, but stored along with the pointer to those objects.

The drawback of C++'s way is that you can't add another trait for a Dog or Cat after the fact, because the layout of the objects, including that of the vtables, is fixed when those structs have been declared. With traits you can add new ones and still have a vector of runtime polymorphic objects.

The drawback of traits is that the vector animals needs to store twice as much. If you have multiple data structures pointing to the same objects, that will increase memory usage compared to OOP interfaces. On the other hand, with C++ every object that is dereived from a base class with virtual member fucntions contains a vtable pointer, even if you don't need it.

I'm pretty sure that you can emulate OOP interfaces in Rust, and traits in C++, but it's of course nicer if the language has a built-in and concise way for doing what you want.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .