11 ms·
An incoherent Rust
- dathinab 7mo agoThis isn't a new discussion it was there around the early rust days too. And IMHO coherence and orphan rules have majorly contributed to the quality of the eco system.
- MeetingsBrowser 7mo agocan you elaborate on how have they contributed to the quality of the ecosystem?
- dathinab 7mo agothere is no good way to handle colliding implementations. Both from parallel crates and due to changes over time. Without it you can have many many additional forms of breakage. Worse you can have "new" breakage between two 3rd party crates without either of them changing due to some impl in a common ancestor changing (e.g. std) and this affecting two wild card implementations in each, now leading to an overlap. When you have an overlap there are two options: - fail compilation, but as mentioned this could be caused by a non breaking change in std in two in theory unrelated 3rd party dependencies - try to choose one of the implementations. But that now gets very messy in multiple points: a) Which impl. to choose when. b) The user knowing which is chosen. c) Overlap with interactions with stuff like double dispatch, thread local variables, and in general side effects. The issues here are similar to specialization (and part why that is stuck in limbo), but a magnitude more complex as specialization is only (meant) for optimizations, while this can be deeply different behavior. Like `foo.bar()` with the same `use Bar as _;` might in one context return an `u32` and in another a `String` In many other ecosystems it's not uncommon to run into having issues where certain libraries can't be used together at all. In rust that is close to not a thing (no_mange collisions and C dependencies are the only exception I can think of). Similar, in my experience the likely hood of running into unintended breaking changes is lower in the rust ecosystem then e.g. python or js, that is partially due to coherence rules forcing a more clean design. Also people are forced to have a somewhat clean dependency tree between crates in ways not all languages requires. This can help with incremental builds and compiler time, a area rust needs any help it can get. (As a side note, clean dependency structures in your modules can (sometimes) help will rust better parallelizing code gen, too.) So overall it I think it's good. Through it can be very annoying. And there is some potential for improvement in many ways. --- EDIT: sorry some keyboard fat-fingering somehow submitted a half written response without me pressing enter... EDIT 2: Fix spelling and sentence structure.
- MeetingsBrowser 7mo ago> In many other ecosystems it's not uncommon to run into having issues where certain libraries can't be used together at all. The same problem exists in Rust, but from the other side. If I use serde for serialization I am effectively locked in to using crates that implement serde traits (or do newtype hacks to define them myself). If I want to use something more niche than serde, I essentially lose access to all the popular crates as they only implement serde traits.
- simonask 7mo agoNewtypes aren’t hacks, they’re perfectly acceptable in my opinion. Especially if you’re willing to also use a crate like `derive_more`.
- MeetingsBrowser 7mo agoIn my experience using newtypes like this causes a constant shuffle between the original type and the newtype. If a library exposes Foo and I wrap it in MyFoo implementing some trait, I need to convert to MyFoo everywhere the trait is needed and back to Foo everywhere the original type is expected. In practice this means cluttering the code with as_foo and as_myfoo all over the place. You could also impl From or Deref for one direction of the conversion, but it makes the code less clear in my opinion.
- simonask 7mo agoOne strategy I like is to declare “view” types for serialization and deserialization, because you’re going to be doing that anyway if your serialized format is meant to be compatible across versions anyway. Serde also comes with a bunch of attributes and features to make it easy to short-circuit this stuff ad hoc. I know this only solves the serialization use case, but that seems to be where most people run into this.
- dathinab 7mo agohonestly in my experience it rarely matters (if you care about stable APIs) as most types you want to have at an API boundary are written (or auto generated) by you this leaves a few often small types like `DateTime<Utc>`, which you can handle with serde serialization function overwrite attributes or automatic conversions not even needing new types (through some of this attributes could be better designed) serde is not perfect but pretty decent, but IMHO the proc macros it provides need some love/a v2 rewrite, which would only affect impl. code gen and as such is fully backward compatible, can be mixed with old code and can be from a different author (i.e. it doesn't have the problem) Anyway that doesn't make the problem go away, just serialization/serde is both the best and worst example. (Best as it's extremely wide spread, "good enough" but not perfect, which is poison for ecosystem evolution, worst as serialization is enough of a special case to make it's best solution be potentially unusable to solve the generic problem (e.g. reflections)).
- dap 7mo agoI've never once pulled in a new dependency and had the program fail to compile just by virtue of that dependency being present [because both my code and the new code both impl'd the same trait on the same type in some other code]. Because that can't happen because of coherence. (Right?) It's so easy to forget about the problems we don't have because of the (good) choices people have made in the past.
- dathinab 7mo ago> Because that can't happen because of coherence. (Right?) yes Through you still can run into it when unsafe is involved, e.g. C FFI/no_mange or ASM with non-mangled labels as they are globally unique. Through IMHO, it's not a common problem and has ways to make it very unlikely for the projects where it matters. In the end if you pull in C-FFI code (or provide it) you do ope yourself up to C ABI specific problems.
- selfmodruntime 7mo agoThere are virtually no incompatible dependencies.
- MeetingsBrowser 7mo agoThe blog post gives an example of how the current approach makes dependencies incompatible. > if someone publishes an alternative to serde (say, nextserde) then all crates which have added support for serde also need to add support for nextserde. Adding support for every new serialization library in existence is unrealistic If I use serde, I cannot use a crate that only implements nextserde. If I want to use nextserde, I lose the ability to use all the crates that only implement serde.
- selfmodruntime 7mo agoLet me rephrase: There are virtually no dependencies that when combined, cause a compiler error.
- ekidd 7mo agoThere's a well-known (and frequently encouraged) workaround for the orphan rule: Create a wrapper type. Let's say you have one library with: pub struct TypeWithSomeSerialization { /* public fields here */ } And you want to define a custom serialization. In this case, you can write: pub struct TypeWithDifferentSerialization(TypeWithSomeSerialization) Then you just implement Serialize and Deserialize for TypeWithDifferentSerialization. This cover most occasional cases where you need to work around the orphan rule. And semantically, it's pretty reasonable: If a type behaves differently, then it really isn't the same type. The alternative is to have a situation where you have library A define a data type, library B define an interface, and library C implement the interface from B for the type from A. Very few languages actually allow this, because you run into the problem where library D tries to do the same thing library C did, but does it differently. There are workarounds, but they add complexity and confusion, which may not be worth it.
- dhosek 7mo agoThe gotcha is what happens when TypeWithSomeSerialization is not something you’re using directly but is contained within SomeOtherTypeWithSomeSerialization which you are using directly. Then things get messy.
- bigfishrunning 7mo agoIn that case, wrap a reference maybe? pub struct TypeWithDifferentSerialization(&TypeWithSomeSerialization)
- deathanatos 7mo agoWe can't say with certainty how an unspecified in-the-future library might work, so I'm going to use serde as a stand-in. You can implement `Serialize` for a wrapper type and still serialize `SomeOtherTypeWithSomeSerialization` (which might be used by the type being wrapper directly or indirectly) differently. It might not be derivable, of course, but "I don't want the default" sort of makes that a given.
- nmilo 7mo agoI will never stop hating on the orphan rule, a perfect summary of what’s behind a lot of rust decisions. Purism and perfectionism at the cost of making a useful language, no better way to torpedo your ecosystem and make adding dependencies really annoying for no reason. Like not even a —dangerously-disable-the-orphan-rule, just no concessions here.
- irishcoffee 7mo agoGo: error handling stinks. Generics would be dope. Rust: if you spent 3 weeks understanding the syntax and borrow-checker, here are all of the other problems, and the list keeps growing. Man this cracks me up.
- voxl 7mo agoGood for better, better for us. Rust is choke full of hard compromises and reactionary subcultures. Just recalling ? alone.
- simonask 7mo agoI think there are legitimate criticisms of Rust that fall in this category, but the orphan rule ain’t it. In most other languages, it is simply not possible to “add” an interface to a class you don’t own. Rust let’s you do that if you own either the type or or the interface. That’s strictly more permissive than the competition. The reasons those other languages have for not letting you add your interface to foreign types, or extend them with new members, are exactly the same reasons that Rust has the orphan rule.
- kryptiskt 7mo agoIt's not a restriction born out of purity, notably uncompromising Haskell allows orphan instances.
- hrmtst93837 7mo ago[flagged]
- postflopclarity 7mo ago
- grougnax 7mo agoIf you think Rust has problems, it is that you've have not understood well Rust.
- nmilo 7mo agoBrilliant
- nixpulvis 7mo agoI don't think explicit naming of impls is wise. They will regularly be TraitImpl or similar and add no real value. If you want to distinguish traits, perhaps force them to be within separate modules and use mod_a::mod_b::<Trait for Type> syntax. > An interesting outcome of removing coherence and having trait bound parameters is that there becomes a meaningful difference between having a trait bound on an impl or on a struct: This seems unfortunate to me.
- wtemple 7mo agoI don't think fully-qualified paths are enough on their own. You also need some way to designate that an impl is symbolically unique and has to be referred to by path. Otherwise, you still end up with a problem where the compiler doesn't know which implementation to use unless you precisely name it. You depend on crates A and B. A impls Foo for Bar. You pass an instance of Bar to a function that accepts `impl Foo`. You are happy. Later crate B adds an impl of Foo for Bar. Clearly _at least_ one of these must be an orphan impl, but both could be. Suddenly it's ambiguous which implementation of Foo you're talking about, so you break because B added an impl. There are many potential problems of this flavor with letting any `impl Trait for Type` be an orphan impl and then referenced by path. What happens, for example, if an impl that was an orphan impl in one version of A becomes a coherent impl in a later version of A? I think there has to be special syntax for named/path-referenced/symbolic impls, even if the impl does not have an identifier name, so that the compiler can know "this impl only resolves if you tell me _specifically this impl_" and the impl provider has a way to create a solid consumer contract about how to use that impl in particular. Also, not having an identifier name would mean you can't have different impls of Foo for Bar in the same module. That's probably not a limitation anyone would care about, but it's there.
- nixpulvis 7mo agoUsing the mod name would give it a unique name, just implicitly through the module, so I don't see the issue, unless you wanted to allow a single module to allow multiple impls of the same item. I also don't see an issue with having multiple impls of the same trait, as long as they don't provide duplicate items inside a module. I often do multiple impl blocks to break up larger logic and organize docs, though this is generally not for trait impls, but I don't see why it couldn't be. Let me be clear though, I'm not saying this is the best path forward on the coherence/orphan situation necessarily, just a minor critique of the blog posts position. This is a famously tricky issue, and I suspect there is no silver bullet here. Though I have always wanted some way to add flexibility to the orphan rule.
- Animats 7mo agoNote the use case - someone wants to have the ability to replace a base-level crate such as serde. When something near the bottom needs work, should there be a process for fixing it, which is a people problem? Or should there be a mechanism for bypassing it, which is a technical solution to a people problem? This is one of the curses of open source. The first approach means that there will be confrontations which must be resolved. The second means a proliferation of very similar packages. This is part of the life cycle of an open source language. Early on, you don't have enough packages to get anything done, and are grateful that someone took the time to code something. Then it becomes clear that the early packages lacked something, and additional packages appear. Over time, you're drowning in cruft. In a previous posting, I mentioned ten years of getting a single standard ISO 8601 date parser adopted, instead of six packages with different bugs. Someone else went through the same exercise with Javascript. Go tends to take the first approach, while Python takes the second. One of Go's strengths is that most of the core packages are maintained and used internally by Google. So you know they've been well-exercised. Between Github and AI, it's all too easy to create minor variants of packages. Plus we now have package supply chain attacks. Curation has thus become more important. At this point in history, it's probably good to push towards the first approach.
- tekacs 7mo agoThis is interesting but I wonder if you would accept that this also has the downside of moving at the speed of humans. In a situation where you're building, I find the orphan rule frustrating because you can be stuck in a situation where you are unable to help yourself without forking half of the crates in the ecosystem. Looking for improvements upstream, even with the absolute best solutions for option 1, has the fundamental downside that you can't unstick yourself.
- tekacs 7mo agoThis is also where I find it surprising that this article doesn't mention Scala at all. There are MANY UX/DX challenges with the implicit and witness system in Scala, so I would never guess suggest it directly, but never have I felt more enabled to solve my own problems in a language (and yes the absolute most complex, Haskell-in-Scala libraries can absolutely an impediment to this). With AI this pace difference is even more noticeable. I do think that the way that Scala approaches this by using imports historically was quite interesting. Using a use statement to bring a trait definition into scope isn't discussed in any of these proposals I think?
- kelnos 7mo agoThis is one of the (several?) things that make me very worried about Rust long-term. I love the language, and reach for it even when it sometimes isn't the most appropriate thing. But reading some of the made-up syntax in the "Removing Coherence" section makes my head hurt. When I used to write Scala, I accepted the fact that I don't have a background in type/set/etc. theory, and that there were some facets of the language that I'd probably never understand, and some code that others had written that I'd probably never understand. With a language like Rust, I feel like we're getting there. Certain GAT syntxes sometimes take some time for me to wrap my head around when I encounter them. Rust feels like it shouldn't be a language where you need to have some serious credentials to be able to understand all its features and syntax. On the other end we have Go, which was explicitly designed to be easy to learn (and, unrelatedly, I don't like for quite a few reasons). But I was hoping that we could have a middle ground here, and that Rust could be a fully-graspable systems-level language. Then again, for more comparison, I haven't used C++ since before they added lambdas. I wonder if C++ has some hairy concepts and syntax today on par with Rust's more difficult parts.
- maccard 7mo ago> I wonder if C++ has some hairy concepts and syntax today https://tartanllama.xyz/posts/cpp-initialization-is-bonkers/ https://tartanllama.xyz/posts/cpp-initialization-is-bonkers/
- fridder 7mo agoI wonder how Zig compares here
- egorelik 7mo agoRust opened the door to innovation in the low-level languages space, but as long as it is already the most theoretically advanced practical language there, it will always attract the audience that actually wants to push it further. I don't know if there is a way to satisfy both audiences.
- estebank 7mo agoI think there is: a schism. Another language, inspired, intelligible and interoperable with Rust, but with other goals, likely ease of use or surface simplicity. In my mind it would be pretty much the same as Rust, but whenever a compile error gives you a suggestion in rustc would instead compile (and at most be a warning in this hypothetical language). Migrating from Rust to this language would be changing a single setting in Cargo.toml. The other way around would be fixing a bunch of compile errors. You could use the entire crate ecosystem in a native way. This language could also serve as a test bed for features that might or might not be suitable for Rust. It can also have a more aggressive evolution schedule, meaning that it wouldn't be perma-1.x, so it can be bolder on what is attempted.
- davej32 7mo ago[dead]
- amluto 7mo agoI’m not convinced that the problem is actually a problem. Suppose someone writes a type PairOfNumbers with a couple fields. The author did not define a serialization. You use it in another type and want it to serialize it as: { "a": 1, "b": 2 } I use it and want to serialize it as: [ 1, 2 ] What we’re doing is fine. You should get your serialization and I should get mine. But if either of us declares, process-wide, that one of us has determined the One True Serialization of PairOfInts, I think we are wrong. Sure, maybe current Rust and current serde make it awkward to declare non-global serializers, but that doesn’t mean that coherence is a mistake.
- lmm 7mo ago> What we’re doing is fine. You should get your serialization and I should get mine. But if either of us declares, process-wide, that one of us has determined the One True Serialization of PairOfInts, I think we are wrong. Well, fine, but then you need to actually implement a module system or something. Currently trait impls are program-wide, and if you say that you're not allowed to make global impls of a trait then that's the same as saying you're not allowed to implement traits at all.
- amluto 7mo agoRust’s orphan rule has the property that there is no spooky action at all distance in terms of program semantics. If I write a library, my library behaves the same way regardless of whether the main program imports a different library. In any case, the OP’s proposed “incoherent” scheme actually is a module system of sorts for conflicting trait impls, and it seems about right for something like serialization.
- mattstir 7mo agoPerhaps that's fine in the particular case of serialization, but that line of thinking breaks down at more fundamental operations like `PartialEq` or `Hash`. Having a different definition for equality fundamentally breaks the program if the two versions ever mix. On the flip-side, it's important that the author is allowed to declare the "correct" way to do something, e.g. in a smart pointer crate where the safety of the program relies on a correct implementation. If traits weren't program-wide, you're just kicking the bucket down the road from the library author having to do define every trait impl to every user defining every trait impl, which is even worse imo
- mbo 7mo agoI never understood why Rust couldn't figure this shit out. Scala did. > If a crate doesn’t implement serde’s traits for its types then those types can’t be used with serde as downstream crates cannot implement serde’s traits for another crate’s types. You are allowed to do this in Scala. > Worse yet, if someone publishes an alternative to serde (say, nextserde) then all crates which have added support for serde also need to add support for nextserde. Adding support for every new serialization library in existence is unrealistic and a lot of work for crate authors. You can easily autoderive a new typeclass instance. With Scala 3, that would be: trait Hash[A]: extension (a: A) def hash: Int trait PrettyPrint[A]: extension (a: A) def pretty: String // If you have Hash for A, you automatically get PrettyPrint for A given autoDerive[A](using h: Hash[A]): PrettyPrint[A] with extension (a: A) def pretty: String = s"<#${a.hash.toHexString}>" > Here we have two overlapping trait impls which specify different values for the associated type Assoc. trait Trait[A]: type Assoc object A: given instance: Trait[Unit] with type Assoc = Long def makeAssoc: instance.Assoc = 0L object B: given instance: Trait[Unit] with type Assoc = String def dropAssoc(a: instance.Assoc): Unit = val s: String = a println(s.length) @main def entry(): Unit = B.dropAssoc(A.makeAssoc) // Found: Playground.A.instance.Assoc Required: Playground.B.instance².Assoc² Scala catches this too.
- switchbak 7mo agoPerhaps I'm insufficiently caffeinated, but isn't the author describing the expression problem? That basically nails what type classes are for (in Scala and elsewhere), no?
- faresahmed 7mo agoTake a look at https://contextgeneric.dev https://contextgeneric.dev, it's as close as one can get to solving this issue without modifying rustc.
- dev_l1x_be 7mo agoHighly Expressive Macros No thanks. Most of the time you do not need macros and adding those is not free. CGP enables you to write overlapping and orphan implementations of any trait, breaking free from Rust's coherence rules while maintaining type safety. I am not sure that I need this. I can't remember to run this issue in the last couple of years. Isn't it the case that coherence is what makes Rust’s dependency graph sound? So, why would I want to give up that?
- hmry 7mo ago> Isn't it the case that coherence is what makes Rust’s dependency graph sound? So, why would I want to give up that? Read the article that comment is on, it's all about why one would want that.
- dev_l1x_be 7mo agoI have read it. I see only theoretical reasons not really practical ones. Maybe I do not use Rust enough to run into issues with coherent Rust.
- mastax 7mo agoLanguage changes could help for sure. There’s a library implementation we can use right now though: https://facet.rs/ https://facet.rs/ Basically a derive macro for reflection. Yeah it’s one (more) trait to derive on all your types but then users can use that to do reflection or pretty printing or diffing or whatever they want.
- devnotes77 7mo ago[dead]
- egorelik 7mo agoSimilar but not exactly the same as named impls, I'd really like to see a language handle this by separating implementing a trait from making a particular existing implementation the implicit default. Orphan rules can apply to the latter, but can be overriden in a local scope by any choice of implementation. This is largely based on a paper I read a long time ago on how one might build a typeclass/trait system on top of an ML-style module system. But, I suspect such a setup can be beneficial even without the full module system.
- ozgrakkurt 7mo agoIt is fundamentally difficult to have an “ecosystem”. Would much rather see a bunch of libraries that implement everything for a given use case like web-dev, embedded etc. Unfortunately this is hard to do in rust because it is hard to implement the low level primitives. Language’s goal should be to make building things easier imo. It should be simple to build a serde or a tokio. From what I have seen in rust, people tend to over-engineer a single library to the absolute limit instead just building a bunch of libraries and moving on. As an example, if it is easy to build a btreemap then you don’t have to have a bunch of traits from a bunch of different libraries pre-implemented on it. You can just copy it, adapt it a bit and move on. Then you can have a complete thing that gives you everything you need to write a web server and it just works
- ozgrakkurt 7mo agoSo what I mean is, having a big library that implements the whole problem is better. Because then each part of that library is simple. Then I can copy paste some parts and change some others to create an alternative library. And it is better for the user of the thing because it is simple. Having everything compatible with everything else and having everything implement every case means every individual part is over-complicated. So it is bad no matter how you combine it together.
- smj-edison 7mo agoI feel like encapsulation and composition are in strong tension, and this is one place where it boils over. I've written a decent bit of Rust, and am currently messing around with Zig. So the comparison is pretty fresh on my mind: In Rust, you can have private fields. In Zig all fields are public. The consequences are pretty well shown with how they print structs: In Rust, you derive Debug, which is a macro that implements the Debug trait at the definition site. In Zig, the printing function uses reflection to enumerate the provided struct's fields, and creates a print string based on that. So Rust has the display logic at the definition site, while Zig has the logic at the call site. It's similar with hash maps: in Rust you derive/implement the Hash and PartialEq trait, in Zig you provide the hash and eq function at the call site. Each one has pretty stark downsides: Zig - since everything is public, you can't guarantee that your invariants are valid. Anyone can mess around with your internals. Rust - once a field is private (which is the convention), nobody else can mess with the internals. This means outside modules can't access internal state, so if the API is bad, you're pretty screwed. Honestly, I'm not sure if there is a way to resolve this tension. EDIT: one more thought: Zig vs Rust also shows up with how object destruction is handled. In Rust you implement a Drop trait, so each object can only have one way to be destroyed. In Zig you use defer/errdefer, so you can choose what type of destructor runs, but this also means you can mess up destruction in subtle ways.
- antonvs 7mo ago> so if the API is bad, you're pretty screwed. Is this really that big a downside? It encourages good APIs. The alternative of everything being public is the kind of feature that quickly becomes a big disadvantage in larger systems and teams, where saying “just don’t footgun yourself” is not a viable strategy. If there’s a workaround to achieve some goal, people will use it, and you end up with an unmaintainable mess. It’s why languages whose names start with C feature so prominently on CVE lists.
- HdS84 7mo agoThere are always corner cases where you might need to do something differently. I had three memorable cases in my career: 1. Python 2.6x had a a stdlib bug where windows event logging did crash the process when the user had some rights set differently. Fix submitted but for the meantime we simply overwrote the private function and could ship. 2. Also python: scikit-learn had a primitive "print everything" strategy, but we need to get it into a logging framework. We overwrote their print wrapper and could ship. 3. In C#, a third party lib insisted on dumping a result to a file. We used reflection to get that as a stream. All three are not ideal - but I think having escape hatches is important. I also think private/public is overrated. Having it as a signal is ok. Forbidding access to privates is too strong.
- bitbasher 7mo agoI used Rust for ~14 months and released one profitable SaaS product built entirely in Rust (actix-web, sqlx, askama). I won't be using Rust moving forward. I do like the language but it's complicated (hard to hold in your head). I feel useless without the LSP and I don't like how taxing the compiler and LSP are on my system. It feels really wasteful to burn CPU and spin up fans every time I save a file. I find it hard to justify using 30+ GB of memory to run an LSP and compiler. I know those are tooling complaints and not really the fault of the language, but they go hand in hand. I've tried using a ctags-based workflow using vim's built in compiler/makeprg, but it's less than ideal. I also dislike the crates.io ecosystem. I hate how crates.io requires a GitHub account to publish anything. We are already centralized around GitHub and Microsoft, why give them more power? There's an open issue on crates.io to support email based signups but it has been open for a decade.
- boardwaalk 7mo agoThose dependencies pretty quickly reveal themselves to be complicated and heavy. I wouldn’t blame Rust for that. I rarely need more than what workspaces and VCS based deps give me, but when I have, putting up and using a non-official registryis pretty easy.
- Ygg2 7mo ago> It feels really wasteful to burn CPU and spin up fans every time I save a file. I find it hard to justify using 30+ GB of memory to run an LSP and compiler. Have you tried using RustRover. I've never seen it go above 2-3GiB of RAM, but I don't write the most complex of software in Rust. > I hate how crates.io requires a GitHub account to publish anything. You don't need Github account to publish iirc, you need it to authorize to crates.io. You can use any Git host, but your account is tied to GitHub.
- gpderetta 7mo agoI have used C++ for 20 years without LSP, but now I wouldn't want to go back to a plain editor.
- andriy_koval 7mo agowhat will you be using for your next project?
- deleted 7mo ago[deleted]
- encody 7mo ago"Note that nonbinary crates still obey the orphan rules." I find it slightly humorous that this sentence contains three words which would be understood completely differently by the majority of the English-speaking population.
- NetMageSCW 7mo agoWas there some reason Rust felt the need to introduce new uses of words for concepts that already had commonly used words?
- hdevalence 7mo agoI don’t think Rust needs this; Rust has done great for the last decade with the coherence rules it has. I am glad to not have to worry about this, and to not have to worry about any of the downstream problems (like linker errors) that coherence structurally eliminates.
- caditinpiscinam 7mo agoI think a lot of developers look at Typescript and come away thinking that a static type system is something you can retrofit onto any language. These devs ask why anyone would still want to use a dynamically typed language, as though static typing is something that can be had for free. But the reality is that a robust type system ends up profoundly shaping the design of a language, and introduces these sorts of thorny design questions, with each option bringing its own tradeoffs and limitations. We want our languages to make it easy to write correct programs. And we want our languages to make it hard to write incorrect programs. And trying to have both at once is very difficult.
- hactually 7mo agoGreat write up of a problem that I'm glad Golang sidesteps The problem with this is that it's systemic and central to Rusts trait-based ecosystem composition. Go’s has a version but it's much smaller and more local. In Go, consumer-defined structural interfaces remove most of the pressure that causes the Rust problem in the first place which is producer led.
- ameixaseca 7mo agoSidesteps by not providing the same level of functionality. As an analogy, it would be equivalent to say that "contrary to an airplane, a car sidesteps the problem of requiring wings". Yes, indeed - but it doesn't fly.
- hactually 6mo agoExcept writing rust doesn't feels like flying, it feels like falling.
- ameixaseca 6mo agoOne could argue flying is just controlled falling.
- Ericson2314 7mo agoAh this is very good, both the directionary tracking and getting rid of as much coherence as possible. Yay!
- Surac 7mo agoAs a non Rust man, how real are the problems in this article? Does it show up in real word or is it just a edge case? I only program in C17, C++ as C with classes and C#. Anyone can give me a good read what Traits even are?
- swiftcoder 7mo ago> As a non Rust man, how real are the problems in this article? Real, but of more concern to folks designing widely-used libraries than to folks using said libraries. > Anyone can give me a good read what Traits even are? You can think of traits as analogous to interfaces in OOP languages (i.e. pure virtual abstract classes in C++ terminology). They just define a set of methods that types can implement to conform to the trait, and then consumers can treat implementing types as if they were the trait. The major differences are: traits are implemented outside the actual type implementation, so arbitrary trait implementations can be added after the type has been written (this is why we need coherence), and rust uses traits as compile-time bounds for generics (templates).
- selfmodruntime 7mo agoVery real for library developers if the ecosystem started to slow down. Nonexistent for current consumers of libraries and application developers.
- kreco 7mo agoAnother imprecise analogy would be to see traits as operators. You decide to define the operator "serde::serialize" for "MyType" but then your are stuck because you can't override or select different operators for "MyType" because only one can exists. That's a regular yet not super common issue with traits (and this is not exclusive to Rust). It's quite irritating because you wouldn't expect this from languages with this degree of modularity.
- shevy-java 7mo agoDoes Rust stumble over its own complexity?
- swiftcoder 7mo agoWho knew that we'd long for the days of SFINAE?
- debugnik 7mo ago> Named Impls and Trait Bound Parameters So they're finally rediscovering OCaml!
- dabinat 7mo agoTo me, the correct solution to the problem of being tied to one ecosystem crate for utility features like serialization or logging is reflection / comptime. The problem is not the orphan rule, it’s that Rust needs reflection a lot more than a dynamically-typed language does, and it should have been added a long time ago. (It’s in development now, but it will most likely be years before it ships in a stable version.)
- WhyNotHugo 7mo agoThe Rust ecosystem does a lot of what I like to call "inverse dependency injection". If a Rust library needs support for TLS, typically that library implements a feature for each existing TLS backend, and keeps first-class integration which each one. The obvious thing would be to have a TLS Trait, and have each TLS library implement that trait (i.e.: dependency injection). Because of to the orphan rule, such a trait would likely have to be declared in a small self-contained library, and each TLS library would implement that trait. I don't see any obvious impediment (aside from the fact that all TLS implementations would have to expose the same API and set of behaviours), but for some reason, the Rust ecosystem has taken the path of "every library has first-class integration with every possible provider". This makes it really tricky to build libraries which rely on other libraries which rely on a TLS library, because your consumers can't easily select, for example, which TLS implementation to use. Libraries end up having lots of feature flags which just propagate feature flags to their dependencies.
- hu3 7mo agoIt's due to the path of least resistance. One solution could be to have common traits versioned in the standard library.
- TrueDuality 7mo agoThe article itself covers the specific reasons that has led to that exact problem and the potential solutions available in the ecosystem with their various trade-offs.
- ozgrakkurt 7mo agoCurious if they could just choose one and move on. It is really toxic to do this on every layer. For example how bad would it be if reqwest only supports rustls and is able to have less traits/generics and compiles faster
- mattstir 7mo agoThat locks users into an ecosystem that may never evolve, which can be fine but doesn't really solve one of the core issues the author was describing. It forces the ecosystem to depend on the oldest and most incumbent crates, rather than newer ones which might be better in some ways.
- SkiFire13 7mo agoRe: specialization and the comptime/reflection initiative Since they allow observing whether a trait is implemented or not in the current crate they would probably become unsound if impls can be declared in downstream crates. They are a partial solution but also make other solutions harder to implement soundly (and viceversa)
- derodero24 7mo ago[flagged]
- hoppp 7mo agoI've been worried about this before and the problem is real. I don 't know who maintains serde but if that gets hacked its gonna be an epic supply chain attack
- mattstir 7mo agoSerde is maintained by dtolnay, who is a very influential figure in Rust mainly through his library development. Serde, syn, anyhow etc end up being pulled in as dependencies to nearly every Rust crate. If his account was compromised, the attack surface is essentially every single other Rust crate... not ideal
- i_don_t_know 7mo agoI’m not sure I fully understand but this seems to be the kind of problem that Ocaml functors solve. You program against an interface (signature) and you supply a concrete implementation (structure) when you want to run it. You can use different implementations in different parts of your application. So maybe do something similar in Rust by expanding how you import and export modules?
- quotemstr 7mo agoDoes the author expect incumbents to relax language rules that grant exorbitant privilege to incumbents? The orphan rule is up there with error handling in ways Rust is a screwed up language that appeals to people who don't know what they're missing. Other systems languages aren't weak in these ways.
- mattstir 7mo agoCould you elaborate on that error handling part? To me, Rust is the only sane language I've worked with that has error-like propagation, in that functions must explicitly state what they can return, so that you don't get some bizarre runtime error thrown because the data was invalid 15 layers deeper
- uzerfcwn 7mo agoI don't know what quotemstr was specifically talking about, but here's my own take. The ideal error handling is inferred algebraic effects like in Koka[1]. This allows you to add a call to an error-throwing function 15 layers down the stack and it's automatically propagated into the type signatures of all functions up the stack (and you can see the inferred effects with a language server or other tooling, similar to Rust's inferred types). Consider the following Rust functions: fn f1() -> Result<(), E1> {...} fn f2() -> Result<(), E2> {...} fn f3() -> Result<(), E3> {...} fn f4() -> Result<(), E4> {f1()?; f2()?;} fn f5() -> Result<(), E5> {f1()?; f3()?;} fn f6() -> Result<(), E6> {f4()?; f5()?;} Now, how do you define E4, E5 and E6? The "correct" way is to use sum types, i.e., `enum E4 {E1(E1), E2(E2)}`, `enum E5 {E1(E1), E3(E3)}` and `enum E6 {E1(E1), E2(E2), E3(E3)}` with the appropriate From traits. The problem is that this involves a ton of boilerplate even with thiserror handling some stuff like the From traits. Since this is such a massive pain, Rust programs tend to instead either define a single error enum type that has all possible errors in the crate, or just use opaque errors like the anyhow crate. The downside is that these approaches lose type information: you no longer know that a function can't return some specific error (unless it returns no errors at all, which is rare), which is ultimately not so different from those languages where you have to guard against bizarre runtime errors. Worse yet, if f1 has to be changed such that it returns 2 new errors, then you need to go through all error types in the call stack and flatten the new errors manually into E4, E5 and E6. If you don't flatten errors, then you end up rebuilding the call stack in error types, which is a whole different can of worms. Algebraic effects just handle all of this more conveniently. That said, an effect system like Koka's isn't viable in a systems programming language like Rust, because optimizing user-defined effects is difficult. But you could have a special compiler-blessed effect for exceptions; algebraic checked exceptions, so to speak. Rust already does this with async. [1] https://koka-lang.github.io https://koka-lang.github.io
- quotemstr 7mo agoAlso it's sort of amazing how few people modify their tools and remove the objectionable bits. For example, Java. Checked exceptions. Everyone hates checked exceptions. They're totally optional. Nothing in the JVM talks about a checked exception. Patch javac, comment out the checked exception checker, and compile. Nothing goes wrong. You can write Java and not deal with checked exceptions. Likewise, you can modify rustc and make it not enforce the orphan rule. Too many people treat their tools as black boxes and their warts as things they must tolerate and not things they can fix with their own two hands without anybody's permission.
- clampd 7mo ago[dead]
- anshulbasia27 7mo ago[dead]
- sanbor 7mo agoTangent to the topic: One of the great things about Go is that the Go team goal is to have a great developer experience. As a result, they try to bundle common third party libraries (mux, zap) into the standard library. For example, they offered an http server, but due to lacking features community packages offered convenience. The Go team used those libraries as a reference to what people wanted, and addrd a performant and simple http routing in the standard library[1]. From that link: > We made these changes as part of our continuing effort to make Go a great language for building production systems. We studied many third-party web frameworks, extracted what we felt were the most used features, and integrated them into net/http. Then we validated our choices and improved our design by collaborating with the community in a GitHub discussion and a proposal issue. Adding these features to the standard library means one fewer dependency for many projects. But third-party web frameworks remain a fine choice for current users or programs with advanced routing needs. [1]: https://go.dev/blog/routing-enhancements https://go.dev/blog/routing-enhancements
- nacozarina 7mo agoIt wasn’t ready then and it isn’t ready now. It was always an agenda masquerading as a solution.
- _davide_ 7mo agoMost examples and presented issues would not compile or be a real issue... I stopped reading midway