It seems weird to me to describe traits as not good enough because gluing together disparate libraries requires newtype wrappers, while proposing a replacement with grotesquely ambiguous semantics based on what could be generously described as academic navel-gazing.
This claim in particular:
I’m sure it won’t take much to convince you; [newtype wrappers are] unsatisfying. It’s straightforward in our contrived example. In real world code, it is not always so straightforward to wrap a type. Even if it is, are we supposed to wrap every type for every trait implementation we might need?
In fact it will take a lot to convince me that an extra couple lines of mechanical glue code is worse! Yes, even if you end up wrapping multiple traits!
The lack of orphan instances in Rust can be frustrating in times when a single library contains multiple crates, especially when I’m doing so to avoid a dependency on alloc (the lack of orphans means I can’t use Cow), but I’ve never wished for making method resolution more implicit. I’ve especially never wished to redefine it such that the programmer has to wade through hundreds of pages of type-theoretical gobbledygook to figure out why cmp() is returning the wrong value.
Yes, that’s… why it’s a local maximum? It works really quite well, so it’s hard to find something better without first making something worse? But if you’re gonna be exploring design space that hasn’t actually been explored much, then understanding the tradeoffs involved are kinda important. Is it worth it? Probably not yet, but people keep reinventing modules and traits have some very concrete downsides. Enforcing coherence needs whole-program type information, as they demonstrate coherence can still have holes, and so on. So, let’s get off our high horse as if typeclasses don’t originate from a couple decades of academic navel-gazing and hundreds of pages of type-theoretic gobbledygook, and poke around to see if we can make something useful out of this interesting variation.
If you knew that it was gonna work already then it wouldn’t be science, now would it.
Yes, that’s… why it’s a local maximum? It works really quite well, so it’s hard to find something better without first making something worse?
To be a local maxima is to know that a better solution exists elsewhere – it doesn’t necessarily have to have been fully described, but it must exist. The author’s claim that traits are a local maxima is equivalent to claiming that they have found a non-trait mechanism for type->method scoping that is better than traits in every way. The rest of the post fails to support that claim.
In particular, this part:
We can imagine we have a global scope of traits, and we only ever want one implementation per type in that scope. I’m going to call enforcing coherence in this way: global coherence.
[…] Our issues with traits all orbit around requiring global coherence. Ironically, global coherence is what prevents traits from being a global maxima.
It’s obvious from here, if global coherence is a local maxima, local coherence is a global maxima. Now all we have to do is figure out if, and what, local coherence is.
is nonsense. The author assumes axiomatically that a globally-consistent mapping of types to methods is undesirable and proposes something called “local coherence” (based on … grammatical negation??), then goes off on a hunt for whatever that might be. They haven’t tried to figure out why they think globally-consistent traits are undesirable, they haven’t tried to figure out something better and then named it; they started with a name and then tried to go backwards to identify a concept.
The problem with their approach in this case, of course, is that in a nominative type system you do want a globally consistent association of types and traits, because otherwise the same code with the same types in different modules might have different behavior.
people keep reinventing modules and traits have some very concrete downsides.
It’s possible that traits have downsides (compared to … what?), but the author hasn’t identified any of them. The best they do is gesture vaguely in the direction of requiring boilerplate when combining libraries, which isn’t convincing in Rust (a language that is full of boilerplate).
If the author wants to explore actual downsides of traits, then good starting points might be:
Traits form a parallel “is-a” hierarchy separate from “contains-a” value types, which makes it difficult to wrap libraries designed for OOP languages where those aren’t clearly distinguished.
For example a UI framework might say that a ToggleButton extends Button and implements Widget, such that toggle_button.click() is (toggle_button as Button).click(), but this layout is difficult (or impossible) to represent in Rust because Button can’t be both a trait and a struct.
The question of “sealed” traits, where implementations of a public trait can only be defined within the library that defines the trait. Sealed traits are useful because they act like a locally-extensible implicit tagged union.
Haskell and Rust both require a sort of scope hack (the public trait depends on a non-exported parent), which can interfere with type inference and cause accidental un-sealing if the internal trait ends up in a public module.
In Rust, adding optional methods to a trait can be a backwards-incompatible change if the method name clashes with an inherent method of a type the trait is implemented for.
This doesn’t affect Haskell because it doesn’t have value-scoped function resolution, so it’s more a problem of Rust’s syntax rather than traits themselves, but it could be solved by requiring the trait methods to be brought into scope (or otherwise unambiguously referenced).
Note that none of these are related to the author’s wish for local scoping of trait implementations.
Enforcing coherence needs whole-program type information, as they demonstrate coherence can still have holes, and so on.
They demonstrate no such thing. They link to a GHC bug in which Haskell’s poor design leads to unexpected behavior, but that’s a problem with Haskell allowing orphan instances in -X Safe code, not with the concept of traits in and of themselves.
So, let’s get off our high horse as if typeclasses don’t originate from a couple decades of academic navel-gazing and hundreds of pages of type-theoretic gobbledygook, and poke around to see if we can make something useful out of this interesting variation.
The origin of an idea is unimportant.
I don’t need to read any papers on type theory to understand the behavior of Haskell’s class or Rust’s trait, and the concept has been successfully implemented in multiple languages.
In contrast, the author’s proposal of local implicit bindings of type-parameterized methods seems to exist only in the form of 84 pages of prose, which is within epsilon of being scrawled in crayon during an LSD trip.
If you knew that it was gonna work already then it wouldn’t be science, now would it.
I don’t see any science happening in this blog post, and I reject the idea that using word games to craft unanswerable questions is science in any sense.
FWIW you can emulate implicits entirely by introducing one extra type parameter, and then specifying that at the override site. This is commonly used across the Rust ecosystem to define behavioural ‘strategies’.
I think it’s not fully explained in the article, but these kinds of “implicits” systems often use values in nearby scopes, so you can’t recreate the same functionality only by passing an extra type parameter (at least in Rust). It’s alluded to in statements like this:
One simple solution is available. union can take its own Ord value, implicitly […]
But, besides that, explicitly threading through an extra type and/or value parameter everywhere is the ergonomic issue that “implicits” are trying to avoid, so it’s not a substitute.
It seems weird to me to describe traits as not good enough because gluing together disparate libraries requires newtype wrappers, while proposing a replacement with grotesquely ambiguous semantics based on what could be generously described as academic navel-gazing.
This claim in particular:
In fact it will take a lot to convince me that an extra couple lines of mechanical glue code is worse! Yes, even if you end up wrapping multiple traits!
The lack of orphan instances in Rust can be frustrating in times when a single library contains multiple crates, especially when I’m doing so to avoid a dependency on
alloc
(the lack of orphans means I can’t useCow
), but I’ve never wished for making method resolution more implicit. I’ve especially never wished to redefine it such that the programmer has to wade through hundreds of pages of type-theoretical gobbledygook to figure out whycmp()
is returning the wrong value.Yes, that’s… why it’s a local maximum? It works really quite well, so it’s hard to find something better without first making something worse? But if you’re gonna be exploring design space that hasn’t actually been explored much, then understanding the tradeoffs involved are kinda important. Is it worth it? Probably not yet, but people keep reinventing modules and traits have some very concrete downsides. Enforcing coherence needs whole-program type information, as they demonstrate coherence can still have holes, and so on. So, let’s get off our high horse as if typeclasses don’t originate from a couple decades of academic navel-gazing and hundreds of pages of type-theoretic gobbledygook, and poke around to see if we can make something useful out of this interesting variation.
If you knew that it was gonna work already then it wouldn’t be science, now would it.
To be a local maxima is to know that a better solution exists elsewhere – it doesn’t necessarily have to have been fully described, but it must exist. The author’s claim that traits are a local maxima is equivalent to claiming that they have found a non-trait mechanism for type->method scoping that is better than traits in every way. The rest of the post fails to support that claim.
In particular, this part:
is nonsense. The author assumes axiomatically that a globally-consistent mapping of types to methods is undesirable and proposes something called “local coherence” (based on … grammatical negation??), then goes off on a hunt for whatever that might be. They haven’t tried to figure out why they think globally-consistent traits are undesirable, they haven’t tried to figure out something better and then named it; they started with a name and then tried to go backwards to identify a concept.
The problem with their approach in this case, of course, is that in a nominative type system you do want a globally consistent association of types and traits, because otherwise the same code with the same types in different modules might have different behavior.
It’s possible that traits have downsides (compared to … what?), but the author hasn’t identified any of them. The best they do is gesture vaguely in the direction of requiring boilerplate when combining libraries, which isn’t convincing in Rust (a language that is full of boilerplate).
If the author wants to explore actual downsides of traits, then good starting points might be:
ToggleButton
extendsButton
and implementsWidget
, such thattoggle_button.click()
is(toggle_button as Button).click()
, but this layout is difficult (or impossible) to represent in Rust becauseButton
can’t be both atrait
and astruct
.Note that none of these are related to the author’s wish for local scoping of trait implementations.
They demonstrate no such thing. They link to a GHC bug in which Haskell’s poor design leads to unexpected behavior, but that’s a problem with Haskell allowing orphan instances in
-X Safe
code, not with the concept of traits in and of themselves.The origin of an idea is unimportant.
I don’t need to read any papers on type theory to understand the behavior of Haskell’s
class
or Rust’strait
, and the concept has been successfully implemented in multiple languages.In contrast, the author’s proposal of local implicit bindings of type-parameterized methods seems to exist only in the form of 84 pages of prose, which is within epsilon of being scrawled in crayon during an LSD trip.
I don’t see any science happening in this blog post, and I reject the idea that using word games to craft unanswerable questions is science in any sense.
FWIW you can emulate implicits entirely by introducing one extra type parameter, and then specifying that at the override site. This is commonly used across the Rust ecosystem to define behavioural ‘strategies’.
I think it’s not fully explained in the article, but these kinds of “implicits” systems often use values in nearby scopes, so you can’t recreate the same functionality only by passing an extra type parameter (at least in Rust). It’s alluded to in statements like this:
But, besides that, explicitly threading through an extra type and/or value parameter everywhere is the ergonomic issue that “implicits” are trying to avoid, so it’s not a substitute.