Just some informal thoughts about a classical problem and a possibly interesting proposition to improve it in Rust. Maybe the topic has too few practical impacts for Rust to care about. But at least I’d be interesting in feedback.
I suppose many of us already know the Circle-ellipse problem, which is a conceptual problem demonstrating the discrepancy between our natural perception of the specific/generic relation and what the sub-typing relation (in the sense of Liskov) allows. Of course Rust has no subtyping between concrete types (structs, enums, tuples, …) but it does define a hierarchy of traits so the same issue may apply. As a reminder this is how we could explain the Circle-ellipse problem with Rust traits:
trait Ellipse {
fn getRadiuses(&self) -> (f64, f64);
fn setRadiuses(&mut self, minor: f64, major: f64);
}
trait Circle : Ellipse {
fn getRadius(&self) -> f64;
fn setRadius(&mut self, radius: f64);
}
Although semantically we naturally consider that every circle is also an ellipse, the design above introduces an issue: how should the function setRadiuses
behave for implementations of Circle
? We can arguably consider it does not make sense in this context but as a consequence we will have to either remove this method from Ellipse
or give up the sub-typing relation between the traits. As generally pointed, the problem is rooted in the interaction between categories and mutability: a change of state for a given item might preserve its membership to a very generic category while at the same time invalidating its membership to a more specific category (as another concrete example, a citizen that got naturalized is still a citizen but he/she might not be a Chinese citizen anymore). Few programming languages define mutability as a syntactic concept and as a consequence they generally cannot provide very satisfying solution. Rust is one of the few languages that includes mutability in its type system. So what about this:
trait Circle : const Ellipse {
fn getRadius(&self) -> f64;
fn setRadius(&mut self, radius: f64);
}
The idea is to provide a weaker kind of relation for traits (that we may call weak or const sub-typing). As a weak sub-trait of Ellipse
, Circle
only imports the part of its interface where self
or Self
appears as non-mutable references in parameters, which excludes setRadiuses
. I guess cases where Self
appears inside non-mutable references of complex types (like &[Self]
) could also be considered as valid. This extension of the type system means that any trait now defines two interfaces: a weak one and the standard (complete) one. This naturally leads to at least 3 additional extensions:
- An implementation may be restricted to a weak trait.
- Any weak trait may appear as a trait bound.
- Const references on trait object may handle instances of a broader set of concrete types (as they naturally require a weaker interface).
I’m aware that for short traits (in particular one-method traits) this is useless as there is no sub-interface to extract. So as said in the introduction in practice that might bring limited benefits if the general philosophy is to have strong restrictions on the traits size (but as a counter-argument trait hierarchy naturally increases the size of traits even when they have only a limited number of proper methods).