Ixrec / rust-orphan-rules

An unofficial, experimental place for documenting and gathering feedback on the design problems around Rust's orphan rules
Apache License 2.0
194 stars 3 forks source link

A fundamental assumption of this repo is false #22

Open steventrouble opened 1 year ago

steventrouble commented 1 year ago

One Big Issue

This document relies on the following assumption:

Rust chooses to enforce coherence

But alas, this assumption is not true.

Rust supports incoherence

Rust actually supports incoherence in many cases. Consider the following code:

trait Foo {
    fn init(&self);
}

trait Bar {
    fn init(&self);
}

struct MyStruct;

impl Foo for MyStruct {
    fn get(&self) {
        println!("Foo.init()");
    }
}

impl Bar for MyStruct {
    fn init(&self) {
        println!("Bar.init()");
    }
}

fn main() {
    let struct = MyStruct;
    struct.init(); // Does not compile
    Bar::init(&struct);
}

Here, there are two valid implementations of init() here, and the developer had to disambiguate which the compiler should use. This is a type of incoherence, and it is fully supported in Rust.

What's going on here?

It is difficult to avoid some amount of incoherence in a language that a) allows multiple implementations on a struct, and b) allows calling implementations without choosing a specific implementation.

To implement these features, you must combine disparate namespaces into a single shared namespace at some point. You can do this by either: a) allowing overlapping symbol names and requiring disambiguation, or b) requiring symbol names to be globally unique.

As seen above, the Rust developers clearly decided that a) disambiguation should be supported for the example above. They also allow disambiguation in the cases of generics, such as in the example of ::<T>collect(). So the question is not "Why does Rust not support incoherence?" because the question has a false assumption. Rust does support incoherence in some capacity.

The real question is: Why does Rust support some kinds of incoherence but not others?

Function Incoherence vs Trait Incoherence

In Rust, there is an easy way to disambiguate functions: using the trait name. When a developer needs to call a function from a trait, it's easy for them to disambiguate it by using the Trait::func(myStruct) syntax.

Impls, however, are not named. Oops! Rust just dumps all valid implementations of a trait into a global namespace and includes them all whenever a trait is used. Because of this, there is no easy way for developers to specify which trait impl should be used, and therefore trait impls are required to be globally unique.

"Ok, but what about..."

In "What's wrong with incoherence", this repo mentions a few potential problems with incoherence, and proposes a solution to these problems. It then criticizes that solution. That is a classic straw man fallacy: arguing against a solution that you yourself proposed. It's a fallacy because better alternatives might exist that you neglected mention.

In particular, generics in Rust avoid all the problems with your solution. You specify generics at the call site, not at the crate level, which avoids all the issues the straw man has. Because why would anyone disambiguate function calls at the crate level? That doesn't even make sense.

What would it take?

To enable disambiguating trait impls, every impl would have to have an identifier, and for backwards compatibility these would have to be auto-generated somehow if the code didn't specify one.

This would be an enormous undertaking, and the auto-generated identiers would make the crate imports section quite messy in the rare cases you needed to disambiguate traits.

The Real Reason

So let's get to the heart of the issue. Why is impl incoherence forbidden, while all other important types of incoherence are allowed?

Occam's razor tells us that the simplest valid explanation is usually the best one:

Because it's hard and we don't want to do it.

Let's stop spreading the lie that incoherence is inherently wrong. Many programming languages, including Rust, support disambiguation in many other cases, so it can't be that bad. Instead, let's just be honest and admit that we don't have the time or energy to implement it for this particular case.