7 ms·
It seems that Rust just keeps getting more and more features. Do people generally feel like the more features the better the language? I'm personally of the op
by Philip-J-Fry 6y ago
It seems that Rust just keeps getting more and more features.
Do people generally feel like the more features the better the language? I'm personally of the opinion that less is more.
Is this a pain point at the moment for Rust devs? Do you feel like the code you write is the same code 99% of other Rust developers would write to solve the same problem? Or is there actually a really large variety of styles?
- sfvisser 6y agoI'm not sure you can only see this in the light of 'more features'. From a more fundamental perspective GATs just lift a restriction. Rust already has generics and Rust already has associated types. Previously you needed to know/remember those features cannot interact for some reason, now they can. To me this is in a sense 'less' and not 'more'. So, a matter of perspective I guess.
- Philip-J-Fry 6y agoAh okay, that makes sense. Thanks
- The_rationalist 6y agoBut why have associated types in the first place since they have the same expressive power as generics? https://stackoverflow.com/questions/33647337/what-is-the-difference-between-generics-and-associated-types https://stackoverflow.com/questions/33647337/what-is-the-dif...
- Twisol 6y agoEven in Java, I'm rather sick of passing around types with `<X, Y, ?, ?>` with two honest-to-goodness generic types and two actually-chosen-by-the-implementation generic types. There's no good way in Java to hide those visible wildcards without making a wrapper class that only exposes the meaningful ones. Associated types make the distinction far clearer. They better capture the qualitative distinction that "you can choose these, I get to choose those". Also, associated types are inherently "functionally dependent" in the sense of Haskell's multi-parameter type classes. If you have a trait `F<X, Y>` with an associated type Z, you know that given X and Y, Z is fixed. Without associated types, `F<X, Y, Z>` could have multiple legal implementations for the same X, Y and different Z. This is extremely meaningful in languages like Haskell and Rust which implicitly thread around the trait methods. Typeclasses in Haskell can be imagined as describing concrete dictionaries of functions over the given types. Languages like Java (manually) and Scala (automatically, using "implicit") reify these typeclasses as dictionaries that are threaded through functions as extra parameters. You can often define multiple implementations of a given trait for the same types, and you get to choose which implementation to pass along. Rust and Haskell assert that traits have a single implementation for a given batch of types, and they automatically look up the correct implementation given the types you've specified. These "functional dependencies" mean that, in my example of `F<X, Y>` with associated type Z, it's sufficient to state what X and Y are to find the right implementation of F -- Z doesn't contribute to the lookup. If you don't have associated types, you could have multiple implementations that simply vary on Z, so you have to tell the compiler explicitly which one to use (by stating what Z is).
- ivanbakel 6y agoBecause they don't. Your link even has an answer that highlights one way which they differ in expressivity - namely, that associated types don't appear in the instance head, so they can produce orphan instances where generics would not. Consider the example of Rust's Iterator trait[0]. It has an associated type for the iteration item. It could have a generic type argument instead, but the two implementations would be different: 1. The associated type is a direct consequence of the instance head. That means that, for the type that Iterator is being implemented on, the associated type is known entirely from that type. If you have an iterable value, you know it only produces one kind of iterator item. 2. As a result, the associated-type version can only be implemented at most once. The generic version would be implementable any number of times, for any number of choices of type argument - and as a result, you would have to specify that you are iterating over an iterator with a particular choice of iterator item type, every time you iterate. [0]: https://doc.rust-lang.org/std/iter/trait.Iterator.html
- Thiez 6y agoBack in the days before associated types you could implement `Deref` multiple times for the same type. That was fun.
- ivanbakel 6y agoYou can still do similarly hellish impls with `Index` - especially if you mix `Deref` into it. IIRC, the compiler will perform automatic recursive derefs when you try to use the indexing operator `[]`, until it finds a deref target which matches the index you're using. I doubt anything relies on that search behaviour outside of the compiler code itself. I imagine abuse of specialisation could also increase the confusion, if you were so inclined.
- karavelov 6y agoType parameters are decided by the use of the type. The associated types are decided by the implementation of the type. Type params are input, associate type are output.
- chombier 6y agoLooking at the replies the difference was not entirely clear to me, but the book has a nice explanation https://doc.rust-lang.org/stable/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types https://doc.rust-lang.org/stable/book/ch19-03-advanced-trait... > "The difference is that when using generics, as in Listing 19-13, we must annotate the types in each implementation; because we can also implement Iterator<String> for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. When we use the next method on Counter, we would have to provide type annotations to indicate which implementation of Iterator we want to use."
- dathinab 6y agoThey don't have the exact same expressive power. If you have a `trait A { type B; ... }` then for any type T you can implement one specific A with one specific B. But if you have `trait A<B> { ... }` then you can have a different implementation of A for every disjoint type B. Then if you have `trait A<B> { type C; ... }` you can have one specific C for every possible disjoint B. So with associated types you can express additional constraints/semantics which you can not express with only "classic" generics. Furthermore the compiler can rely on this constraints for e.g. type inference.
- brundolf 6y agoCame here to say this. Adding new syntaxes is something I have mixed feelings about at this stage, but opening up what you can do with existing ones (and, indeed, removing the complexity of remembering special-cases of what's not allowed) is a strict win in my book.
- danappelxx 6y agoI would say, with the exception of the async split, no. The biggest pain is still the learning/skill curve, it always feels (to me) like there's a language feature I'm not taking advantage of that would make a piece of code more "rusty". Not different in style necessarily, just more elegant.
- caspper69 6y agoI can relate to this 100%. I'm working though a Rust course right now, and while my code works fine (once it compiles), I always see the reference implementations of the code, and it's like 2 lines of filter/map/collect, and voila. Meanwhile, my 8 line frankenfunction looks like the Charlie Brown Christmas tree.
- stevedonovan 6y agoRemember, using a loop is not a sin! (And I would hate us to get to the point where loops are automatically considered kindergarten code)
- s-combinator 6y agoUsing a loop is not a sin, but it is often counterproductively over-prescriptive. A loop says, 'do these things in this order in a single thread'. If the loop body is an effect-free function, then the order doesn't matter, why not use a map to let the compiler or run-time decide how many cores/containers/botnets to throw at the problem? Similarly, if the loop combines the results of its iterations in a unital and associative way, why not use a fold/reduce in order to get as much parallelism as possible for free? Sure, your compiler could try to do some fancy static analysis to try to figure out whether the loop you've written is equivalent to a more efficient program, and if so, replace it for you, but that's a lot of work for the compiler writer and inherently limited: we're a long way away from compilers being able to guess properties of programs and synthesize proofs for them. Sometimes avoiding loops is both conceptually clearer and practically more efficient.
- 6y ago
- johncolanduoni 6y agoGATs are removing some restrictions on the existing facilities of traits, and less creating totally new features. To be clear figuring out the subtleties of exactly how this is verified and edge cases with the type system is non-trivial, but as a developer this extends the set of types that “just work” as associated types. Crucially, it allows avoiding some weird hacks to get the borrow checker happy with non-static lifetime associated types in many situations.
- roblabla 6y agoRust hasn't fundamentally changed all that much in the end, the only notable exception being `async`, and I feel like that's a pretty big success so far, being used far and wide from web servers to embedded task scheduling. I do not currently feel like there are many different solutions to a problem - at least, not in the sense that the solutions only differ by style. Of course each problems have different solutions that are different points in the performance spectrum (static vs dynamic dispatch, etc...). All the big features that are coming (GAT, const generics), all feel like lifting restrictions that will remove the need for hacks. To take const generics as an example, it will obsolete the need for `typenum` and `generic-array`, giving us a more consistent style. Less type-hacking is a good thing. I do agree with the sentiment: Rust is already a big language, and has to be careful when weighting new language features to make sure they don't spend too much mental overhead. So far, I feel like they've done a good job at it.
- Blikkentrekker 6y agoRust is indeed starting to enter the territory where features are added on top of features as a way to deal with that there were too many restrictions at the start. It is starting to become a pain to me, but I'm glad that apparently there is some semblance of h.k.t.'s now, which was very much wanted.
- jokethrowaway 6y agoI think rust does a good job at keeping things simple in the std. Where I get some analysis paralysis is in getting one library over another one. Eg. I'd love to have a std lib recommended way of doing async and task execution with a thread pool eg. Async-std vs Tokyo Vs Rayon. They're all individually great pieces of software but none of them feel complete and I always reach out for something that is not there. To be fair I don't think this is rust specific, I think this is just the bane of OSS solutions with unpaid maintainers. Sure, I could contribute something or fork them but it takes time. I'd prefer to just buy in into a solution, pay a monthly fee and know that I'll get a full featured solution with a well paid maintainer.
- nicoburns 6y ago> They're all individually great pieces of software but none of them feel complete and I always reach out for something that is not there. I'm not convinced that the libs being in std would help you with this issue. Instead you'd probably have the same incompleteness with a whole bunch of the functionality only available in nightly rust because of the stability contract implied by std. Picking either tokio or async-std (flip a coin if you can't decide) and using their recommended way doing a given async task isn't really all that different from doing the same with a hypothetical std::async.
- swsieber 6y agoI would lament if everything were in the std at this point. There will be another wave of change after GATs stabilize. That said, there are some traits being standardized and moving to the std, which should be great for interop.
- f154hfds 6y agoIs there any work on creating feature-based sub-standard libraries that are aggregations of popular (and robust) crates but tying all of their versions together? For example, I'd like to put in my Cargo.toml: sstd = { version = "1.0", features = ["clap", "serde", "rand"] } Where the sstd crate essentially has a whole mess of crates bundled together ensuring only one version is used for each individual dependency? Perhaps this is a question best asked elsewhere - your comment just brought it once again to my mind.
- cerebellum42 6y agoI mean, I've run into situations where GATs would have been a good solution before and had to find a clumsier workaround using boxing or other solutions until now. So I don't really know what to say to your initial question, except "this feature is useful", and that the question (and especially your answer to it) may be a bit overly reductionist.
- CryZe 6y agoI'd argue that for the most part Rust is not really gaining more features. Instead rough edges are being polished. GATs for example sound like a fancy feature, but in reality, it just allows you to make your associated types generic, just like everywhere else where you can define a type. A Rust beginner would expect this to work by default, and actively has to learn that it currently does not work. By enabling GATs new learners will have to learn less, not more. Most other upcoming features have been planned for a long time, such as const generics and allowing more things in const fn (which yet again, you currently have to learn the limitations, which are constantly being lifted). I'd expect the development of new features to rapidly slow down once those are finished. In fact they are already struggling to justify a Rust 2021 edition, as there's not a whole lot of features that would even require an edition.
- steveklabnik 6y agoLack of monads is not a super huge pain point, but GATs do solve a big pain point: they're needed in order to have async methods. To be clear here, you don’t need to write any of this stuff in the blog post to write an async method, but the desugaring of an async method involves GATs, so the language has to have them.
- dathinab 6y agoGAT limited to lifetimes would already be good enough for me. What I need is something similar to: trait AsyncCallback { type Input<'a>: 'a; type Output<'a>: Future + 'a fn call<'a>(self, Self::Input<'a>) -> Self::Output<'a>; }
- steveklabnik 6y agoLifetimes are already generics, so it is harder to add "lifetime only GATs" than it is GATs, in my understanding.
- dathinab 6y agoLifetimes do not influence code generation (besides potential specialization on 'static, which I'm not sure if it's a thing). But besides that the amount work is indeed very close by each other. Which is why there is currently no plan to first have lifetime only GATs stable as far as I know.
- steveklabnik 6y agoYou're not wrong that they don't influence codegen, but I don't know why you're bringing that up. I don't see the connection. You cannot specialize on lifetimes, it is not sound.
- dathinab 6y agoBecause to have lifetime only GATs you need to change the syntax and liftime checker but once you checked the type you can treat it ignoring lifetimes and in turn ignoring the GAT aspect. But then if you did so you probably already implemented most/all of the code to typecheck non lifetime GATs and then doing the rest proper is probably not to hard either and might be not harder then doing liftime only GATs. > You cannot specialize on lifetimes, it is not sound. Good to know, given that specialization was stuck in nightly in the last years I didn't use it and in turn didn't look to much into it.
- bluGill 6y agoYes, more features make a better language. I need those features to abstract my software so I can wrap my mind around all 15 million lines of code I have to deal with (that is both too many for one human to understand, and yet I know many will respond that they work on much larger system!). We don't need new languages without features: it has already been proven that you only need exactly one feature in your programming language to write any program possible. Look up single instruction languages for proof that it is possible and such single features languages exist. What we need are languages with many features that are easy to use to create complex programs that are still easy to understand. C++ is a very powerful language, but the various features do not fit together well and so even experts tend not to know how to use everything and so the whole suffers. Most languages since then have attempted to take the [all or some subset of] the features of C++ to make a better language. (when any other language comes up with a good feature C++ copies it so is often getting credit for ideas that actually come from elsewhere - possibly better) When someone is saying they want less features they mean one of the following: "I don't know how to use some feature and I've gotten this far so it must be useless". "That feature is useless for my domain so I don't want to pay the compromises required to allow it". "That feature is too often abused to write bad code and so it isn't worth having". "That features is neat but it makes the whole language ugly and so it isn't worth having". There is probably something I missed but you get the idea. Some of the above are logical fallacies, the others are compromises that apply to a particular problem. None of them are universal truths for problems.
- Thaxll 6y ago"Yes, more features make a better language" At the pace of what Rust is adding feature, it's the next C++ in the next 5-10years.
- steveklabnik 6y agoThe pace has already slowed down significantly, and will continue to do so. The RFC for GATs was opened in 2016. It's still not clear when this is hitting stable Rust yet.
- 6y ago
- swsieber 6y agoThe reason Rust has so many features is to manage the complexity lifetimes introduce in a safe, zero-cost way. The big recent additions and outstanding work is trying to fill gaps there. * async - you can write futures without async, but async lets you do more because of how it resolves lifetime issues you'd normally get when using combinators. The other route without async would be to manually write unsafe state machines. Yuck. * GATs - these are required to be able to have native support for async methods in traits (interfaces) without needing to box the return value (ie. have statically dispatched ... not sure what the term is, but it lets it be online too, IIRC; I might be wrong about the performance ipact, but ergonomic pain is real) * const generics - this gives better support for stuff like arrays, since really what it's doing is stuff like supporting array length as a generic type parameter instead of a special case.
- adamnemecek 6y agoI would not call them features per se but generalizations of existing features.
- dathinab 6y ago> Is this a pain point at the moment for Rust devs? No, in rust as long as you don't use a feature you don't need to know about it. > Or is there actually a really large variety of styles? There are official(-ish?) style guidelines. So while there are a variety of styles most code uses mostly the same style. > I'm personally of the opinion that less is more. Sure, but you normally want to have a "complete well rounded language". For this in rust you need GAT, but you don't need HKT, Functors, Monades or similar. I hope there will never be a Monade trait in std, it would add a lot of mostly hardly useful complexity we really don't need and would mostly be used for IMHO mostly useless over abstraction. But GAT are really needed. E.g. to properly work with certain async use cases (through for this you only need GAT for lifetimes, not arbitrary type. In my experience GAT limited to lifetimes cover 80+% of the cases where you really really need GAT).
- alvarelle 6y ago> in rust as long as you don't use a feature you don't need to know about it. Is that not the same in C++ and in most languages? ... until someone else in your team uses it in your code base. And then you need to know about it in Rust as well.
- dathinab 6y agoThe difference is that with rusts safety you can to some degree "try thinks out". Which doesn't work with C++ as you might have hidden UB. Also C++ has a bunch of "hidden" features and unexpected interactions with other features and UB like e.g. forward guarantee because of which `while(1);` is UB.
- Philip-J-Fry 6y ago> No, in rust as long as you don't use a feature you don't need to know about it. I don't think that's a particularly good argument, since it's true for all languages. Even if you don't use a feature, you will use and read code written by others all the time and understanding that code can be pretty important.
- 6y ago
- staticassertion 6y agoAs someone who uses Rust, "more features" is not a win. Most of the time it rarely impacts me - especially in Rust it seems that a "Box" here or there can fix code at the potential cost of performance. But there are cases, often for libraries, where features can be quite helpful and make me faster. If a feature is basically "make code that should have already worked actually work", that's a huge win. A lot of Rust features tend to be that. It's like "OK, we have 'impl trait', but it only works in some places. Let's let it work in more places." So,yeah, sure, that's a new feature - impl trait in new positions - but it's really just supporting code that many people would have expected to work. I see this GAT feature similarly. It wouldn't be hard to "accidentally" try to have a generic associated type - in fact, I have probably run into this myself. And so GAT isn't really adding more complexity to me, it's just unlocking code that I would have already written. Similarly, with GAT, we can unlock 'async' in traits. I already know 'async', I know it on functions and methods. So this is, again, just making an existing feature more consistent.
- eggy 6y agoI have decided to learn Rust to replace C (for me) instead of Zig, which I like, but Rust has more support for my learning in terms of community and documentation. I plan to return to Zig when it matures a bit more. I get my dose of FP when I program in J or APL. I use them for very mathy and fun things, and I don't even think about FP; they are FP. I am working my way through an 11-page paper that implements a CNN in APL in 10 functions that amounts to 10 lines of APL, 1 function per line [1]. Amazing and simpler than my stabs at Haskell and Idris or Scala and F#. [1] https://dl.acm.org/doi/pdf/10.1145/3315454.3329960?download=false https://dl.acm.org/doi/pdf/10.1145/3315454.3329960?download=...
- joel_ms 6y ago>I'm personally of the opinion that less is more. I used to have this position as well, writing everything in either assembly or pure lambda calculus, to avoid all these pesky higher level language features.