8 ms·
I think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic type
by sedatk 2y ago
I think the article makes a good point, but the actual example isn’t Rust’s worst, not even close. It gets really hard to follow code when multiple generic types are combined with lifetime markers. Then it truly becomes a mess.
- hckr1292 2y agoAgree about the example! I can't tell if this article is tongue-in-cheek or earnest. I'm unclear on the point the author is trying to make.
- tcfhgj 2y agoThe author explains it in the first sentence, i.e. not the syntax of lifetimes may be your problem, but the feature itself
- zarzavat 2y agoMy reading is: people use Rust because it’s fast but then they complain about the semantics that make it fast. In other words, be careful what you wish for. Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. However, once you deviate from the goal of “as fast as possible”, then you have to choose which parts you want to sacrifice speed for productivity. Like Excel, everybody agrees that Rust is too complicated but nobody can agree on which 10% to remove.
- d_tr 2y ago> Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. D maybe? D and Rust are the two languages which come to mind when I think about "possible C++ replacements".
- cdogl 2y agoWhen GP said “most”, I interpreted it more broadly. Most applications simply do not require the guarantees of a non-GC language. When you expand that horizon, list of contenders becomes considerably larger - even when restricted to statically typed languages.
- zarzavat 2y agoYes for example many Python users switched to Go, a native code GC language, and are satisfied with the performance. There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations.
- dwattttt 2y ago> There’s also the middle ground of Swift’s memory management which uses compiler-elided refcounting - i.e. the compiler detects when a count goes up then down again and removes those operations. In the face of threading that's not a safe optimisation; if another thread decrements the refcount inbetween those two removed operations, boom. The compiler will have to track every variable that crosses threads or something. EDIT: spelling
- sk11001 2y ago> people use Rust because it’s fast but then they complain about the semantics that make it fast. I don't think most people use Rust because it's fast - fast is nice but Rust is being thrown at a bunch of use cases (e.g. backend services and APIs) for which it replaces "slower" garbage collected languages (the language being faster doesn't always make the overall product/service faster but that's a separate question). What Rust gives you is a viable potential alternative to C and C++ in places where you absolutely can't have a GC language, and that's a huge deal, the problems and confusion start when people try to use Rust for everything. > everybody agrees that Rust is too complicated I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion. > Most people would probably be better served by a language that was a tiny bit slower but had better developer productivity. True, and such languages already exist and are widely used, Rust doesn't need to fit that use case.
- zerodensity 2y ago> I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion. Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros)
- swiftcoder 2y ago> Personally I feel it's not complicated enough. Where is my function overloading, variadic templates and usable compile time reflection? (Sure you can sometimes use macros but ew macros) Indeed. Rust is really crying out for a real CTFE implementation + richer macros to replace the mess that is procmacros (I really don't want to have to run an arbitrary external binary with full system access just to manipulate the AST...)
- zarzavat 2y ago> I don't think this is true either - a large part of the Rust community seem to think that it's as complicated as it needs to be. As a beginner/outsider, I found it kind of cumbersome to get started with, but that's certainly not everyone's opinion With any language there’s an active part of the community and then there’s the “dark matter” of people who use the language but are not actively involved in shaping its direction, forums or subreddits, etc. Of course the people who are actively involved are likely to be of the opinion that all the complexity is necessary, but I doubt that applies to the broader Rust userbase.
- LoganDark 2y agoI always, always forget what `'a: 'b` means, because I remember it always being the opposite of what I think it is, but memorizing that obviously doesn't work because then it will just flip again the next time. It's so annoying.
- nrabulinski 2y agoI always describe it to myself this way - T: Foo means T is a superset of Foo (because it at least implements Foo but most likely more) thus 'a: 'b means 'a is at least as wide as 'b, and possibly wider
- delifue 2y agoIn lifetime, subtype means longer lifetime (it's unintuitive). 'a : 'b means 'a is a subtype of 'b, which contains 'b and can be longer. Rust can improve this by introducing syntax like `'a contains 'b`
- runiq 2y agoIf you think of it as 'a implements b', it makes sense for both lifetimes and (other) subtypes: If lifetime `a` implements `b`, it is obviously valid for `b` (and maybe longer).
- LoganDark 2y agoI always do `<T, U extends T, V extends U>` etc for generics, but for lifetimes it's `<'a: 'b, 'b: 'c, 'c>` which always trips me up...
- lifthrasiir 2y agoIt was a good signal to me that you are overthinking into the architecture if that is really required. Rust makes something pretty much impossible in C/C++ possible, but not necessarily easy, and that would be one such example.
- LoganDark 2y ago> It was a good signal to me that you are overthinking into the architecture if that is really required. Sure, maybe I don't need to statically guarantee the correct execution of code that could easily just throw at runtime instead, but it sure is a fun hobby.
- namjh 2y agoIMHO the mentioned examples of complexity like multiple type variables and lifetimes with bounds are for who "really" wants compile-time contracts. These are mostly opt-in so higher level use cases(like writing backend business logics) should not care about that, just wrapping everything with Boxes and Arcs. Of course Rust is not perfect; there is some 'leakages' of low level aspects to high level like async caveats(recursion, pinning, etc.). I'm not sure how these can be avoided. Maybe just trial-and-errors for all..?
- Hamuko 2y agoI do remember the compiler constantly suggesting lifetimes to me as a newcomer to the language, so it didn't really feel that opt-in. Quite a lot of the suggestions also started to look like someone poured alphabet soup all over the code.
- scotty79 2y agoThat's mostly because borrows are a curiosity of Rust that newcommers are quickly introduced to while they are mostly just a perfomance gimmick. If you come to rust from high level language you can just do everything with Rc and cloning. It's still hard because Rust, in opposition to every pipular language, is a value oriented language. But at least you won't have much contact with alphabet soup.
- namjh 2y agoThat's because the code triggering compilation error is using reference. If you use Rc or Arc (which pays runtime cost) there should be no lifetime at all. Albeit I admit there somewhat exists a community sentiment like "if you use Rust, you should maximize its zero cost abstraction feature so lifetime is good and generics good", and my (minor) opinion is that, it's not always true to all users of Rust. And the clumsy Arc<Mutex<Something<TheOtherThing>>> makes users feel bad about using runtime cost paid types. Maybe we should introduce easy Rust dialect which transpiles into Rc/Clone everywhere but I doubt it's trivial to transpile.
- dmurray 2y ago
- yulaow 2y agothrow in some async too and I really lose myself most of the times
- WhyNotHugo 2y agoHere's a nice example of a Trait that has async functions: fn list_items<'life0, 'life1, 'async_trait>( &'life0 self, collection_href: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<ItemRef>, Error>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, Rendered docs: https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorage/base/trait.Storage.html#tymethod.list_items https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag... Source: https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta0/item/vstorage/src/base.rs#L79 https://git.sr.ht/~whynothugo/vdirsyncer-rs/tree/v2.0.0-beta...
- virtualritz 2y ago> https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorage/base/trait.Storage.html#tymethod.list_items https://mirror.whynothugo.nl/vdirsyncer/v2.0.0-beta0/vstorag... Unrelated to parent but maybe relevant to you: Rust API naming guidelines say no `get_` prefix on getters. https://rust-lang.github.io/api-guidelines/naming.html#getter-names-follow-rust-convention-c-getter https://rust-lang.github.io/api-guidelines/naming.html#gette...
- mrweasel 2y agoThe use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language. It's made worse by the fact that you can still use '...' as s character literal. Not only is is incredibly ugly it's also rather confusing, but it fits well into how I view Rust, complicated for the sake of making the developers look smart. It wouldn't fit the syntax of the language obviously, but why not simply have the developer prefix the variable with the keyword "lifetime", rather than assigning a symbol. It seems a little like starting functions in Go with a upper case letter to export them, dude just give us an export keyword, it's fine.
- the_mitsuhiko 2y ago> The use of ' as a symbol that has any meaning on it's own has got to be one of the most stupid choices I've seen in a language Can you clarify why you have that opinion? What would your syntax suggestion have been?
- nazka 2y agoThat's why I am a huge fan of Rust but at the same time at the end of the day all I want is the features of the language that Rust has minus the memory management and a GC. That would be my dream language. If only ReasonML/Rescript were more popular... Or I guess Elixir
- neonsunset 2y agoIt's not necessarily identical to what you are looking for per se but a mix of C# and F# will be the closest overall to Rust in terms of performance, access to systems programming features, language expressiveness and tooling experience. Cons are: - C# has OOP (you don't have to use it heavily) - No Hindler-Milner type inference in C#, nested generic arguments may need to be specified by hand - Smaller amount of supported targets by CoreCLR: x86, x86_64, arm, arm64 for ISAs and Linux, Windows, macOS and FreeBSD for OSes. NativeAOT-based support is experimentally available on iOS, and is undergoing further work. As you can imagine, LLVM targets absolutely everything under the sun and above it too. no-std story in Rust is first-class. C# has bflat and zerosharp but they are niche. - In C#, type unions will only be available in one of the coming versions. F# to the rescue - Error handling is a combination of exceptions and e.g. int.TryParse patterns, there is no implicit returns with ? like in Rust - Lack of associated types and the types own their interface implementations unlike traits which you can introduce without control over the source. This results in having to implement wrapper types (even if they are structs) if you want to modify their interface implementations - You only control shallow immutability - readonly struct will not prohibit modification of the contents of a Dictionary<K, V> that it holds - async/await is more expensive - Big popular libraries often have features or implementation incompatible or not guaranteed to work with native compilation via NativeAOT - Object reference nullability (e.g. 'T?') happens at the level of static analysis i.e. does not participate in type system the same way Option<T> does in Rust Pros are: - Has FP features like in Rust: high order functions, pattern matching (match val -> val switch { , also 'is'), records, tuples, deconstruction - Fast to compile and run, AOT not so fast to compile but tolerable - If you know how to use Cargo, you know how to use .NET CLI: cargo init -> dotnet new {console, classlib, etc.}, cargo run -> dotnet run, cargo build -> dotnet build/publish, cargo add {package} -> dotnet add package {package} - Monomorphized structs generics with the same zero-cost abstraction assurance like in Rust, &mut T -> ref T, &T -> ref readonly T, sometimes in T but with caveats - Box/Arc<T> -> class or record, Arc<Mutex<T>> -> class + lock (instance) { ... } - Easy to use async/await but without ever having to deal with borrow checker and misuse-resistant auto-scaling threadpool. Task<T>s are hot started. Simply call two task-returning network calls and await each one when you need to. They will run in background in parallel while you do so. While they are more expensive than in Rust, you can still do massive concurrency and spawn 1M of them if you want to - Built-in Rayon - Parallel.For and PLINQ, there is Channel<T> too, you can e.g. 'await foreach (var msg in chReader) { ... }' - Iterator expressions -> LINQ, seq.filter(...).map(...).collect() -> seq.Where(...).Select(...).ToArray(), unfortunately come with fixed cost but improve in each version - Rust slice that wraps arbitrary memory -> Span<T>, e.g. can write the same fast idiomatic text parsing on top of them quite easily - Stupid fast span routines like .IndexOf, .Count, .Fill, .CopyTo which use up to AVX512 - Compiler can devirtualize what in Rust is Box<dyn Trait> - Can make small native or relatively small JIT single-file executables that don't require users to install runtime - Rich and fast FFI in both directions, can statically link into Rust, can statically link Rust components into itself (relatively new and advanced) - Great tooling, Rider is very good, VSCode + base C# extension about as good as rust-analyzer - Controversial but powerful runtime reflection and type introspection capability, can be used in a very dynamic way with JIT and compile additional code on the fly - A bit easier to contribute to, depending on area owner and project (runtime, roslyn, aspnetcore, ...) - CoreLib has full-blown portable SIMD API that is years ahead of portable-simd initiative in Rust Because I occasionally use Rust and prefer its formatting choices, I carry this .editorconfig around: https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d123de00d https://gist.github.com/neon-sunset/c78174b0ba933d61fb66b54d... to make formatting terser and more similar. Try it out if K&R and `I` prefix on interfaces annoy you.