4 ms·
One highlight from the article: > Traits. I generally don't like traits / typeclasses. To my eyes they're too type-directed, too global, too much like programm
by elcritch 3y ago
One highlight from the article:
> Traits. I generally don't like traits / typeclasses. To my eyes they're too type-directed, too global, too much like programming and debugging a distributed set of invisible inference rules, and produce too much coupling between libraries in terms of what is or isn't allowed to maintain coherence.
Very much this!
> I wanted (and got part way into building) a first class module system in the ML tradition. Many team members objected because these systems are more verbose, often painfully so, and I lost the argument. But I still don't like the result, and I would probably have backed the experiment out "if I'd been BDFL".
I seen this the ML first class module system mentioned in a few different contexts and want to pick it up at some point.
- wuiheerfoj 3y agoMay I ask what you don’t like about traits (and perhaps insight on what the author meant)? Being a relative rust noob compared to other languages, I always felt traits were a super power when compared to eg interfaces in Java/C++ and flexible but with useful constraints compared to the structural typing nature of Typescript interfaces
- nyanpasu64 3y agoImporting a trait into scope can silently materialize methods on other types, with no syntax at the method call site pointing to which import statement created the methods, requiring you to ask the compiler/IDE. This can also mean that removing a use statement with no references to the type being used in the entire rest of the source file, can make code suddenly stop compiling. I've worked around this by strictly confining "specialized" traits (like std::io::Write or std::fmt::Write, why are there two Writes?!) to the beginning of a single function, but I'm lazy and put "general" traits like std::borrow::Borrow and std::str::FromStr at the top of my file. Which types grow new trait methods is dependent on trait implementations, which can be generic and apply to an unbounded number of types, based on complex matching rules, and figuring out requires reading every impl of that trait to see if any match a given type, or asking the compiler/IDE. I want to explore languages which explicitly select a trait implementation at the call site (like a Heap<int, int_greater> type which uses int_greater::cmp() to implement a min/max heap), or naming the trait (but not picking an impl) at the trait method call site (like Write::write_all(stream, "hello world") or (stream as Write).write_all("hello world")). I think Zig takes a similar direction.
- zozbot234 3y agoYou can use universal function call syntax in Rust: <Type<TyArg> as Trait>::trait_fn(self_arg, fn_arg1, fn_arg2);
- glandium 3y agoThere's worse. When your trait adds function foo and your code does obj.foo(), if the underlying type later adds a foo method, compilation breaks. This is very common with traits that usefully add useful methods that are missing in libstd... which break when said method is finally added there.
- Gwypaas 3y agoI ran into this case yesterday, for some cases there are warnings now. In this case I used ".div_ceil(...)" from from the Num [0] crate. (some_integer).div_ceil(&2) ^^^^^^^^ = warning: once this associated item is added to the standard library, the ambiguity may cause an error or change in behavior! = note: for more information, see issue #48919 <https://github.com/rust-lang/rust/issues/48919> = help: call with fully qualified syntax `num::Integer::div_ceil(...)` to keep using the current method = note: `#[warn(unstable_name_collisions)]` on by default [0]: https://crates.io/crates/num https://crates.io/crates/num
- sanxiyn 3y agoIn a trait-like system (including Java interface), there can be only one implementation of a trait for a type. That's why it is global, unlike ML module-like systems. Global is another word for anti-modular. For example, in the real world, there are multiple ways to order strings (called collation), but since there can be only one implementation of (String, Ord) type-trait pair, one ordering is canonical. That may be bearable, but having canonical (String, Hash) implementation is not. What if you want to use (say) faster CityHash instead of canonical MurmurHash? So Rust resorts to things like BuildHasher, to get back multiple implementations. Because it is global/anti-modular, it interferes with separate compilation, and it is one of reasons why Rust is slow to compile. Then why would one use trait instead of module? Since there can be multiple implementations in module, you need to specify. So module is more verbose. In my (and Graydon's) opinion, a bit more verbosity is worth it for modularity, but many people disagreed. This is another theme: Graydon is okay with verbosity, boilerplate, and being bureaucratic. In my (and Graydon's) opinion, programming is work that is secretarial, not artistic, so it is unimportant whether code is ugly or not. You may think current Rust is ugly, but no, it is the way it is because lots of people really cared about Rust code being pretty. If Graydon was a BDFL, Rust would be even more ugly, and in my opinion, as a result, would be a better programming language.
- wuiheerfoj 3y agoThanks for this - the sort example made a lot of sense to me. I thought it was possible to parameterise with specific trait implementations for such a case though I haven’t used it much with I could be misunderstanding.
- giraffe_lady 3y agoI loved reading that part! I've always found ocaml's (and now rescript's) first class modules flexible and powerful. They're definitely awkward but in that way where you're having to explicitly think about and declare relationships you'd happily ignore until they bite you. Recently I've seen a lot of programmers I respect consider them a weaker or failed alternative to typeclasses, basically a dead-end. And I had been reluctantly coming to the conclusion that I must be wrong in some way I'm not able to fully perceive yet. Seeing such a notable PL designer come out on their side makes me feel less like a fool.
- dan-robertson 3y agoI often felt that being canonical was a pretty big advantage of traits. For example if you use first-class modules then every time you want a sort or a binary search tree you need to specify the choice of comparison operator for the elements. And a data structure for a search tree must somehow encode this in the type system so eg you can’t merge two trees with the same key type but different comparison functions. The most obvious way to implement this causes problems for other cases like writing functions generic in the types of keys/values (else you get extremely verbose code). Canonical instances goes against modularity but something like modular implicits for ML-family languages could improve the terse was a lot to something closer to Haskell levels. You still miss out on the other advantage rust has: the dot operator is great because it allows identifiers to contain less context (they don’t need to have long global names or be hidden away in some tree of modules: you can have .map mean different things for different types) and gives a massive hint to autocomplete for which things to suggest (I don’t know how you even signal to autocomplete in a language like ML that you would like a function to operate on the following value so please suggest things with the right type).
- zozbot234 3y agoMost type systems are not full-featured enough to elegantly encode traits/typeclasses or ML modules. You need dependent types, as seen in languages like Agda or Idris. And these in turn are hard to implement in a language with a traditional compile/run phase distinction like Rust: the _full_ feature-set of dependent types is pretty much only available at compile time. (Which is why dependent-typed languages tend to add "program extraction" features, which reintroduce that phase separation in a rather ad-hoc way.)
- eru 3y agoHaving used both OCaml and Haskell in production, I can say that the typeclass way of doing things is massively more convenient. But I guess enough syntactic sugar might make things palatable.