Hi, i’m starting a new thread because the related ones are quite old, but for reference: named instances, traits + ML modules, revisiting modules (this one mostly discusses syntax/usability).
What is this all about: I don’t like the hard restrictions that Rust puts on implementing instances and the bunch of newtype pattern it implies; also I like the ML way of expressing interfaces more.
Comparison of alternatives
Okay so in the field of languages with an expressive static type system there are 2 big contenders for expressing interfaces (a set of operations that multiple types can provide):
-
Haskell with typeclasses. I’m not gonna extend on this, it’s basically the same as Rust’s traits, but an important fact is that this system can’t handle two implementations of an interface for a given type.
The biggest downside of Haskell-like modules is the fact that you have to ensure you never get two clashing instances but because they are not named you can’t let the user manage them properly: you have to either behave weirdly on imports (see orphan instances in Haskell) or have drastic restrictions on where you can implement an instance for a given interface and type (rust). Also, lots of people say we rarely need two implementations of the same interface for a given type. This is just not true given how much the newtype pattern is used in Rust and Haskell (sure, newtypes are also used for other things but even then), this is especially true for combinations of basic types and basic interfaces like
Hash
,Ord
orAdd
for types like integers, floats or strings. -
the ML family has powerful modules that are typed. The core language doesn’t know anything about interfaces but you can express it via a module type item:
module type Display = sig type t val display: t -> string end module DisplayInt: Display = struct type t = int let display n = ... end
On the other hand, I can’t argue with the fact that ML modules are too verbose: you must specify which instance you use every time (and making function which take modules as arguments – Rust calls this virtual dispatch – is quite a lot of pain).
-
Come a third contender to the party: explicitly implicit arguments! This is the path taken by Scala (implicit parameters), Agda (instance arguments) but also Idris has named implementations that look like it and some people on OCaml work on modular implicits which is again the same idea. The definition is ML-style: implementations are named, it boils down to a module conforming to some module-type (the interface) but the usage is typeclass-style. The trick to achieve that is to let the user explicitely mark instances to be added to the local implicits pool which is used to resolve which instance to use in function calls with trait bounds. This pool will maintain the condition that an unique instance should be available.
Surface level
The main thing is that impl
items would be named, but there are a few different ways to make this implicits pool happen on the surface level (not exclusive to each other):
- The marking is done automatically on definition with
impl trait_type: Trait for Type {...}
. When writinguse foo::bar
everyimpl
item infoo::bar
is added to the pool and there would be an opt-out withuse foo::bar hiding disturbing_impl
. - As python says, “explicit is better than implicit”, so the marking could be done on import with say
implicit use foo::bar::useful_impl
but this line could also be added in some prelude to make things less painful. There could also be the convention thatimpl
items for a modulefoo::bar
live infoo::bar::impls
so that one can doimplicit use foo::bar::impls::*;
.
Note that in any case, this may also lift the requirement of use
-ing a trait when calling a method (not completely sure about that): one would need to implicitely use an instance instead.
There would be a slight semantic change to the meaning of <T as Trait>
which would now mean the “the concrete module derived from the local implicit pool” (the same would happen to Trait::method(arg)
and arg.method()
, with T
and Trait
just being the inferred from type of arg
and from the method
identifier as is currently done).
Instances, modules and anonymous traits
In the ML-style, instances are plain modules, but in Rust (unlike Haskell), every trait has one special parameter Self
so instances are tied to a particular type. It may be valid to drop that but I don’t think so because almost every typeclass is used to describe an interface and interfaces always have this single special Self
parameter. So I’m suggesting to keep current modules like they are (namespaces) and only make impl
items more like ML-modules (but this can be interesting to discuss).
There is still one special thing to worry about: anonymous traits. What I call an anonymous trait is the unnamed trait in the current impl Type {...}
. A good idea might be to have Type
as a magic name for the anonymous trait associated with Type
, to have the Type::method
syntax. I think that these impls should still be named, because apart from the fact that the trait definition is inferred and unnamed, they are impl
items. This would enable to get rid of the TypeExt
(SliceExt
, FileExt
) pattern for which you define a trait that is ever only implemented for one type.
Misc
- Trait objects should be left unharmed by this (just to mention I considered it).
Quick mockup
I don’t care about the syntax/keywords here (you may like use impl
better), it’s just to make it clear how it may look like for people that never encountered this kind of implicit thing:
trait Foo<T> {
fn bar(&self) -> Option<T>;
}
struct Baz<T> = { data: i32, more_data: T }
mod impls {
impl foo_baz<T: Clone>: Foo<T> for Baz<T> {
fn bar(&self) -> Option<T> {
Option::Some(self.more_data.clone())
}
}
}
mod third_party_crate {
use super::{Foo,Baz};
// this is not currently added to the implicits pool
impl my_foo_baz<T>: Foo<T> for Baz<T> {
fn bar(&self) -> Option<T> {
Option::None
}
}
// this should fail because the implicits pool is empty
fn test_1<T>(arg: &Baz<T>) -> Option<T> {
arg.bar()
}
// use the foreign one by implicit use
fn test_2<T: Clone>(arg: &Baz<T>) -> Option<T> {
implicit use super::impls::*;
arg.bar()
}
// use the local one by implicit use
fn test_3<T>(arg: &Baz<T>) -> Option<T> {
implicit use my_foo_baz;
arg.bar()
}
// this lets the caller choose the impl
fn test_4<T, U: Foo<T>>(arg: &U) -> Option<T> {
arg.bar()
}
// explicitely overriding the implicit resolver
fn test_5<T: Clone>(arg: &Baz<T>) -> Option<T> {
implicit use super::impls::*;
test_4::<T, my_foo_baz<T>>(arg)
// same as my_foo_baz::bar(arg)
}
}
Please make comments, note that this is not even a pre-RFC, i just want to know if the ML-cheerleaders back me up here, why you think this is bad/difficult (because similar things have already been discussed, even if not in this way afaik) or what awesome usecase you would have for such a feature.