Table of Contents for Programming Languages: a survey
Rust
"Rust combines low-level control over performance with high-level convenience and safety guarantees. Better yet, it achieves these goals without requiring a garbage collector or runtime, making it possible to use Rust libraries as a “drop-in replacement” for C....What makes Rust different from other languages is its type system, which represents a refinement and codification of “best practices” that have been hammered out by generations of C and C++ programmers." -- [1]
"Rust is a language that allows you to build high level abstractions, but without giving up low-level control – that is, control of how data is represented in memory, control of which threading model you want to use etc. Rust is a language that can usually detect, during compilation, the worst parallelism and memory management errors (such as accessing data on different threads without synchronization, or using data after they have been deallocated), but gives you a hatch escape in the case you really know what you’re doing. Rust is a language that, because it has no runtime, can be used to integrate with any runtime; you can write a native extension in Rust that is called by a program node.js, or by a python program, or by a program in ruby, lua etc. and, however, you can script a program in Rust using these languages." -- Elias Gabriel Amaral da Silva
"...what Rust represents:
Pros
Tutorials and books
- http://doc.rust-lang.org/master/intro.html
- http://doc.rust-lang.org/book
- https://cheats.rs/
- https://docs.google.com/presentation/d/1q-c7UAyrUlM-eZyTo1pd8SZ0qwA_wYxmPZVOQkoDmH4/edit#slide=id.p
- http://science.raphael.poss.name/rust-for-functional-programmers.html
- http://learnxinyminutes.com/docs/rust/
- http://nercury.github.io/rust/guide/2015/01/19/ownership.html
- http://words.steveklabnik.com/a-new-introduction-to-rust
- https://news.ycombinator.com/item?id=9121940
- http://doc.rust-lang.org/book/pointers.html
- http://rustbyexample.com/
- http://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html
- http://manishearth.github.io/blog/2015/05/03/where-rust-really-shines/ : points out that in Rust, if a library is exposing an internal list of things through an API, you can safely give it to the client as either an immutable copy, or as a pointer to an object instace which wraps accesses to the data, or using a handle (double indirection), just as a pointer to the actual list; only the last two aare zero-overhead; in C++ the last option, return a pointer, would be unsafe, because what if the API eventually adds so many things to the pointer that it must reallocate and move it, but the client holds onto the pointer and uses it after it has been moved? The post then goes into a detailed example
- http://blog.burntsushi.net/rust-error-handling/
- https://doc.rust-lang.org/book/
- https://rust-lang.github.io/book/second-edition/
- https://rust-lang.github.io/book/first-edition/
- http://lucumr.pocoo.org/2015/5/27/rust-for-pythonistas/
- https://doc.rust-lang.org/nightly/adv-book/
- https://doc.rust-lang.org/stable/nomicon/ and https://doc.rust-lang.org/nightly/nomicon/README.html
- https://github.com/carols10cents/rust-out-your-c-talk/blob/master/slides-with-speaker-notes.pdf
- http://blog.adamperry.me/rust/2016/06/11/baby-steps-porting-musl-to-rust/
- https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html
- https://github.com/EbTech/rust-algorithms
- A quick high-level summary of Rust's memory management is found in [2], subsection "The bathroom at the end of the universe"
- http://cglab.ca/~abeinges/blah/too-many-lists/book/
- http://www.goldsborough.me/rust/web/tutorial/2018/01/20/17-01-11-writing_a_microservice_in_rust/
- http://dtrace.org/blogs/ahl/2015/06/22/first-rust-program-pain/ (not really a tutorial)
- https://github.com/crazymykl/rust-koans
- https://github.com/rustlings/rustlings
- https://rcoh.me/posts/rust-linked-list-basically-impossible/
- https://rust-lang-nursery.github.io/rust-cookbook/
- https://cheats.rs/
- Learn Rust With Entirely Too Many Linked Lists
- https://tourofrust.com/00_en.html
- https://github.com/Dhghomon/easy_rust
- https://fasterthanli.me/blog/2020/a-half-hour-to-learn-rust/
- https://christine.website/blog/TLDR-rust-2020-09-19
- https://fasterthanli.me/articles/a-half-hour-to-learn-rust
- https://os.phil-opp.com/async-await/
- https://fasterthanli.me/articles/understanding-rust-futures-by-going-way-too-deep
- https://nora.codes/post/its-time-to-get-hyped-about-const-generics-in-rust/
- https://dystroy.org/blog/how-not-to-learn-rust/
- https://docs.rs/dtolnay/0.0.9/dtolnay/macro._02__reference_types.html
- https://itsallaboutthebit.com/arc-mutex/
- https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md
- https://fasterthanli.me/articles/the-curse-of-strong-typing
- https://marabos.nl/atomics/
- https://google.github.io/comprehensive-rust/
- https://fiberplane.com/blog/getting-past-ampersand-driven-development-in-rust
- https://lobste.rs/s/mfd2jr/getting_past_ampersand_driven#c_mzgytc
- https://github.com/BurntSushi/rsc-regexp
Best practices and tips, libraries, tools
fn double_or_multiply(x: i32, y: Option<i32>, double: bool) -> Result<i32> { if double { if y.is_some() { return Err("Y should not be set"); } x * 2 } else { if y.is_none() { return Err("Y should be set"); } x * y.unwrap() } }
Yes, I know its a completely contrived example, but I’m sure you’re familiar with that kind of pattern in code. The issue is that this is using the shallow aspects of Rust’s type system – you end up paying for all of Rust but only reaping the benefits of 10% of it. Compare that to what you could do by leveraging the type system fully:
enum OpKind? { Double(x), Multiply(x, y), }
fn double_or_multiply(input: OpKind?) -> i32 { match input { Double(x) => x * 2, Multiply(x, y) => x * y, } } " -- [5]
Old/probably out of date but otherwise look good:
Features
- zero-cost abstractions (this and the next 8 features from [6])
- move semantics
- guaranteed memory safety
- threads without data races
- trait-based generics
- pattern matching
- type inference
- minimal runtime
- efficient C bindings
- futures and streams
- you can create functions with a C ABI using 'extern "C"' [7] (as of this writing, Rust does not currently otherwise have a stable ABI [8])
- "
concurrency features
" Rust's ownership rules are as follows:
Each value in Rust has a variable that's called its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped / released.
Then there are rules about references (think intelligent pointers) to owned values:
At any given time, you can have either one mutable reference or any number of immutable references.
References must always be valid.
Put together, these rules say:
There is only a single canonical owner of any given value at any given time. The owner automatically releases/frees the value when it is no longer needed (just like a garbage collected language does when the reference count goes to 0).
If there are references to an owned value, that reference must be valid (the owned value hasn't been dropped/released) and you can only have either multiple readers or a single writer (not e.g. a reader and a writer).
The implications of these rules on the behavior of Rust code are significant:
Use after free isn't something you have to worry about because references can't point to dropped/released values.
Buffer underruns, overflows, and other illegal memory access can't exist because references must be valid and point to an owned value / memory range.
Memory level data races are prevented because the single writer or multiple readers rule prevents concurrent reading and writing. (An assertion here is any guards - like locks and mutexes - have appropriate barriers/fences in place to ensure correct behavior in multi-threaded contexts. The ones in the standard library should.)
" -- [9]
great explanation of ADTs and their use for "Encoding and Enforcing Invariants in the Type System", with an example, in https://gregoryszorc.com/blog/2021/04/13/rust-is-for-professionals/ section "Encoding and Enforcing Invariants in the Type System".
" some of my favorite (other) parts about the language, in no particular order.
Native types in Rust are intelligently named: i32, u32, f32, f64, and so on. These are, indisputably, the correct names for native types.
Destructuring assignment: awesome. All languages which don’t have it should adopt it. Simple example:
let ((a, b), c) = ((1, 2), 3); a, b, and c are now bound
let (d, (e, f)) = ((4, 5), 6) compile-time error: mismatched types
More complex example:
What is a rectangle? struct Rectangle { origin: (u32, u32), size: (u32, u32) }
Create an instance let r1 = Rect { origin: (1, 2), size: (6, 5) };
Now use destructuring assignment let Rect { origin: (x, y), size: (width, height) } = r1; x, y, width, and height are now bound
The match keyword: also awesome, basically a switch with destructuring assignment.
Like if, match is an expression, not a statement, so it can be used as an rvalue. But unlike if, match doesn’t suffer from the phantom-else problem. The compiler uses type-checking to guarantee that the match will match something — or speaking more precisely, the compiler will complain and error out if a no-match situation is possible. (You can always use a bare underscore _ as a catchall expression.)
The match keyword is an excellent language feature, but there are a couple of shortcomings that prevent my full enjoyment of it. The first is that the only way to express equality in the destructured assignments is to use guards. That is, you can’t do this:
match (1, 2, 3) { (a, a, a) => “equal!”, _ => “not equal!”, }
Instead, you have to do this:
match (1, 2, 3) { (a, b, c) if a == b && b == c => “equal!”, _ => “not equal!”, }
Erlang allows the former pattern, which makes for much more succinct code than requiring separate assignments for things that end up being the same anyway. It would be handy if Rust offered a similar facility.
My second qualm with Mr. Match Keyword is the way the compiler determines completeness. I said before “the compiler will complain and error out if a no-match situation is possible,” but it would be better if that statement read if and only if. Rust uses something called Algebraic Data Types to analyze the match patterns, which sounds fancy and I only sort-of understand it. But in its analysis, the compiler only looks at types and discrete enumerations; it cannot, for example, tell whether every possible integer value has been considered. This construction, for instance, results in a compiler error:
match 100 { y if y > 0 => “positive”, y if y == 0 => “zero”, y if y < 0 => “negative”, };
The pattern list looks pretty exhaustive to me, but Rust wouldn’t know it. I’m that sure someone who is versed in type theory will send me an email explain how what I want is impossible unless P=NP, or something like that, but all I’m saying is, it’d be a nice feature to have. Are “Algebraic Data Values” a thing? They should be.
It’s a small touch, but Rust lets you nest function definitions, like so:
fn a_function() { fn b_function() { fn c_function() { } c_function(); works } b_function(); works c_function(); error: unresolved name `c_function` }
Neat, huh? With other languages, I’m never quite sure where to put helper functions. ... Mutability rules: also great. Variables are immutable by default, and mutable with the mut keyword. It took me a little while to come to grips with the mutable reference operator &mut, but &mut and I now have a kind of respectful understanding, I think. Data, by the way, inherits the mutability of its enclosing structure. This is in contrast to C, where I feel like I have to write const in about 8 different places just to be double-extra sure, only to have someone else’s cast operator make a mockery of all my precautions.
Functions in Rust are dispatched statically, if the actual data type is known as compile-time, or dynamically, if only the interface is known. (Interfaces in Rust are called “traits”.) As an added bonus, there’s a feature called “type erasure” so you can force a dynamic dispatch to prevent compiling lots of pointlessly specialized functions. This is a good compromise between flexibility and performance, while remaining more or less transparent to the typical user.
Is resource acquisition the same thing as initialization? I’m not sure, but C++ programmers will appreciate Rust’s capacity for RAII-style programming, the happy place where all your memory is freed and all your file descriptors are closed in the course of normal object deallocation. You don’t need to explicitly close or free most things in Rust, even in a deferred manner as in Go, because the Rust compiler figures it out in advance. The RAII pattern works well here because (like C++) Rust doesn’t have a garbage collector, so you won’t have open file descriptors floating around all week waiting for Garbage Pickup Day. " [10]
rust loops are expressions; break can return a value
rust blocks are expressions
rust has loop, while, for
" Rust’s price for improved control is the curse of choice:
struct Foo { bar: Bar } struct Foo<'a> { bar: &'a Bar } struct Foo<'a> { bar: &'a mut Bar } struct Foo { bar: Box<Bar> } struct Foo { bar: Rc<Bar> } struct Foo { bar: Arc<Bar> } " -- https://matklad.github.io/2020/09/20/why-not-rust.html
Opinions
- http://arthurtw.github.io/2014/12/21/rust-anti-sloppy-programming-language.html
- https://news.ycombinator.com/item?id=9122211
- LTU Rust discussion 2010
- http://pcwalton.github.io/blog/2013/05/20/safe-manual-memory-management/
- "Rust really looks like it can be a serious C++ contender in the long run...you can produce shared libraries with rust, and call them from existing C/C++ programs through a C interface. That's a really big deal in places with a lot of legacy code...Another big deal (from the scientific computing POV) is the lack of garbage collection." -- [11]
- "Rust...has gotten many things right -- a really good approach to safety, didn't sacrifice performance, powerful well thought out type system. I think we might finally see the next generation "systems language"." -- [12]
- http://blog.viraptor.info/post/i-wrote-a-website-in-rust-and-lived-to-tell-the-tale
- "Compiled to native binary, garbage collected, no JVM and not object oriented is why I like it." -- https://news.ycombinator.com/item?id=9711409 . "I would also add nice UTF-8 support, easy concurrency and overall simplicity." -- https://news.ycombinator.com/item?id=9711709
- "Rust doesn't have lazy evaluation, or a particularly extensive standard library. I'm not aware that Rust has heap or thread profiling any better than Go's. For everything else, though, yes. I found Rust pretty easy to learn (coming from Java with a bit of Python background); i haven't mastered lifetimes or coherence, but i manage my day-to-day work without needing to. No GC means no GC pauses, and if there are big slow drop cascades, they are at least confined to one thread. Rustfmt seems to be mostly reasonable. Library versioning is done properly. The type system provides generics, sum types, and strong, if unconventional, control over side effects. Godoc uses source order and Markdown. Struct fields have to be initialized. Boilerplate is minimal; in particular, checking errors is one character, and sorting detects and uses an existing. ordering, or takes a single function to define one. " [13]
- "Beyond the “big ideas” contained in Rust (e.g. the ownership model), the “small ideas” that most other systems-y language don’t contain (Option, Result, Iterator, discriminated unions, pattern matching, etc. etc. etc.) make minute-to-minute programming much nicer." [14]
- "Holy cow are the compiler error messages generally fantastic" [15]
- "Rather than being locked into one paradigm, I found myself being able to cleanly use different programming styles as warranted in different situations.2 Certain sections of my code worked best in a functional style and made heavy use of pure functions and Iterator; other sections were mostly cleanly expressed with imperative code, and a for loop and mutable collections did the trick. Sometimes “objects” (i.e. structs with no pub fields, with everything hidden behind a constructor, encapsulated methods, and drop) are the best way to handle certain pieces of data; other times, dumb data bag structs with all of it fields marked pub and almost no impl worked best. None of these styles felt “second class”, and I felt trusted as a programmer to make the right decisions about how to structure my code. Ultimately, the game logic heavy parts of my code look about 90% as nice as they would in e.g. Ruby, and the performance critical, more optimized sections of my code look about 90% as nice as they would in C....This flexibility can definitely be a downside in a team-based setting, as every team member needs to understand every possible permutation of code style. For me working alone, the option to code in different styles is a clear win. " [16]
- "Rust is not easy to learn." [17]
- "Cargo is a solid package manager and build tool. Adding a new library dependency: a one line addition to Cargo.toml. Forking a dependency: hit “fork” in GitHub?; tweak forked repo; add git attribute to dependency in Cargo.toml. Building someone else’s Rust project: 99% chance all you need to do is run cargo build" [18]
- "Rust code..platform independent by default" [19]
- "build.rs handles custom build tasks pretty well. It took me a little while to figure out exactly what should go in build.rs vs in my Makefile, but I eventually landed on build.rs containing the bare minimum to make cargo run work, and no more." [20]
- "Compile times with Rust are Not Great. This is easily my single biggest gripe about Rust right now. The build for A Snake’s Tale takes 15+ seconds, which makes iterating rather tedious." [21]
- "Calling (and being called by) C code is straightforward. " [22]
- "...assignment, evaluated as an expression, returns (). I have no idea why that is the case, or how that could ever be more useful than returning the newly-assigned rvalue." [23]
- this is partially addressed in [24]
- "Rust has a decent FFI mechanism, so I didn’t have to actually write any glue code in C. However, at times I felt like I was jumping through an unnecessary number of hoops just to call a C function and convert its return value to something Rusty." [25] (that post article gives a detailed example following this statement)
- "I've built now several concurrent services with Rust. The language definitely gives confidence to try several things with different approaches to concurrency. None of my services crash (except once per 3-4 months when I deployed something "that will never crash" using `.unwrap()`). The crashes are always my own laziness, but if I follow the pattern of checking return values and unwraping only when the input is static and visible a few lines before, the resulting programs are fast, have a small footprint and basically never crash." [26]
- " I've spent the last few days aggressively parallelizing some Rust code with crossbeam, and it's really just... painless (once you're used to Rust). Rust actually understands data races, and it grumbles at me until my code is provably safe, and then everything Just Works. The Rayon library is also lovely for data parallelism. Sometimes, I think, "Rust is basically a nicer C++ with the obvious foot guns removed and great tooling", but then there are those moments where I'm just blown away by what a good job it does. (I think it helps that I have some functional programming experience under my belt, and that I tend to use mutability sparingly, and mostly in very simple ways.) " [27]
- "The things that excite me the most about Rust are: * The borrow checker, which greatly improves memory safety (SEGFAULTs no more!), * Immutability (const) by default, * Intuitive syntactic sugar such as pattern matching, * No built-in implicit conversions between (arithmetic) types." [28]
- "Rust macros are exemplary of what a good macro system should be" [29]
- https://news.ycombinator.com/item?id=17673711
- https://blog.rust-lang.org/2018/11/27/Rust-survey-2018.html#challenges
- "The language itself is amazing, the pattern matching is super expressive, the borrow checker is incredible in the kinds of errors it can pick up on, and rust-analyzer is leagues beyond where RLS was. But... the compile times are an absolute non-starter for me." -- https://news.ycombinator.com/item?id=23818039
- "Rust Is Surprisingly Good as a Server Language" -- https://stu2b50.dev/posts/rust-is-surpris76171
- "Rust intentionally picked slow compilers in the generics dilemma. This is not necessary the end of the world (the resulting runtime performance improvements are real), but it does mean that you’ll have to fight tooth and nail for reasonable build times in larger projects." [30]
- "Rust also lacks an analog for the pimpl idiom, which means that changing a crate requires recompiling (and not just relinking) all of its reverse dependencies." [31]
- "it’s not to hard to come up with a list of cases where Rust leaves some performance on the table relative to C++. The biggest one is probably the fact that Rust’s move semantics is based on values (memcpy at the machine code level). In contrast, C++ semantics uses special references you can steal data from (pointers at the machine code level). In theory, compiler should be able to see through chain of copies; in practice it often doesn’t: #57077. A related problem is the absence of placement new — Rust sometimes need to copy bytes to/from the stack, while C++ can construct the thing in place...But, to reiterate, these are cherry-picked examples, sometimes the field is tilted the other way. For example, std::unique_ptr has a performance problem which Rust’s Box lacks." [32]
- something that the author thinks ISN'T a problem with Rust: "Dynamic linking (“Rust should have stable ABI”) — I don’t think this is a strong argument. Monomorphization is pretty fundamentally incompatible with dynamic linking and there’s C ABI if you really need to. I do think that the situation here can be improved, but I don’t think that improvement needs to be Rust-specific."
- https://kevinlynagh.com/rust-zig/
- "I feel like Rust has definitely obliterated its complexity budget in unfortunate ways. Every day somebody comes to the sled discord chat with confusion over some async interaction. The fixation on async-await, despite it slowing down almost every real-world workload it is applied to, and despite it adding additional bug classes and compiler errors that simply don’t exist unless you start using it, has been particularly detrimental to the ecosystem. Sure, the async ecosystem is a “thriving subcommunity” but it’s thriving in the Kuhnian sense where a field must be sufficiently problematic to warrant collaboration...I had experienced a lot of nearly-identical pain around async when using various flavors of Scala futures for a few years before picking up Rust in 2014...I think that async/await was a great thing for javascript, and generally it seems to work well in languages that have poor threading support. But Rust has great threading support, and Rust’s future-based strategy aimed from the beginning at copying Scala’s approach. A few loud research-oriented voices in the Rust community said “we think Scala’s approach looks great” and it drowned out the chorus of non-academic users of Scala who had spent years of dealing with frustrating compilation issues and issues where different future implementations were incompatible with each other" -- [33] and [34] and [35]
- "In Scala, futures were annoying because exceptions and meaningless stacktraces. In Rust, you get the right stacktraces and error propagation. In Rust, Futures sucked for me due to error conversions and borrowing being basically unsupported until async await. Now they are still annoying because of ecosystem split (sync vs various partially compatible async)." [36]
- "It was emulating Finagle from the beginning: https://medium.com/@carllerche/announcing-tokio-df6bb4ddb34 ... Having worked in Finagle for a few years before that, I tried to encourage some of the folks to aim for something lighter weight" [37]
- "I’m curious if you stuck around in Scala or pay attention to what’s going on now because I think it has one of the best stories when it comes to managing concurrency. Zio, Cats Effect, Monix, fs2 and Akka all have different goals and trade offs but the old problem of Future is easily avoided" [38]
- "Agreed. I always told people that async/await will be just as popular as Java’s synchronized a few years down the road. Some were surprised, some were offended, but sometimes reality is uncomfortable." [39]
- "
- different syntax for struct creation and function calls, their poor syntax choices also mean that structs/functions won’t get default values any time soon.
- ; is mandatory (what is this, 1980?), but you can leave out , at the end.
- The severe design mistake of using <> for generics also means you have to learn 4 different syntax variations, and when to use them.
- The whole module stuff is way too complex and only makes sense if you programmed in C before. I have basically given up on getting to know the intricacies, and just let IntelliJ? handle uses.
- Super weird that both if and switch exist. " -- [40]
- "> For instance, it has different syntax for struct creation and function calls Perhaps they are trying to avoid the C++ thing where you can’t tell whether foo(bar) is struct creation or a function call without knowing what foo is? ... This is a good thing. Creating a struct is a meaningfully different operation from calling a function, and there’s no problem with having there be separate syntax for these two separate things. The Rust standard library provides a Default trait, with examples of how to use it and customize it. I don’t find it at all difficult to work with structs with default values in Rust. " [41] and [42]
- "> ; is mandatory (what is this, 1980?), but you can leave out , at the end. Ugh this one gets me every time. Why Rust, why. " [43]
- "In almost all languages with mandatory semicolons, they exist to prevent multi-line syntax ambiguities. The designers of Go and Lua both went to great pains to avoid such problems in their language grammars. Unlike, for example, JavaScript?. This article about semicolon insertion rules causing ambiguity and unexpected results should help illustrate some of these problems." [44]
- "...maestro Andrei Alexandrescu was right when he said Rust feels like it "skipped leg day" when it comes to concurrency and metaprogramming capabilities." [45]
- "The classic “what color is your function” blog post describes what is, I think, such a pain? You have to choose in your API whether a function can block or not, and it doesn’t compose well." [46]
- "Rust uses informed polling: the resource can wake the scheduler at any time and tell it to poll. When this occurs it is virtually identical to completion-based async (sans some small implementation details).What informed polling brings to the picture is opportunistic sync: a scheduler may choose to poll before suspending a task. This helps when e.g. there is data already in IO buffers (there often is).There's also some fancy stuff you can do with informed polling, that you can't with completion (such as stateless informed polling)....informed polling is really elegant. " [47]
- "> ...what is informed and stateless informed polling?
stateful async would be writing IO. You've passed in a buffer, the length to copy from the buffer. In the continuation, you'd need to know which original call you were working with so that you can correlate it with those parameters you passed through.
var state = socket.read(buffer);
while (!state.poll()) {}
state.bytesRead...
Stateless async is accepting a connection. In 95% of servers, you just care that a connection was accepted; you don't have any state that persists across the continuation:
while (!listeningSocket.poll()) {}
var socket = listeningSocket.accept();
Stateless async skirts around many of the issues that Rust async can have (because Pin etc. has to happen because of state). ... when you poll a future, you pass in a context. The future derives a "waker" object from this context which it can store, and use to later trigger itself to be re-polled.
By using a context with a custom "waker" implementation, you can learn which future specifically needs to be re-polled.
Normally only the executor would provide the waker implementation, so you only learn which top-level future (task) needs to be re-polled, but not what specific future within that task is ready to proceed. However, some future combinators also use a custom waker so they can be more precise about which specific future within the task should be re-polled. " [48] and [49] "when implementing poll based IO with rust async, typically you have code like “select(); waker.wake()” on a worker thread. Select blocks. Waking tells the executor to poll the related future again, from the top of its tree. The waker implementation may indeed cause an executor thread to stop waiting, it depends on the implementation. It could also be the case that the executor is already awake and the future is simply added to a synchronised queue. Etc."[50]
- I agree that Rust async is currently in a somewhat awkward state. Don't get me wrong, it's usable and many projects use it to great effect. But there are a few important features like async trait methods (blocked by HKT), async closures, async drop, and (potentially) existential types, that seem to linger. The unresolved problems around Pin are the most worrying aspect. The ecosystem is somewhat fractured, partially due to a lack of commonly agreed abstractions, partially due to language limitations. ... But what I disagree with is that polling was a mistake. It is what distinguishes Rusts implementation, and provides significant benefits. A completion model would require a heavier, standardized runtime and associated inefficiencies like extra allocations and indirection, and prevent efficiencies that emerge with polling. Being able to just locally poll futures without handing them off to a runtime, or cheaply dropping them, are big benefits. Completion is the right choice for languages with a heavy runtime. But I don't see how having the Rust dictate completion would make io_uring wrapping more efficient than implementing the same patterns in libraries. UX and convenience is a different topic. Rust async will never be as easy to use as Go, or async in languages like Javascript/C#. To me the whole point of Rust is providing as high-level, safe abstractions as possible, without constraining the ability to achieve maximum efficiency . (how well that goal is achieved, or hindered by certain design patterns that are more or less dictated by the language design is debatable, though) " [51]
- "> Is there a good explanation on the difference between polling model and completion model? (not Rust-specific) ...the primary difference is that in the latter model the buffer becomes "owned" by runtime/OS while task is suspended. It means that you can not simply drop a task if you no longer need its results, like Rust currently assumes. You have either wait for the data read request to complete or to (possibly asynchronously) request cancellation of this request. With the current Rust async if you want to integrate with io-uring you would have to use awkward buffers managed by runtime, instead of simple buffers which are part of the task state. Even outside of integration with io-uring/IOCP we have use-cases which require async Drop and we currently don't have a good solution for it. So I don't think that the decision to allow dropping tasks without an explicit cancellation was a good one, even despite the convenience which it brings. " [52]
- (reply to a different comment) "In a completion-based model (read io-uring, but I think IOCP behaves similarly, though I am less familiar with it) it's a runtime who "notifies" tasks about completed IO requests. In io-uring you have two queues represented by ring buffers shared with OS. You add submission queue entries (SQE) to the first buffer which describe what you want for OS to do, OS reads them, performs the requested job, and places completion queue events (CQEs) for completed requests into the second buffer. So in this model a task (Future in your terminology) registers SQE (the registration process may be proxied via user-space runtime) and suspends itself. Let's assume for simplicity that only one SQE was registered for the task. After OS sends CQE for the request, runtime finds a correct state transition function (via meta-information embedded into SQE, which gets mirrored to the relevant CQE) and simply executes it, the requested data (if it was a read) will be already filled into a buffer which is part of the FSM state, so no need for additional syscalls or interactions with the runtime to read this data! If you are familiar with embedded development, then it should sound quite familiar, since it's roughly how hardware interrupts work as well! You register a job (e.g. DMA transfer), dedicated hardware block does it, and notifies a registered callback after the job was done. Of course, it's quite an oversimplification, but fundamental similarity is there." [53]
- "As you point out, a lot of the ergonomics issues of closures in Rust appear when they are long-lived (stored in structs, sent to other threads, etc), because now you have to think hard about all the lifetime calculus that the compiler would elide or otherwise handle for you in simple callback cases. For multi-threaded work, this means that closures can be made a lot more ergonomic when abiding with a structured concurrency paradigm where no concurrent task is allowed to evade the current scope. See scoped threads in rayon and crossbeam if you are not familiar with the concept. The much-loved rayon parallel iterators are actually just a special case of it. I wonder what currently prevents the async ecosystem from producing a good 99% solution that is as ergonomic as scoped threads are for threads. I suspect the need for long-lived framework-ish infrastructures (event loops, IO backends, etc) might be it, but am not sure. " [54]
- "I've always looked at Rust as sort of the Breaking 2.0 Version of C++ twenty years later and as such benefits immensely from the mistakes that where made in C++. Templates/Generics, RAII, Move semantics, zero-cost-abstractions, smart pointers where all first tried in C++ and sometimes to questionable results. Rusts answers to the problems (use haskell style type classes, use RAII with a normal function as constructor, move by default, just use zero cost abstractions) are all the direct result of lessons learned in C++." [55]
- "> I am also troubled by the fact that two closures with identical signatures are not considered identical by the type checker, so you can't eg. return closures from different match arms. (Although I'm sure there is a reason for this which I am unaware of.) Something like this has actually been proposed in the early days of futures and impl Trait, where people wanted to be able to return two different kinds of concrete types from a function returning impl Trait. But the Rust designers were uneasy about it at the time because it could not be made zero-cost, which means that making it too transparent would create a performance footgun. To see why, remember that the difference between a function and a closure is that a closure can store state (references to variables in the surrounding scope, or moved/copied versions thereof). At the implementation level, it is like a struct holding that captured state. Every such struct is potentially unique because every closure captures potentially different state, so to store a closure, you need a variable whose type is specific to that closure. If we wanted to store N different kinds of closures in a single variable, then we would need a type which can store N different kinds of concrete structs, i.e. an enum or a (boxed) trait object. Both of these abstractions have nontrivial overhead. Enums require discriminant checking and have a space overhead that's the maximum of the N stored types, whereas trait objects require memory allocation (in current Rust, there is work to relax this in selected cases in the future) and vtable indirection. I personally think that given sufficiently loud syntax, this would still be accepted and become a nice language feature. But as usual, someone needs to put in the work of designing that loud syntax and resolving all the unforeseen edge cases that don't come up when one is just toying with a concept. " [56]
- " > ... without the typing depth a proper monadic system like in haskell provides. Several prominent Rust designers have stated in the past that a monadic system designed like that of Haskell wouldn't work in Rust in their opinion. The pain points usually mentioned are... Without further constraints, higher-kinded types like Monad make type inference undecidable. The constraints used by Haskell, based on currying, would feel out of place in Rust, where currying is not commonly used. There are difficult interactions with the ownership, borrowing and lifetime system, common pain points being handling multiple kinds of references and (in the specific case of async and generators) correctly handling borrows across suspend and resume points without introducing avenues for memory unsafety. A monadic design makes it difficult to handle complex control flow, such as early returns or general loops. This is not usually a problem for functional programming languages, where such control flow is rare as it is almost always done for the purpose of mutating data or selectively avoiding side effects. But since Rust is a lot friendlier towards mutation, it feels more limiting there. There are a lot of API problems that Haskell solves with their local equivalent of a tree of boxed trait objects, rather than trying to produce maximally efficient state machine types, and this would feel like an uncomfortable amount of overhead in a performance-focused language like Rust. Two particularly interesting recaps to read are:
- Niko Matsakis on HKTs like Monad in Rust : https://smallcultfollowing.com/babysteps/blog/2016/11/09/associated-type-constructors-part-4-unifying-atc-and-hkt/ (end of a series, with links to the previous posts)
- Withoutboats summarizing why async await cannot be easily replaced with monads and do notation : https://twitter.com/withoutboats/status/1027702531361857536 . To be clear, I don't think this invalidates your overall point that there might be benefits to a more general-purpose abstraction here, but I think there's a reasonable amount of evidence that the designs that work for Haskell cannot be applied as-is to an imperative language like Rust or Javascript, especially when performance is a concern. The "clean" solution to async/await like problems in those languages may eventually turn out to be something completely different like some sort of generalized coroutine abstraction. " [57]
- "Many people have explained the problems with asynchronous programming – the famous what colour is your function essay, for example.1 However, I think there are a number of things specific to the design of Rust that make asynchronous Rust particularly messy, on top of the problems inherent to doing any sort of asynchronous programming...Closures can be pretty radioactive...It’s a lot easier to make some data (a struct) with some functions attached (methods) than it is to make some functions with some data attached (closures)...Trying to use closures is hard...the restrictions applied to using closures infect types that contain them...Bearing this in mind, it is really quite hard to make a lot of asynchronous paradigms (like async/await) work well in Rust...you have an entire language ecosystem built on top of the idea of making these Future objects that actually have a load of closures inside...You can actually manually implement Future on a regular old struct. If you do this, things suddenly become a lot simpler, but also you can’t easily perform more async operations inside that struct’s methods...Oh, and all the problems from what colour is your function are still there too, by the way – on top of the Rust-specific ones...Beginner (and experienced) Rust programmers look at the state of the world as it is and try and build things on top of these shaky abstractions, and end up running into obscure compiler errors, and using hacks like the async_trait crate to glue things together, and end up with projects that depend on like 3 different versions of tokio and futures (perhaps some async-std in there if you’re feeling spicy) because people have differing opinions on how to try and avoid the fundamentally unavoidable problems, and it’s all a bit frustrating, and ultimately, all a bit sad." [58]
- "I'm not totally sure what the author is asking for, apart from refcounting and heap allocations that happen behind your back. In my experience async Rust is heavily characterised by tasks (Futures) which own their data. They have to - when you spawn it, you're offloading ownership of its state to an executor that will keep it alive for some period of time that is out of the spawning code's control. That means all data/state brought in at spawn time must be enclosed by move closures and/or shared references backed by an owned type (Arc) rather than & or &mut. If you want to, nothing is stopping you emulating a higher-level language - wrap all your data in Arc<_> or Arc<Mutex<_>> and store all your functions as trait objects like Box<dyn Fn(...)>. You pay for extra heap allocations and indirection, but avoid specifying generics that spread up your type hierarchy, and no longer need to play by the borrow checker's rules. What Rust gives us is the option to _not_ pay all the costs I mentioned in the last paragraph, which is pretty cool if you're prepared to code for it." [59]
- "The problem, IMO, isn't about allocations or ownership. In fact, I think that a lot of the complaints about async Rust aren't even about async Rust or Futures. The article brings up the legitimate awkwardness of passing functions/closures around in Rust. But it's perfectly fair to say that idiomatic Rust is not a functional language, and passing functions around is just not the first tool to grab from your toolbelt. I think the actual complaint is not about "async", but actually about traits. Traits are paradoxically one of Rust's best features and also a super leaky and incomplete abstraction. Let's say you know a bit of Rust and you're kind of working through some problem. You write a couple of async functions with the fancy `async fn foo(&x: Bar) -> Foo` syntax. Now you want to abstract the implementation by wrapping those functions in a trait. So you try just copy+pasting the signature into the trait. The compiler complains that async trait methods aren't allowed. So now you try to desugar the signature into `fn foo(&x: Bar) -> impl Future<Foo>` (did you forget Send or Unpin? How do you know if you need or want those bounds?). That doesn't work either because now you find out that `impl Trait` syntax isn't supported in traits. So now you might try an associated type, which is what you usually do for a trait with "generic" return values. That works okay, except that now your implementation has to wrap its return value in Box::pin, which is extra overhead that wasn't there when you just had the standalone functions with no abstraction. You could theoretically let the compiler bitch at you until it prints the true return value and copy+paste that into the trait implementation's associated type, but realistically, that's probably a mistake because you'd have to redo that every time you tweak the function for any reason. IMO, most of the pain isn't really caused by async/await. It's actually caused by traits. " [60]
- " also a long-time rust user, and I buy this. one of the things it took me longest to realize when writing rust is to reach for traits carefully/reluctantly. they can be amazing (e.g. serde), but I've wasted tons of time trying to make some elegant trait system work when I could have solved the problem much more quickly otherwise. " [61]
- "> The article brings up the legitimate awkwardness of passing functions/closures around in Rust. That's a hard problem when you have linear/affine types! Closures don't work so neatly as in Haskell; currying has to be different. " [62]
- " I've been working on a reasonably complicated project that is using rust, and I think about de-asyncing my code somewhat often because there are some nasty side effects you can see when you try to connect async & non async code. E.g., tokio makes it very difficult to correctly & safely (in a way the compiler, not runtime) catches launch an async task in a blocking function (with no runtime) that itself may be inside of a async routine. It makes using libraries kind of tough, and I think you end up with a model where you have a thread-per-library so the library knows it has a valid runtime, which is totally weird. All that said, the author's article reads as a bit daft. I think anyone who has tried building something complicated in C++ / Go will look at those examples and marvel at how awesome Rust's ability to understand lifetimes is (i.e. better than your own) and keep you from using resources in an unintended way. E.g., you want to keep some data alive for a closure and locally? Arc. Both need to writable? Arc Mutex. You are a genius and can guarantee this Fn will never leak and it's safe to have it not capture something by value that is used later in the program and you really need the performance of not using Arc? Cast it to a ptr and read in an unsafe block in the closure. Rust doesn't stop you from doing whatever you want in this regard, it just makes you explicitly ask for what you want rather than doing something stupid automatically and making you have a hard to find bug later down the line. " [63]
- " I like to joke that the best way to encounter the ugliest parts of Rust is to implement an HTTP router. Hours and days of boxing and pinning, Futures transformations, no async fn in traits, closures not being real first class citizens, T: Send + Sync + 'static, etc. I call this The Dispatch Tax. Because any time you want more flexibility than the preferred static dispatch via generics can give you - oh, so you just want to store all these async fn(HttpRequest?) -> Result<HttpResponse?>, right? - you immediately start feeling the inconvenience and bulkiness of dynamic dispatch. It's like Rust is punishing you for using it. And with async/await taking this to a new level altogether, because you are immediately forced to understand how async funcs are transformed into ones that return Futures, how Futures are transformed into anon state machine structs; how closures are also transformed to anon structs. It's like there's no type system anymore, only structs. That's one of the reasons, I think, why Go has won the Control Plane. Sure, projects like K8s, Docker, the whole HashiCorp? suite are old news. But it's interesting and telling that even solid Rust shops like PingCAP? are using Go for their control plane. It seems to me that there's some fundamental connection between flexibility of convenient dynamic dispatch and control plane tasks. And of course having the default runtime and building blocks like net and http in the standard library is a huge win. That said, after almost three months of daily Rust it does get better. To the point when you can actually feel that some intuition and genuine understanding is there, and you can finally work on your problems instead of fighting with the language. I just wish that the initial learning curve wasn't so high. " [64]
- " Definitely dynamic dispatch + async brings out a lot of pain points. But I only agree that the async part of that is unfortunate. Making dynamic dispatch have a little extra friction is a feature, not a bug, so to speak. Rust's raison d'être is "zero cost abstraction" and to be a systems language that should be viable in the same spaces as C++. Heap allocating needs to be explicit, just like in C and C++. But, I agree that async is really unergonomic once you go beyond the most trivial examples (some of which the article doesn't even cover). Some of it is the choices made around the async/await design (The Futures, themselves, and the "async model" is fine, IMO). But the async syntax falls REALLY flat when you want an async trait method (because of a combination-and-overlap of no-HKTs, no-GATs, no `impl Trait` syntax for trait methods) or an async destructor (which isn't a huge deal- I think you can just use future::executor::block_on() and/or use something like the defer-drop crate for expensive drops). Then it's compounded by the fact that Rust has these "implicit" traits that are usually implemented automatically, like Send, Sync, Unpin. It's great until you write a bunch of code that compiles just fine in the module, but you go to plug it in to some other code and realize that you actually needed it to be Send and it's not. Crap- gotta go back and massage it until it's Send or Unpin or whatever. Some of these things will improve (GATs are coming), but I think that Rust kind of did itself a disservice with stabilizing the async/await stuff, because now they'll never be able to break it and the Pin/Unpin FUD makes me nervous. I also think that Rust should have embraced HKTs/Monads even though it's a big can of worms and invites Rust devs to turn into Scala/Haskell weenies (said with love because I'm one of them). " [65]
- "...Pin was rushed and has serious problems. " [66]
- "First of all yes, Rust futures use a poll model, where any state changes from different tasks don't directly call completions, but instead just schedule the original task to wake up again. I still think this is a good fit, and makes a lot of sense. It avoids a lot of errors on having a variety of state on the call stack before calling the continuation, which then gets invalidated. The model by itself also doesn't automatically make using completion based IO impossible. However the polling model in Rust is combined with the model of always being able to drop a Future in order to cancel a task. This doesn't allow to use lower level libraries which require to do this without applying any additional workarounds...." [67]
- "There is NO meaningful connection between the completion vs polling futures model and the epoll vs io-uring IO models. comex's comments regarding this fact are mostly accurate. The polling model that Rust chose is the only approach that has been able to achieve single allocation state machines in Rust...After designing async/await, I went on to investigate io-uring and how it would be integrated into Rust's system. I have a whole blog series about it on my website: https://without.boats/tags/io-uring/. I assure you, the problems it present are not related to Rust's polling model AT ALL. They arise from the limits of Rust's borrow system to describe dynamic loans across the syscall boundary (i.e. that it cannot describe this). A completion model would not have made it possible to pass a lifetime-bound reference into the kernel and guarantee no aliasing. But all of them have fine solutions building on work that already exists." [68] (note: 'comex's comments' appears to refer to: [69], or possibly within this thread: https://news.ycombinator.com/item?id=26407564)
- "In short: Rust's abstractions are a double-edged sword. They can hide suboptimal code, but also make it easier to make algorithmic improvements and take advantage of highly optimized libraries. I'm never worried that I'm going to hit a performance dead-end with Rust. There's always the unsafe escape hatch that allows very low-level optimizations (and it's not needed often). Fearless concurrency is real. The occasional awkwardness of the borrow checker pays off in making parallel programming practical. My overall feeling is that if I could spend infinite time and effort, my C programs would be as fast or faster than Rust, because theoretically there's nothing that C can't do that Rust can. But in practice C has fewer abstractions, primitive standard library, dreadful dependency situation, and I just don't have the time to reinvent the wheel, optimally, every time. ... To sum it up Rust is low-level enough that if necessary, it can be optimized for maximum performance just as well as C. Higher-level abstractions, easy memory management, and abundance of available libraries tend to make Rust programs have more code, do more, and if left unchecked, can add up to bloat. However, Rust programs also optimize quite well, sometimes better than C. While C is good for writing minimal code on byte-by-byte pointer-by-pointer level, Rust has powerful features for efficiently combining multiple functions or even whole libraries together. But the biggest potential is in ability to fearlessly parallelize majority of Rust code, even when the equivalent C code would be too risky to parallelize. In this aspect Rust is a much more mature language than C. " [70]
- [71] has many more details on things that make Rust faster or C faster
Example of: " For example, this code:
fn bar() -> i32 { 5 }
fn foo() -> &'static i32 { &bar() }
gives this error:
error[E0716]: temporary value dropped while borrowed --> src/lib.rs:6:6
|
6&bar() |
^^^^^ creates a temporary which is freed while still in use |
7} |
- temporary value is freed at the end of this statement |
|
= note: borrowed value must be valid for the static lifetime...
bar() produces a value, and so &bar() would produce a reference to a value on foo()‘s stack. Returning it would be a dangling pointer. In a system without automatic memory management, this would cause a dangling pointer. " -- [72]
- http://dtrace.org/blogs/bmc/2018/09/18/falling-in-love-with-rust/ likes Rust's ownership model, error handling, macros, format!, include_str!, serde, tuples, integrated testing, community, and performance (in no particular order, or rather, just in the order that e noticed them).
- " Language-wise, Rust is really top-notch, I find it hard to fault any design decision. There are some design choices that annoy me (e.g., two closure types can never be equal), but there is always a fact-based rationale for why things are the way they are. My two major pain points in Rust are (1) the compile times, (2) the high variance of quality in the ecosystem...Anyone can easily contribute to the crates ecosystem and post their libraries and programs to crates.io...there is no real way to know what's production-quality and what's a toy. You can try and rely on the number of downloads, the names of the contributors, etc., but there is no system that tells you what other Rustaceans think of a crate." [73]
- "...there are almost no ugly parts in Rust. Sure, you can find minor annoyances like those:
- Type inference pretty much being killed by method calls. For instance, code like this won't work:
fn x() -> Vec<i32> {
let mut x = (0..3).collect();
x.sort(); // Calling any method of Vec
x // Cannot infer that `x` is `Vec<i32>` because a method was called
}
- Turbofish syntax is ugly. For instance, in Rust you say `std::mem::size_of::<T>()`. It would be nice if you could replace `::<` with `<`.
- Negative modulo follows C semantics. This means `-2 % 12` is `-2`, not `10`. There is a sane implementation in `mod_euc` method, but it's not the default.
- Lexical lifetimes reject code that should be valid necessitating weird workarounds. NLL will fix that one.
- Trait objects types used to be written using a trait name like `Display`. There is a new syntax which is more clear: `dyn Display`, but for backwards compatibility reasons the old syntax is accepted.
- Macros (including procedural derives) cannot access private items in a crate, requiring workarounds like exporting private items in a crate, and having `#[doc(hidden)]` attribute to hide them from documentation.
- Trait version incompatibility issue. That one is weird, but essentially it's possible for a program to have two versions of the same library. It's possible for a library to say, have a function that requires an argument implementing `Serialize` interface from serde 0.9. If you try to implement `Serialize` interface from serde 1.0 then you will get an error message complaining about not implementing `Serialize`, despite you having implemented it, just in the wrong version of a library.
- Missing higher kinded polymorphism.
- Missing const generics.
- The compiler is slow. Like, really slow.
- The language is hard to learn because of many complicated features like ownership and borrow checking. That said, I think those features are a good thing, I'm missing those in other programming languages, but they are problematic when you are learning the programming language. " [74]
- minor ugly parts of Rust: "
- RC<RefCell?<data>> for lambdas to access struct data, specially painful on GUI callbacks
- lack of support for binary libraries on cargo " [75]
- "It's surprising how many things in (( https://doc.rust-lang.org/nomicon/ )) don't have anything to do with unsafe per se. It's just that, when you can't rely on the inference system to "do the right thing", you now have to actually think about subtype variance and the order that destructors run in, instead of letting the compiler worry about all that bookkeeping. " [76]
- "I fear the high-level web developers are in the driver's seat, which may doom rust as a systems language. I'm hesitating on rust because I would miss bitfields and various unsafe performance features. I like the opt-out safety of the "unsafe" keyword, but I want more things that I can opt out of. I want goto, computed goto, no-default switches, something like Microsoft's __assume keyword, and all the other low-level stuff you'd want for making boot loaders and high-performance kernels." [77]
- list of "things that Rust shipped without that should have been included?": https://news.ycombinator.com/item?id=9827364
- https://www.evanmiller.org/a-taste-of-rust.html
- "Besides the learning difficulty, I'd there are some real ergonomic issues related to ownership, such as the difficulty of cloning into a closure [1], or nested &mut receiver calls being rejected despite being sound [2]. [1] https://github.com/rust-lang/rfcs/issues/2407 [2] https://github.com/rust-lang/rust/issues/6268" [78]
- "...if I could have one wishlist from the Rust language, it would be proper keyword arguments, as the builder pattern feels clunky, and the struct-with-default pattern is no better." -- [79]
- "All of this is not to say that Rust is perfect; there are certainly some minor annoyances (rustfmt: looking at you! https://github.com/rust-lang/rustfmt/issues/4306 )," [80]
- "As an aside, format_args! is really magical" [81]
- "...from an Oxide perspective is that Rust has proven to be a really good fit — remarkably good, honestly — at more or less all layers of the stack. You can expect much, much more to come from Oxide on this...Rust is going really well for us at Oxide, but for the moment I want to focus on more personal things — reasons that I personally have enjoyed implementing in Rust. These run the gamut: some are tiny but beautiful details that allow me to indulge in the pleasure of the craft; some are much more profound features that represent important advances in the state of the art; and some are bodies of software developed by the Rust community, notable as much for their reflection of who is attracted to Rust (and why) as for the artifacts themselves. It should also be said that I stand by absolutely everything I said two years ago; this is not as a replacement for that list, but rather a supplement to it." [82]
- "Language-level support for libraries and top-notch build system/package manager allow for a thriving ecosystem...Additionally, low-level nature of the Rust programming language often allows for “perfect” library interfaces. Interfaces which exactly reflect the underlying problem, without imposing intermediate language-level abstractions." [83]
- "For Complex Applications, Rust is as Productive as Kotlin" [84]
- "I feel that Rust is significantly more productive when it comes to basic language nuts and bolts — structs, enums, functions, etc. This is not specific to Rust — any ML-family language has them. However, Rust is the first industrial language which wraps these features in a nice package, not constrained by backwards compatibility." [85]
- "Emphasis on data over behavior. Aka, Rust is not an OOP language. The core idea of OOP is that of dynamic dispatch — which code is invoked by a function call is decided at runtime (late binding). This is a powerful pattern which allows for flexible and extensible system. The problem is, extensibility is costly! It’s better only to apply it in certain designated areas. Designing" [86]
- "One small syntactic thing I enjoy about Rust is how it puts fields and methods into different blocks syntactically: struct Person { first_name: String, last_name: String, } impl Person { fn full_name(&self) -> String { ... } } Being able to see at a glance all the fields makes understanding the code much simpler. Fields convey much more information than methods. " [87]
- "Sum types. Rust’s humbly named enums are full algebraic data types. This means that you can express the idea of disjoint union: enum Either<A, B> { A(A), B(B) } " [88]
- "Performance Predictability What is more important is that Rust’s performance is predictable. Generally, running a program N times gives more-or-less the same result. This is in sharp contrast to JVM, where you need to do a lot of warm-up to stabilize even microbenchmarks. I’ve never succeeded with reproducible macro benchmarks in IntelliJ? Rust. More generally, without runtime there’s much less variation in the behavior of the program. This makes chasing regressions much more effective. " [89]
- "“is this type thread-safe?” is a property which is reflected in Rust’s type system (via Send and Sync traits). Compiler automatically derives thread-safeness, and checks that non thread-safe types are not accidentally shared." [90]
- "...the module system. Which is, in Rust, mostly orthogonal to type-system (unlike OCaml, modules are not first-class values), but is very much a language concern. Getting rid of a global namespace of symbols is important. The insight about cyclic/non-cyclic dependencies both being useful and the compartmentalization of the two kinds into modules and crates is invaluable. It’s interesting though that the nuts and bolts of the module system are not perfect – it is way to sophisticated for the task it achieves. It’s a shame to spend so much complexity budget on something that could’ve been boring. (I won’t go into the details here, but suffices to say that Rust has two flavors of module system, 2015 and 2018, because the first try was empirically confusing). But this annoyances do not negate the fundamentally right structure. However, I also do think that the theory of depedency hell less library ecosystem would be useless without cargo being there and making it easy to put everything to practice. A lot of thought went into designing Cargo’s UX, and I doubt many people would notice the language-level awesomeness, if not for this tool" [91]
- "... ecosystem taking semver seriously (compare to amount of work Linux distros need to put in to keep sem-whatever packages work together) compiler taking 1.0 back-compat seriously (in contrast, Node.js has semver-major breaking changes regularly, and my old JS projects just don’t run any more) concept of crates exist in the language (e.g. Rust can have equivalent of -std=cXY set by each dependency individually. C/C++ can’t quite do it, because headers are shared) not giving up on Windows as a first-class supported platform (in C it’s “not my problem that Windows sucks”. Cargo chose to make it its problem to fix) zero-effort built-in unit testing. There’s no excuse for not having tests :)
Type system also helps, because e.g. thread-safety is described in types, rather than documentation prose. Rust also made some conscious design decisions to favor libraries, e.g. borrow checking is based only on interfaces to avoid exposing implementation details (this wasn’t an easy decision, because it makes getters/setters overly restrictive in Rust). " [92]
- "...If I’m programming in C or Python and I’m using a library I have to carefully read the documentation to know what I can pass where without breaking things. If it’s java and I’m using a library I have to keep track of a few things like null’s… by carefully reading the documentation. Rust libraries for the most part actually manage to make it difficult to missuse them, so I can be much less careful about reading the documentation. Moreover on the documentation point, rust as a language lends itself to concise and accurate auto generated documentation much more than most languages do, so rust libraries will generally have better documentation despite it being less important. Partially this is tooling, but it is also things like not having SFINAE (C++), not having automatic implementation of interfaces (go), and so on..." [93]
- "The thing I like about rust over any other language (though go is pretty close) is the fact that if it fails to compile (which in a typed language is 60% of the battle, at least getting started) It will tell me what went wrong and exactly where in the documentation it talks about that issue. The time saved there, along with the good documentation and functioning packaging system make rust one of the languages I most hope to be programming in professionally in the near future." [94]
- https://gregoryszorc.com/blog/2021/04/13/rust-is-for-professionals/
- https://kerkour.com/blog/rust-for-web-development-2-years-later/
- https://www.thecodedmessage.com/posts/async-colors/
- "Rust’s borrowing model can’t express all types of ownership. Most notably, it can’t reason about circular (self-referential) structures. When you hit a case that doesn’t fit Rust’s model, then you either need unsafe or some runtime check, and that feels disappointing compared to the level of safety and performance Rust usually gives." -- [95]
- https://kdy1.dev/posts/2022/1/tsc-go
- "Speaking as someone who has written non-trivial compilers in both Rust and Go... Compilers tend to use a bit thornier data structures and graphs than you might see in other realms. It is very convenient to just represent these graphs as objects connected by pointers, and know that you don't have to worry about objects that become unreachable. For example, the values in an SSA-based IR are all full of pointers to other values. Each value represents the computation to produce that value, like "x12 / 4". A strength-reduction pass goes in and mutates this node, replacing it with "x12 >> 2" (if such a change is valid). It knows the change is valid because it follows pointers from the node to its inputs. The change is available to all upstream values immediately because it is mutated. These changes may make other values unreachable, at which point, you don't care about them. You only care about the values which are reachable from computations with side effects. These nodes definitely have cycles and IMO, the notion of weak references between them doesn't make sense. I'm sure there's a way to do that in Rust. Please, nobody try to explain how. I could figure it out. The thing is... it's not obvious how to do it in Rust, and figuring out how to "explain ownership to the Rust compiler" without creating Rc<RefCell?<T>> cycle leaks is a non-trivial task that sucks away time I could be spending writing the compiler. This reminds me of a paper I saw go by years back about writing a compiler in Haskell. Pure Haskell code doesn't let you mutate objects and doesn't let you do pointer comparisons, so it creates problems if you're trying to implement an algorithm which is conceptually pure (at a high level, a function with inputs and outputs) but where you're taking the implementation from a paper which constructs the output iteratively, piece by piece. The problem is that your data structures have to be annotated to show which parts are mutable (IORef, STRef, or TVar), but "which parts are mutable" may have a different answer depending on whether you are inside some function transforming or constructing a data structure. It was obvious to me, reading the paper, that while I love functional purity, and functional purity solves all sorts of problems and makes it easier to write code in general, it sometimes gets in the way of getting shit done. The same is true of Rust's notion of ownership... super useful when it lets me write fast, efficient code without having to think about ownership (when it just works and gets out of the way), and super annoying when you have to figure out how to explain your program to Rust." klodolph
- https://lucumr.pocoo.org/2022/1/30/unsafe-rust/
- Rust is not a good C replacement
- https://blog.yossarian.net/2020/05/20/Things-I-hate-about-rust and https://blog.yossarian.net/2022/03/10/Things-I-hate-about-Rust-redux
- Rust: A Critical Retrospective by bunnie
- "The main Rust syntax is OK, but as the author points out, macros are a mess. The "cfg" directive is closer to the syntax used in ".toml" files than to Rust itself, because some of the same configuration info appears in both places." -- [96]
- https://shopify.engineering/porting-yjit-ruby-compiler-to-rust
- https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-of-mainstream-programming.html
- "When I compare it with C# or Java, I find my self envying it's structural type system, error handling philosophy and constructs, often very useful and descriptive compiler messages, the absence of null, the best implementation of the iterator pattern I've seen in any language, traits, tuples, enums, amazing pattern matching and the list goes on." -- https://github.com/Hirrolot/hirrolot.github.io/issues/6#issuecomment-1145881193
- "Indeed, Rust encourages you to write my_array.iter().map(myFn).collect() instead of doing for loop." [97]
- A Pleasing Symmetry in Rust: Appreciating how Rust enum variants are mirrors of its kinds of structs
- "Rust lacks named and optional parameters in function calls" -- [98] (note: in context, this was not a complaint, just a statement; the author is obviously a huge fan of Rust)
- https://hg.sr.ht/~icefox/garnet#pain-points-in-rust-to-think-about
- "My personal list of Rust grievances (September 2021)"
- "I believe “allowing semicolons to be omitted after some expressions, like match, if/else, and if.” is more a consequence of blocks being values, and if you don’t place a semicolon then the last value is the final value of the block, whereas if you do place a semicolon, the block value becomes (). This is a thing you can run into all the time with smaller functions where you are just relying on “return the final value”, but you put a semicolon at the end of the line (like every line before it) and it decides you didn’t want to return the last value. I really like blocks being values, but the semicolon behavior feels like a real anti-feature to me (though there’s maybe some deep reason why this is done that " -- [99]
- "Rust is awesome, for certain things. But think twice before picking it up for a startup that needs to move fast...As mentioned above, the service we were building was a fairly straightforward CRUD app. The expected load on this service was going to be on the order no more than a few queries per second, max, through the lifetime of this particular system. The service was a frontend to a fairly elaborate data-processing pipeline that could take many hours to run, so the service itself was not expected to be a performance bottleneck. There was no particular concern that a conventional language like Python would have any trouble delivering good performance. There were no special safety or concurrency needs beyond what any web-facing service needs to deal with. The only reason we were using Rust was because the original authors of the system were Rust experts, not because it was an especially good fit for building this kind of service..Rust has made the decision that safety is more important than developer productivity...Rust makes roughing out new features very hard...Rust has a huge learning curve...My primary experience from Rust comes from working with it for a little more than 2 years at a previous startup. This project was a cloud-based SaaS? product that is, more-or-less, a conventional CRUD app: it is a set of microservices that provide a REST and gRPC API endpoint in front of a database, as well as some other back-end microservices (themselves implemented in a combination of Rust and Python). Rust was used primarily because a couple of the founders of the company were Rust experts. Over time, we grew the team considerably (increasing the engineering headcount by nearly 10x), and the size and complexity of the codebase grew considerably as well...There are definitely things I like about Rust, and features from Rust that I’d love to have in other languages. The match syntax is great. The Option, Result, and Error traits are really powerful, and the ? operator is an elegant way of handling errors...I would absolutely use Rust for projects that need a high level of performance and safety and for which I was not terribly worried about the need to rapidly evolve major parts of the code with a whole team that is growing fast. For individual projects, or very small (say, 2–3 person) teams, Rust would likely be just fine. Rust is a great choice for things like kernel modules, firmware, game engines, etc. where performance and safety are paramount, and in situations where it may be hard to do really thorough testing prior to shipping.--- [100]
- "From my experience, using Rust has lead to faster developer velocity (even for CRUD apps) simply because the strong type system allows you to encode invariants to be checked at compile time, and the tooling and libraries are amazing...However, I do think there is a situation where this advice holds true..., which is for a very specific kind of startup: a hyper-growth startup with 100%+ YoY? engineer hiring. The issue with Rust is not that its slower to develop in, I don’t think that is true, its that in order to develop quickly in Rust you have to program in Rust...if your influx of new developers who need to learn Rust is too large, you won’t be able to properly onboard them. Trying to write Java using Rust is horrible...It isn’t even lifetimes or borrowing that are necessarily tricky...The bigger problems are around how to structure code" -- [101]
- "How much does Rust's bounds checking actually cost?...At the end of the day, it seems like at least for this kind of large-scale, complex application, the cost of pervasive runtime bounds checking is negligible." -- https://blog.readyset.io/bounds-checks/
- https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html
- https://www.warp.dev/blog/why-is-building-a-ui-in-rust-so-hard
- https://matklad.github.io/2023/03/28/rust-is-a-scalable-language.html
- https://bitbashing.io/async-rust.html
- https://news.ycombinator.com/item?id=37435515
- """...If I had to do this in C++, I'd be fighting crashes all the time. There's a reason most of the highly publicized failed metaverse projects didn't reach this level of concurrency. In Rust, I have about one memory related crash per year, and it's always been in someone else's "unsafe" code. My own code has no "unsafe", and I have "unsafe" locked out to prevent it from creeping in. The normal development process is that it's hard to get things to compile, and then it Just Works. That's great! I hate using a debugger, especially on concurrent programs. Yes, sometimes you can get stuck for a day, trying to express something within the ownership rules. Beats debugging. I have my complaints about Rust. The main ones are:
- Rust is race condition free, but not deadlock free. It needs a static deadlock analyzer, one that tracks through the call chain and finds that lock A is locked before lock B on path X, while lock B is locked before path A on path Y. Deadlocks, though, tend to show up early and are solid problems, while race conditions show up randomly and are hard to diagnose.
- Async contamination. Async is all wrong when there's considerable compute-bound work, and incompatible with threads running at multiple priorities. It keeps creeping in. I need to contact a crate maintainer and get them to make their unused use of "reqwest" dependent on a feature, so I don't pull in Tokio. I'm not using it, but it's there.
- Single ownership with a back reference is a very common need, and it's too hard to do. I use Rc and Weak for that, but shouldn't have to. What's needed is a set of traits to manage consistent forward and back links (that's been done by others) and static analysis to eliminate the reference counts. The basic constraints are ordinary borrow checker restrictions - if you have mutable access to either parent or child, you can't have access to the other one. But you can have non-mutable access to both. If I had time, I'd go work on that.
- I've learned to live without objects, but the trait system is somewhat convoluted. There's one area of asset processing that really wants to be object oriented, and I have more duplicate code there than I like. I could probably rewrite it to use traits more, but it would take some bashing to make it fit the trait paradigm... """ -- [102] (my note: i think e meant Rust is data race free, not race condition free)
- https://lobste.rs/s/cryfiu/async_rust_is_bad_language
- "I think Rust’s async is fine. On the technical side, for what it does, it’s actually pretty good. Async calls and .await don’t need to do heap allocations. The Future trait is relatively simple (API surface of C++ coroutines seems much larger IMHO). Ability to use futures with a custom executor makes it possible to use them, efficiently, on top of other languages’ runtimes, or for clever applications like fuzzing protocols deterministically. Usability-wise, like many things in Rust, they have a steep frustrating learning curve, and a couple more usability issues than other Rust features. But once you learn how to use them, they’re fine. They don’t have any inherent terrible gotchas. Cancellation at any await point is the closest to a gotcha, but you can get away with that since Rust uses destructors and guards so much. In contrast with old node-style callbacks, which had issues no matter how experienced you were: subtlety of immediate vs deferred callbacks, risk of forgetting to call a callback on some code path or when an exception is thrown, need to deal with possibility of having a callback called more than once, etc. The famous color article implied that async should be an implementation detail to hide, but that’s merely a particular design choice, which chooses implicit magic over clarity and guarantees. Rust doesn’t hide error propagation. Rust doesn’t hide heap allocations, and Rust doesn’t hide await points. This is a feature, because Rust is used for low-level code where it can be absolutely necessary to have locks, threads, and syscalls behave in specific predictable ways. Having them sometimes invisibly replaced with something completely else would be as frightening as having UB. " -- https://lobste.rs/s/cryfiu/async_rust_is_bad_language#c_quwnov
- https://notgull.net/why-you-want-async/
- https://fasterthanli.me/articles/pin-and-suffering
- https://old.reddit.com/r/rust/comments/16v13l5/influxdb_officially_made_the_switch_from_go_rust/
- The Rust I Wanted Had No Future by graydon2
- "My biggest gripe about async is actually tokio… or rather the fact that you pretty much must use tokio. By deciding not to put an async runtime into std and not to put a fully fleshed out framework for generalizing async runtimes into std either, Rust created a situation where you end up with one de-facto standard runtime that everyone uses. You get the worst of both worlds then: it’s not standard, but you have no choice unless you want to fork a ton of code. Tokio is not even the best runtime. Smol is much better with its use of lifetimes and structured concurrency. The fact that dropping a JoinHandle? in tokio is a no-op is just awful. It’s also not the fastest runtime, though the difference these days is not large." -- [103]
- "There are a few traits that would already be very beneficial in std, such as AsyncRead?, AsyncWrite? and as a bonus AsyncDrop?. If we’d have the first two and a common way of spawning, it would be much easier to write runtime-independed libraries." -- [104]
Opinionated Comparisons
- "Other than OCaml and (maybe?) F#, I don't know of a language that has as sophisticated a pattern matcher as Rust does, which helps a lot with this stuff." -- https://news.ycombinator.com/item?id=9701086
- "in domains where GC is intolerable and C++ is considered too much of a risk" -- [105]
- "a safe, modern C++" -- [106]
- implicitly comparing Rust to Java and Golang, with Rust, "I had been shown the light, and there is no going back once you have been to the promised land." -- [107]
- compared to C: "C is still probably my most favorite language. It’s pure, raw power. It’s also a huge pain in the neck to debug, and there’s always that question, like what happens when you call strstr() on a non-null terminated buffer?" and "C; buffer overflows, out-of-bound array access, race conditions, etc" -- [108]
- vs. Go: https://blog.ntpsec.org/2017/01/18/rust-vs-go.html
- https://sdevprog.blogspot.com/2018/04/overview-of-efficient-programming.html?m=1
- http://arthurtw.github.io/2015/01/12/quick-comparison-nim-vs-rust.html
- https://medium.com/@george3d6/the-success-of-go-heralds-that-of-rust-73cb2e4c0500
- http://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python/
- https://blog.usejournal.com/systems-languages-an-experience-report-d008b2b12628
- vs C++: http://cliffle.com/blog/m4vga-in-rust/
- "I've settled into sort of a hierarchy. If I really want to hack out a small prototype quickly, I use NodeJS?. Not having types helps me change things around quickly. If I already have a pretty good idea of my data model but still want to develop quickly, I'll use Go. If it's something 1.0.0+ and I want to make it as reliable as possible, I'd use Rust. My problem seems to be few of my projects ever get to that stage, so I'm mostly writing Go these days... " [109]
- "I think ultimately the biggest Rust contribution will be for GC languages (tracing GC
RC) to also adopt some kind of early reclamation, similarly to Swift's ongoing approach, and we will reach a good enough situation and that will be it. " [110] |
- "Personally I often use Rust for "non systems-programming" tasks, even though I really wish a more suitable language existed. Rust has plenty of downsides, but hits a particular sweet spot for me that is hard to find elsewhere:
- Expressive, pretty powerful, ML and Haskell inspired type system
- Memory safe. In higher level code you have almost zero justification for `unsafe`, unless you really need a C library.
- Immutable by default. Can feel almost functional, depending on code style.
- Error handling (it's not perfect by any means, but much better than exceptions in my book)
- Very coherent language design. The language has few warts, in part thanks to the young age.
- Great package manager and build system.
- Good tooling in general (compiler errors, formatter, linter, docs generation, ... )
- Library availability is great for certain domains, decent for many.
- Statically compiled. Mostly statically linked. Though I often wish there was an additional interpreter/JIT with a REPL.
- Good performance without much effort.
- Good concurrency/parallelism primitives, especially since async
- Increasingly better IDE support, thanks to the author of this blog post! (rust-analyzer) So I often accept the downsides of Rust, even for higher level code, because I don't know another language that fits. My closest alternatives would probably be Go, Haskell or F#. But each don't fit the above list one way or another. " [111]
- "I enjoy the lack of inheritance in Rust." [112] (same author as previous bullet point)
- "I (hobby gamedev) moved from Rust to C a few weeks ago, here's something that made me switch: 1. Compilation time (no more words needed) 2. Forced to use Cargo for managing dependencies. The downside of a good dependency toolchain is that people will use it and there will be tons of indirect dependencies. For cross-platform development it's very common to see one of your indirect dependencies doesn't support the platform you wanted, and there's no way around it (even you know how to fix it you'll have to submit a pr and wait till the merge and crates.io release (takes years), or download the whole dependency chain and use everything locally which is a total mess) 3. Too strict. As a hobby gamedev the thing I value the most is the joy of programming, I don't want to spend all my time trying to figure out how to solve a borrow checker issue (you'll always face them no matter how good you're at Rust, and I don't want to use Rc<RefCell?<T>> everywhere), and I just want to be able to use global variables without ugly unsafe wrappers in my naive single threaded application. As a language I love every aspect of Rust, just ergonomically I don't want to deal with it now. " [113]
- "> (even you know how to fix it you'll have to submit a pr and wait till the merge and crates.io release (takes years), or download the whole dependency chain and use everything locally which is a total mess) For what it's worth, cargo supports patching dependencies without maintaining the whole dependency chain locally. See here: https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html " [114]
- "I adore Rust for it's unique combination of features:
- can write fast, and systems-level code; something typically dominated by c/++
- Makes standalone executables
- Modern, high-level language features
- Best-in-class tooling and docs (As the article points out) I'm not familiar with another language that can do these together. It's easy to pidgeonhole Rust as a "safe" language, but I adore it on a holistic level. I've found Rust's consolidated, official tooling makes the experience approachable for new people. " [115]
- "It's the modern high-level language features that get me. Despite Rust technically being a lower-level language, they are so well implemented that I often find Rust more enjoyable to code something in than other supposedly friendlier languages. The killer is the compile times. If Rust had Go-like compile times it would be almost perfect." [116]
- "Python is often my go to for writing tools, but I mistrust its typing and how it behaves when a library throws an exception - not forgetting that pylint warns if you try to catch Exception. Conversely, Rust error handling feels a bit more verbose, but I worry less about what it does in failure scenarios. I either handle the error or pass it up the stack and carry on with my day. " [117]
- "One major problem I experienced in my last job (where I worked on production rust) was the insane amount of macros and proc macros used. This was an originally C++ heavy shop so they leaned on it more than, say, a python shop moving to rust would." [118]
- "I'm hoping that with the advent of projects like autocxx, the C++ compatibility story will rapidly change....Rust's compatibility with other languages like Python and Javascript is already, imo, second to none thanks to a few well designed libraries and the macro system. The only language that comes close is C# with its IronPython? integration, except with Rust it's a lot easier for library authors to integrate runtimes. It took me less than a day to construct a JS/Python monstrosity with an absurd call stack (Node => Rust => Python => Rust => Node => Rust => Python) when I needed to wrangle together a bunch of legacy proprietary libraries in webapp form." [119]
- "Rust's typesystem really makes it a great fit for business applications. Totally agree. Kotlin comes close, though. Rust itself is production ready, but not the ecosystem." -- [120]
- "Traits do not make Rust more powerful than your average scripting language. They are much less powerful. The biggest hole here is the complete absence of any dynamicism. Rust types do not have any real runtime presence. Concretely, there is no way to dynamically check whether this value implements some Trait. This is used in Golang (Copy -> WriterTo?) but is impossible in Rust without manual implementation." -- [121]
- https://blog.logrocket.com/when-to-use-rust-and-when-to-use-golang/
- "I'm a fan of Rust for things like a missile control system or a stock exchange, but not, say, writing a CRM. I'd gladly write a CRM in Zig however, given the proper library support and tooling. Why? productivity. In my experience it just takes 2-3x longer to get the same thing done in Rust vs. something like Zig/Crystal/Go and certainly vs. Ruby/Python/etc. For extremely mission critical code, this is fine, but for most startups I would argue it probably isn't appropriate." [122]
- "While I like what Rust is trying to achieve, I would never use something that is not formally proven. There are many, many other static analysis tools in the C and Ada world that can achieve exactly the same thing. They imply less risk, they are way more mature (~40 yo) and have bigger communities around them." [123]
- "Agreed Rust has a bit of an odd fit -- it is in many ways a bit too onerous for day-to-day things like CRMs and web dev, but for the real scenarios where you want formal verification, you can of course also use actual formal methods and get a much higher degree of certainty perhaps. It is well suited for OS and driver dev though, which is arguably the target use case." [124]
- ""It is well suited for OS and driver dev though" Rust is brilliant, but for drivers, I would go for a language with OOM safety and strict static allocation, especially for embedded environments." [125]
- "Over the years, compilers added the capacity to drop assembly into C, but the verb here is apt: the resulting assembly was often dropped on its surrounding C like a Looney Tunes anvil, with the interface between the two often being ill-defined, compiler-dependent or both. Rust took this approach at first too, but it suffered from all of the historical problems of inline assembly — which in Rust’s case meant being highly dependent on LLVM implementation details...Fortunately, Amanieu d’Antras took on this gritty problem, and landed a new asm! syntax. The new syntax is a pleasure to work with, and frankly Rust has now leapfrogged C in terms of ease and robustness of integrating inline assembly!" -- [126]
- vs Zig: https://scattered-thoughts.net/writing/assorted-thoughts-on-zig-and-rust/
- vs F#: "Rust has excellent tooling, great libraries, a delightful community, etc. But after spending about a month on it, I can't say I like writing Rust. Especially, I don't like writing async code in Rust. I like a nice high level language, and that's kinda what you need when you have a project as big as Dark to build. And Rust is not that. I'll publish "Why Dark didn't choose Rust" next. Or I might call it "you'll never believe just how much a Garbage Collector does for you!", because that's the summary." -- Paul Biggar
- vs Ocaml: https://blog.darklang.com/first-thoughts-on-rust-vs-ocaml/ "I actually wrote quite a bit on why I didn't like Rust a few weeks ago. I think those main reasons stand, so I'll just link to them rather than repeat the 1800 words again. As a quick summary, the good parts were:
- tooling is great
- library ecosystem is great
- community is great
- macros are nice (though I feel I was overusing them to cover problems in the language) and the bad parts were
- having to do memory management sucks
- pattern matching doesn't work all that well
- too many ways to do things (Arc vs Rc, async vs sync, different stdlibs)
- the language isn't immutable
- having to fight the compiler Again, for more on those, have a read of the previous post. Ultimately, when it came time to decide, it came down to a few major things: missing a GCP library, and the low-level nature of the language."
- vs Ocaml: "Overall, Rust the language is frustrating but not terrible, while the tooling, community and ecosystem are truly excellent. Meanwhile, OCaml has poor tooling, poor ecosystem, and no community, while the language itself is truly excellent (except for the horrendous syntax, of course)." -- Paul Biggar
- "Cargo is probably the best build system I've ever used. The fact that they have integrated the package manager, the build system, and the compiler, and then it all just works? Magic!" -- Paul Biggar
- "Too many ways to do things...I've already seen the seeds of Rc vs Arc. I'm using im, a library of immutable data structures (more on that later). I read that the idiomatic way to do composable errors in Rust is to use error-chain, which uses Arc. I was using im with Rc and that wasn't compatible with the Arcs in error-chain. This seems to have come up before, as im actually has two libraries: im (which uses Arc) and im-rc (which uses Rc). The fact that there are two identical libraries with identical functionality, except that one uses Rc and the other uses Arc, seems to be a giant red flag that I feel is going to cause me a lot of trouble some day. Similarly, it seems that sync and async Rust use entirely different standard libraries, which is also a big red flag. Do all flavors of im and Rc/Arc and sync/async play well together? No idea, but finding out is not an exciting prospect." -- Paul Biggar
- vs Go: https://bitfieldconsulting.com/golang/rust-vs-go
- "Rust’s poor compile times are not the result of its advanced type system, but of a combination of other factors. Some are essential, like the runtime guarantees it makes (e.g. monomorphization) whereas others are accidental, like some aspects of its module system." [127]
- " In the case of BEAM NIFs, I find Rust's compiler guarantees especially compelling. That's because a crashing NIF can take down the whole Erlang VM. If fault-tolerance is what brought you to Erlang/Elixir and OTP to begin with, as it is for many, having the server crash could be a major issue." AlchemistCamp
- "True, Rust has the strongest guarantees. Nim offers similar guarantees overall (checked arrays etc) and Nimler has exception checking enabled so the compilers checks if you have any possible checkable exceptions. Personally, I use Nim since it’s easier to use on embedded Linux devices by precompiling to C. Rust has some odd dependencies on the target’s C linker but doesn’t check the standard $CC or $LD variables. Zig is interesting but it offers the least memory protection." elcritch
- "Rust has some nice quality-of-life improvements that make up for the toil of memory management. Even when I don’t strictly need a systems programming language, I still use Rust, because I like sum types, pattern matching, foolproof resource cleanup, and everything-is-an-expression so much. So for glue code in GUI apps or CRUD webapps I’d love some Rustified TypeScript?, Rustscript on the Golang runtime, or a Rust/Swift hybrid." [128]
- Q: "...I’m curious, which features of Rust do you miss when using TypeScript?? The obvious one is traits, but I’m curious if there’s anything else."? A: "Not the person you replied to, but true sum types, especially Option. It’s much cleaner than undefined, even with all of the nice chaining operators. And not having to worry about exceptions thanks to Result." [129]
- "Ah, I see your point. TypeScript? discriminated unions are used frequently to overcome this limitation, but I agree it would be preferable to have proper sum types with pattern-matching."
- "Oh god I miss TypeScript’s? sum types so much when I’m writing Rust. When I context switch back to Rust after writing some TypeScript? I with I could do type Foo = Bar
- "I’d probably fix the kinda-broken Ord and Eq hierarchies that Rust inherited from Haskell. That would be an easy win. Simplicity-wise, it would probably make sense to move the decision of reference type vs. value type to the type definition site, and not make it a choice in e. g. method signatures. Syntactically, getting rid of the distinction between :: and . (by removing ::) and not using <> for generics gives probably the biggest gains there. Also, replacing -> with : in function result type signatures. Oh, forgot: Drop mandatory semicola – it’s 2020. Looks like I’ll be collecting lots of flags from Rust fans for this … ;-)" [133]
- "...The combination of strict syntax, strong typing, and helpful compiler with auto-fixes ensures that accidentally inserted or forgotten semicolon isn’t a problem. Then it works beautifully with the “last expression in a block is the block’s value” rule. It makes the syntax so much more elegant and orthogonal: no need for ternary operators. let val = { block } makes immutable bindings practical..." [134]
- "This is a much more eloquent expression of exactly what I had in mind with my original comment. The way semicolons work in Rust means that, rather than being an arbitrary line ending marker that serves as a crutch for the parser, they’re just another piece of syntax that expresses a concept in the language. A semicolon at the end of a line means “this line as a whole should not resolve to a value”, whereas no semicolon means the opposite." [135]
- "“almost all other languages” don’t have proper expression blocks, outside the functional ones. The ; in rust makes a lot of sense if you realize it’s a separator, not a terminator. a; b is just evaluating a, discards its results, and evaluates the same as b. OCaml does the exact same thing. You’re allowed to write { … a; } as a shortcut for { …; a; () } but that’s just sugar." [136]
- Q: "What problems do you have with Ord and Eq? If it’s about how they work with floats, I for one appreciate how that works, due to the number of times I’ve caught folks using equality testing on floats in unit tests." A: " I prefer designs where collection operations work reliably, regardless of the type of the values I put into them. So not this: Scala: List(Double.NaN?).contains(Double.NaN?) false Rust: &[0.0/0.0].contains(0.0/0.0) false Haskell: elem (0.0/0.0) [0.0/0.0] false It’s easy to get right, and there is little excuse to get it wrong for languages designed in the 21st century. " [137]
- "Ah, so you’d prefer that collection operations depend on Eq instead of PartialEq?? This is one of those things where 50% of the people expect it to work one way, and 50% expect the other. If you enforce correctness with Eq, then people will complain about the lengths they have to go through to check that there’s not a NaN? somewhere. If you allow flexibility with PartialEq?, then you get the confusing behavior your illustrated. This is why I push back against the “principle of least astonishment”. And it’s why there’s no such thing as languages “getting [this] wrong”. I assumed you disliked the separation between PartialEq? and Eq, whereas I like having the distinction. So I wanted to provide a real-world use case where too many people fall back on strict equality checking on floats, instead of doing a comparison with an acceptable range. But it appears you do like the distinction. And not only that, rust’s == works on PartialEq?, when I thought it was based on Eq. Whoops. "
- " No, the problem is the way Haskell and Rust put partial and total orderings into a hierarchy, probably enticed by the extremely poor naming choices they made along with that. Instead of Rust’s/Haskell’s approach, have two different kind of traits/typeclasses that are completely separate. I named them Identity and Equality. Then you can provide two different methods that offer both suggested behaviors in the case of “is x in thing”?. "
- "Ah, I like that solution."
- "I’m working on some ‘pretty big’ (several kilolines) project on rust, and two things that frustrate me to no end: All the stuff around strings. Especially in non-systems programming there’s so much with string literals and the like, and Rust requires a lot of fidgeting. Let’s not even get into returning heap-allocated strings cleanly from local functions. I (think) I get why it’s all like this, but it’s still annoying, despite all the aids involved Refactoring is a massive pain! It’s super hard to “test” different data structures, especially when it comes to stuff involving lifetimes. You have to basically rewrite everything. It doesn’t help that you can’t have “placeholder” lifetimes, so when you try removing a thing you gotta rewrite a bunch of code. The refactoring point is really important I think for people not super proficient in systems design. When you realize you gotta re-work your structure, especially when you have a bunch of pattern matching, you’re giving yourself a lot of busywork. For me this is a very similar problem that other ADT-based languages (Haskell and the like) face. Sure, you’re going to check all usages, but sometimes I just want to add a field without changing 3000 lines. I still am really up for using it for systems stuff but it’s super painful, and makes me miss Python a lot. When I finally get a thing working I’m really happy though :) " [138]
- "Your pain points sound similar to what I disliked about Rust when I was starting. In my case these were symptoms of not “getting” ownership. The difference between &str and String/Box<str> is easy once you know it. If it’s not obvious to you, you will be unable to use Rust productively. The borrow checker will get in your way when you “just” want to return something from a function. A lot of people intuitively equate Rust’s references with returning and storing “by reference” in other languages. That’s totally wrong! They’re almost the opposite of that. Rust references aren’t for “not copying” (there are other types that do that too). They’re for “not owning”, and that has specific uses and serious consequences you have to internalize. Similarly, if you add a reference (i.e. a temporary scope-limited borrow) to a struct, it blows up the whole program with lifetime annotations. It’s hell. <'a> everywhere. That’s not because Rust has such crappy syntax, but because it’s basically a mistake of using wrong semantics. It means data of the struct is stored outside of the struct, on stack in some random place. There’s a valid use-case for such stack-bound-temp-struct-wrappers, but they’re not nearly as common as when it’s done by mistake. Use Box or other owning types in structs to store by reference. And these aren’t actually Rust-specific problems. In C the difference between &str and Box<str> is whether you must call free() on it, or must not. The <'a> is “be careful, don’t use it after freeing that other thing”. Sometimes C allows both ways, and structs have bool should_free_that_pointer;. That’s Cow<str> in Rust. " [139]
- "Indeed, but I think this proves the “Complexity” section of TFA. There are several ways to do things including: References Boxed pointers RC pointers ARC pointers COW pointers Cells RefCells? There’s a lot of expressive power there, and these certainly help in allowing memory-safe low-level programming. But it’s a lot of choice. Moreso than C++. " [140]
- "Error Handling. When it comes to null safety, Kotlin and Rust are mostly equivalent in practice. There are some finer distinctions here between union types vs sum types, but they are irrelevant in real code in my experience. Syntactically, Kotlin’s take on ? and ?: feels a little more convenient more often. However, when it comes to error handling (Result<T, E> rather than Option<T>), Rust wins hands down. Having ? annotating error paths on the call site is very valuable. Encoding errors in function’s return type, in a way which works with high-order functions, makes for robust code. I dread calling external processes in Kotlin and Python, because it is exactly the place where exceptions are common, and where I forget to handle at least one case every single time. " [141]
- "Although Rust’s types and expressions usually allow one to state precisely what one wants, there are still cases when the borrow checker gets in a way. For example, here we can’t return an iterator which wants to borrow from a temporary: utils.rs. When learning Rust problems of this kind are very frequent. This is primarily because applying the traditional “soup of pointers” design to Rust doesn’t work. With experience, design-related borrow checker errors tend to fade away — building software as a tree of components works, and it is almost always a good design. The residual borrow checker limitations are annoying, but don’t matter in the grand scheme of things." [142]
- Re: Integrating "safe" languages into OpenBSD?
- " Knowing one part of Rust doesn’t help me know another part and there are special cases and exceptions everywhere (?Sized, type+lifetime declarations that are 100+ characters long, etc). The module system seems overly complicated. I can’t just solve my problem, I have to wrestle with my language. Now, I will say that I don’t have a whole lot of Rust experience: maybe 100 hours. But I know that I was able to write good, working, productive code in Go well before 100 hours (and had fun doing it). After just 10 hours of Swift, I was able to do some good stuff. Rust to me is like C++: just endless features thrown together. ... That being said, I’m still doing my new project in Rust. Let me give you my reasons:
- all the momentum appears to be behind Rust...
- cross-language libraries...
- libraries
- despite my criticism, I do like the strong safety guarantees Rust provides and I do like large parts of the language (traits, the well-defined iterator semantics, cargo, ADT-style enums, etc). At the end of the day, I need a language that can run close to the hardware, without a heavy runtime, that can write libraries for other languages, and has a good library ecosystem of its own. That pretty much means C, C++, or Rust. It’s just a shame that Rust isn’t…fun. (And I am really sad that Swift on non-Apple platforms/as a systems language appears to be DOA.) " [143]
- "I was able to write some nontrivial code in it, but it was a constant fight with the compiler, everything was way more verbose than I liked, and ultimately it was less enjoyable than my daily-driver C++. Currently my “fun” language is Nim. It’s fast, low-overhead, integrates super well with C, and is mostly quite fun to use. It’s a bit too loosely-goosey with safety — the unsafe language constructs are very close to the surface and too easy to reach for — and the community is strongly against adding Rust-style safety, but I can live with that. " [144]
- "Maybe someone needs to write a Rust-- that cuts out most of the complexity." [145]
- "You might be interested in reading this post from withoutboats, a well known Rust contributor." [146]
- "From what I’ve seen, that would be Swift..." [147]
- "This brings to mind Type Checking vs. Metaprogramming; ML vs. Lisp. Zig is way better at metaprogramming, but doesn’t give you great safety guarantees. Rust is better at type checking, but has at least 3 different kinds of metaprogramming to patch over usability/composability holes created by that rigidity (2 kinds of macros and const contexts) " [148]
- "I won’t lie; after reading this, it has further pushed my doubts about Rust. This is coming from someone who uses Rust in production and hobby projects. My largest issue was the fact that it had no standards document. My second issue is there’s only one implementation which is complete - with mrustc probably forever trailing behind. This article really highlights another big glaring issue: compiler time programming is terrible. And I thought maybe dependency sizes were justifiable in some “futuristic” way, but Zig shows otherwise. IMO this is not a problem because it can be pecked at over time. The rest of the article seems to be the author struggling with types. Rust’s type system and the borrow checker are probably the best things about it. I find if you stay within the realm of these two things, you’re going to have a really good time. " [149]
- "Lately, maybe because of my age, I’ve begun feeling uneasy about the increase in complexity of Rust. One can understand why many of the recent features were introduced (async, const generics, generic associated types, etc.) and in isolation, they seem perfectly justified. But when looking at the whole package, I feel the complexity budget of Rust is now too far into the red for my personal comfort. That said, I will keep using Rust for the near and medium future (although some younger Rustaceans might have issues with my increasingly un-modern style of Rust). It has a good ecosystem, good tooling, allows fast-as-C code, and most importantly to me: it helps to keep me away from memory safety issues. We’ve seen the articles about how ~70% of vulnerabilities are due to memory bugs and Rust’s ownership model and borrow checker give every user of the language a common tool to prevent those vulnerabilities. (By the way, I think it’s fascinating that 70% is a figure that has been found independently by many organizations.) Nevertheless, I keep eyeing Zig as a possible escape hatch from Rust. The older I get, the closer I find myself philosophically to Zig. The idea of mastering a smaller tool is becoming more attractive to greying me than learning ever more features. (In other “get off my lawn” technology opinions, I completely distrust IoT? appliances and I want buttons and knobs and not touch screens.) I’ve played with Zig a little bit, and I liked what I saw. I don’t think they have an answer to the 70% problem above, but if they came up with something for it, even if it was something not as effective at finding problems as Rust’s borrow checker, I would definitely start looking seriously at using it for some of my side projects. " [150]
- vs Zig, on safety: https://scattered-thoughts.net/writing/how-safe-is-zig/
- "If I'm calling an NFS library, say, on Windows I need to use UNIX paths. Rust needs WindowsString? and UnixString? on every platform, with OsString? as a synonym for whichever is most useful locally...In this case it's nuts that I need to implement my own UnixString? because the standard library doesn't expose it, and when I run on Linux I have two incompatible versions of the same thing." [151]
- "The "as" operator is often considered to have been a mistake. Both because of unchecked casts and because of "doing to much". So I wouldn't be surprised if in the (very) long term there will be a rust edition deprecating `as` casts (after we have alternatives to all cast done with `as`, which are: Pointer casts, dyn casts/explicit coercion and truncating integer casts, for some we already have alternatives for on stable for other not). And for all who want to not have `as` today you can combine extension traits (which internally still use `as`) + clippy lint against any usage of `as`." [152]
- vs Java, Lisp: https://renato.athaydes.com/posts/revisiting-prechelt-paper-comparing-languages.html
- vs Zig: https://expandingman.gitlab.io/tvu-compare/
- vs C: https://pngquant.org/rust.html
- vs C++: https://www.thecodedmessage.com/tags/rust-vs-c++/
- vs C++, Go: https://news.ycombinator.com/item?id=31435739
- a comparison of a loop in Rust vs Python syntax: https://news.ycombinator.com/item?id=31437554
- vs C# and JS: "> I don't want to work with languages that require a relatively heavy cognitive load on simply reading the lines of the source code Strongly agree on this, I haven't tried to introduce it where I work for the same reason. The cognitive load is massive compared to a language like C# or JS and the gain is minimal for the average developer writing microservices for React frontends. In this context you need a JSON serializer, iterators and maybe generics, and Rust is not much better than C# on this front." [153]
- vs C++ templates: "...the false equivalency between C++ template metaprogramming and generic programming in languages with expressive static typing. It's not clever or inscrutable like templates, quite the opposite. It's explicit about constraint. Generic Rust makes it easier to understand complex code and write it correctly." -- [154]
- vs languages without explicit lifetimes: "pretty much every programmer needs to be aware of thinking about lifetimes. The classic issues are creating a local and then giving a reference to it, to something much longer lived, or even undying. In that last case, if your doing it a lot, with no regard to the objects change in lifetime, your effectively creating a leak (I can guarantee you someone, somewhere, is making this mistake in js right now). Most collection strategies won't touch an object that still has a reference to it. The second issue that became really common when people stopped paying attention to lifetimes, is that many resources you may be using (file handles, db connections, etc...) have very different constraints than memory. So you have to be cognizant of the fact that even though something has gone out of scope, it's lifetime really doesn't end until the gc gets around to collecting it. This is the reason special syntax and functions, like "with" and Dispose had to be added to languages like C# and Java. Python with it's reference counting maybe somewhat less susceptible to this second issue than the others, but it's not immune to it. Finally in many cases being aware of object lifetimes and specifically manipulating them can get you performance speed ups." -- [155]
- "I use rust weekly and I find it to have the best DX. I have done work with Oracle Java 5-8, IBM XL C99, MSVC++11, CPython 2-3, C# .NET Core 3.1. Stable Rust 2021 is overall the most readable, least surprising, BUT only with the right tool which also makes it the most discoverable, with rust-analyzer. My only gripe is the lack of consensus on strongly typed error handling (anyhow+thiserror being the most sensible combination I found after moving away from bare Results, to failure, to just anyhow). " -- [156]
- "The wire pulling example is exactly what I find to be the thing about coding in rust. If you stay within some default confines, you never get spooky action at a distance. Outside of OCaml I haven’t had that experience in other languages. It seems like Java or C++ should be close, but the write/compile/fix loop isn’t as tight in those and you end up with more chaotic trouble than the error guided refactoring I experience in Rust." -- [157]
- "...I’ve been working on some complex software in Rust at $WORK (https://securedna.org), and I’ve definitely written some hairballs as I was wrapping my brain around the problem domain. But when it comes to smoothing them out, it’s just… way… easier than in any other language I’ve written. Compared to languages that have commonly-used “holes” in the type system (as and any in Typescript, void* in C, ad-hoc casting in Java, ~everything in typed Python), I can do refactors until the compiler stops complaining, run the code, and it passes all the tests first time no problems. Part of this is the type system / surrounding constructs – things like exhaustiveness-checked-by-default enum matching, explicitly passed results instead of exceptions that bubble up in unexpected places, etc. However, a lot of it is a stdlib and library author focus on writing code that does the reasonable thing, catches as much as possible at compile time with enums and typestates, and forces you to think about the remaining edge cases by returning appropriate Result / Option types. Issues about adding more compile-time checks to APIs and making invalid states unrepresentable being a common sight in Rust OSS repos is just as important to the Rust developer experience as any language feature." -- [158]
- some comments on Rust syntax: https://lobste.rs/s/wiavtb/rust_critical_retrospective#c_e3k6ph
- vs F#: https://cragwind.com/blog/posts/comparing-voxel-game-fsharp-rust/
- vs Go: "Imo, Go does performant concurrency right. Rust would be smart to adopt what Go offers." -- [159]
- vs Go: "...Rust is a better option when you also need one or more of the following: a) tight control over allocations b) deterministic performance c) strong data race protection d) zero overhead C interop (e.g., processing audio using ffmpeg) " -- [160]
- vs Python: "For me, I’m currently running a Rust webapp and a Python webapp on a server; both are of similar complexities, and I’m probably a bit better at Python. I keep on having to fix the Python app, because of both OS upgrades and problems with the implementation. The Rust app has been working without any issues for the last three years. For a relatively simple web server, where I don’t need to collaborate with anyone, I’m choosing Rust." -- [161]
- vs Golang and Python: "I've got a similar thing. I wrote a bespoke time-series database in Rust a few years ago, and it has had exactly one issue since I stood it up in production, and that was due to pessimistic filesystem access patterns, rather than the language. This thing is handling hundreds of thousands of inserts per second, and it's even threaded. Given that I've been programming professionally for over a decade in Python, Perl, Ruby, C, C++, Javascript, Java, and Rust, I'll pick Rust absolutely any time that I want something running that I won't get called at 3 AM to fix. It probably took me 5 times as long to write it as if I did it in Go or Python, but I guarantee it's saved me 10 times as much time I would have otherwise spent triaging, debugging, and running disaster recovery." -- [162]
- vs C, Zig: https://www.scattered-thoughts.net/writing/how-safe-is-zig/
- vs Python: "For those not familiar with ownership concepts, it does apply to many things beyond just memory management. Most obvious example of this is file handles. In python you have to close() them or scope within a context manager to auto close when the block exits. If you need more complex logic around, such as transfer the handle into another function, the context manager is no longer sufficient and you quickly loose track of who should call close(), is it the caller or the callee. Rust keeps track of this, no matter how deep you twist your logic and it does so for every variable, no context manager blocks required." -- [163]
- vs Python: "Dev velocity, which was supposed to be the claim to fame of Python, improved dramatically with Rust." -- [164]
- vs Python and Golang: "I've been a professional Python developer for 15 years, and I can't believe Python ever had the reputation for "high dev velocity" beyond toy examples. In every real world code base I've worked in, Python has been a strict liability and the promise that you can "just rewrite the slow parts in C/C++/Numpy/etc" has never held (you very often spend at least as much time marshaling the data to the target language format than you gain by processing in the faster language and you have all of the maintainability problems of C/C++/Numpy). Python trades developer velocity for poor runtime performance. I don't think Rust is the paragon of high dev velocity languages either, but it seems to be more predictable. You don't have something that works as a prototype, but then you run into a bunch of performance problems (which virtually cannot be worked around) when you go to productionize, nor do you get all of the emergent quality and maintainability issues that come about from a dynamic type system (and last I checked, Python's optional static type system was still pretty alpha-grade). I strongly recommend avoiding Python for new projects. Use Go if you're looking for an easy GC language with max productivity and a decent performance ceiling. Use Rust if you're writing really high performance or correctness-is-paramount software. I'm sure there are other decent options as well (I've heard generally good things about TypeScript?, but I'd be concerned about its performance even if it is better than Python). -- [165]
- vs Golang: "I'd add one more addition to use Rust (speaking from an ex-Go dev): Use Rust if you want a robust std lib. Go is good, i used it for ~5 years, but man Rust was a breath of fresh air with the amount of tooling that helped me solve problems. Iterators, are a great example. I also preferred Crates vs Github Repos, but i think that's just personal preference.. not objective." -- [166]
- vs Golang, for CRUD: on Go: "if you value utmost simplicity more than sum types, that is a splendid choice. I think Go is the single language which is in the same class as Rust when it comes to “quality of implementation”, and, arguably, it’s even better than Rust in this respect. If only it had enums and null-checking..." -- [167]
- vs OCaml, for CRUD: "OCaml -- this one is pretty ideal when it comes to the language machinery, but two+ build systems, two standard libraries, etc, make it not a reasonable choice in comparison" -- [168]
- (futher comment on the previous two bullet points, by the same source): "On balance, I would say Rust’s “quality of implementation” story is ahead of mature languages, and that’s exactly what creates this “let’s write CRUD in Rust” pressure...Really, it seems that there’s “Go with enums / OCaml with channels and Cargo”-shaped hole in our language-space these days, which is pretty imperfectly covered by existing options. I really wish that such language existed, this would relieve a lot of design pressure from Rust and allow it to focus on systems use-case more." -- [169]
- someone else points out "Ocaml 5 has channels along with the new multi core support. And algebraic effects." -- [170]
- vs C++ (blog post series): https://www.thecodedmessage.com/tags/rust-vs-c++/
- https://www.thecodedmessage.com/posts/cpp-move/
- "C++, unlike Rust, does not require that the bytes contained in an object mean the same thing in any arbitrary location. The object could contain a reference to itself, or to part of itself, that would be invalidated by moving it. Or, there could be a data structure somewhere with a reference to it, that would need to be updated. C++ would have to give types an opportunity to address such things. Safe Rust forbids these things. The lifetime of a value takes moves into account; you can’t move from a value unless there are no references to it. And in safe Rust, there is no way for the user to create a self-referential value (though the compiler can in its implementation of async – but only if the value is already “pinned,” which we will discuss in a moment)." -- https://www.thecodedmessage.com/posts/cpp-move/
- vs Ocaml: https://hirrolot.github.io/posts/compiler-development-rust-or-ocaml.html
- "My naive hope would be that we start making threads and context switching more lightweight....In the short term I just use Go and accept the runtime overhead in return for being able to avoid the function coloring problem and also get access to a decent integrated epoll event loop." -- [171]
- vs Golang: https://www.shuttle.rs/blog/2023/09/27/rust-vs-go-comparison
- vs Zig: https://alic.dev/blog/dense-enums
Dev blogs
Best practices
Retrospectives
- https://github.com/rust-lang/rfcs/tree/master/text
- Graydon's Five lists of six things about Rust
- https://doc.rust-lang.org/reference/influences.html
- http://venge.net/graydon/talks/rust-2012.pdf
- https://brson.github.io/2021/05/02/rusts-most-unrecognized-contributor
- https://lobste.rs/s/nw07cy/rust_s_most_unrecognized_contributor
- https://www.infoq.com/presentations/rust-2019/
- Project Servo - Technology from the past come to save the future from itself. Graydon describes as "The first public presentation about (or even admission I was working on) Rust. Very retro, way overconfident, needlessly hard on C++. Sorry! Posted here for posterity / amusement."
- Rust - A safe, concurrent, practical language. Graydon describes as "Second talk I gave on Rust after a couple years of getting beat up by the "last 10%", at a lunch break at Tantalus, a power-control company where Frances was working at the time. On reflection, a good enough set of slides to show outside that small room."
- "Rust wants to make systems programming memory safe (and type safe while at it). It's not really about any specific language, it's about dragging the field forwards on the safety front. Well it's not really Rust, it's the Rust community, which kinda wrestled it away from Graydon Hoare: the original inception of Rust was more of an applications language (what with the evented model and the split stack and the planned-though-never-really-implemented GC'd pointers) — which explains part of the historical confusion with / comparison to Go. But then a critical mass of people took a look at this early rust and figured "we've got plenty of memory-safe and type-safe (ish) languages for applications, but this thing has precise control over memory and lifetimes and shit, we could actually build reliable infrastructure with that", and it coincided with a lot of memory safety issues news (which has been continuing ever since), and the rest is history, pretty much. " masklinn
- a 4-part series about why Rust compiles slowly by Brian Anderson (founding member of the Rust and Servo projects): https://pingcap.com/blog/rust-compilation-model-calamity https://pingcap.com/blog/generics-and-compile-time-in-rust https://pingcap.com/blog/rust-huge-compilation-units https://pingcap.com/blog/reasons-rust-compiles-slowly
- old rust:
Rust Gotchas
- "sometimes you can't do a.b().c(), but have to write tmp = a.b(); tmp.c()." [174]
- "assignment in Rust is not a totally trivial topic. Thanks to Rust’s memory checking, this program produces an error:
enum MyValue? { Digit(i32) }
fn main() { let x = MyValue::Digit(10); let y = x; let z = x; }
The reason is that z might (later) mess with x, leaving y in an invalid state (a consequence of Rust’s strict memory checking — more on that later). Fair enough. But then changing the top of the file to:
- [derive(Copy, Clone)] enum MyValue? { Digit(i32) }
makes the program compile. Now, instead of binding y to the value represented by x, the assignment operator copies the value of x to y. So z can have no possible effect on y, and the memory-safety gods are happy.
To me it seems a little strange that a pragma on the enum affects its assignment semantics. It makes it difficult to reason about a program without first reading all the definitions. The binding-copy duality, by the way, is another artifact of Rust’s mixed-paradigm heritage. Whereas functional languages tend to use bindings to map variable names to their values in the invisible spirit world of no-copy immutable data, imperative languages take names at face value, and assignment is always a copy (copying a pointer, perhaps, but still a copy). By attempting to straddle both worlds, Rust rather inelegantly overloads the assignment operator to mean either binding or copying. " [175] this is partially addressed in [176]
- "...I was surprised to find out how the unsafe keyword actually works. My initial belief was that a function that does something unsafe must, itself, be unsafe, but in reality it’s more of an honor code system. A function that does something unsafe (e.g. pointer arithmetic or calling C libraries) can be exported as safe. This may come as a surprise if you are using someone else’s Rust libraries, as it’s possible they’re doing something unsafe under the hood and just not telling you about it. Of course, the lax rules about un-safety are rather convenient if you want to do something unsafe in a trait (interface) implementation. If the unsafe keyword propagated up the call chain, then you’d be unable to implement RAII or iterators with any unsafe logic, because in Rust, safe and unsafe functions have incompatible method signatures, and RAII and iterator methods are marked safe." [177]
- this is partially addressed in [178]
- https://ridiculousfish.com/blog/posts/least-favorite-rust-type.html (range) "I’m comfortable with C/++ and Java, to a greater extent than a lot of people I’ve worked with. There are still things in Rust that are papercuts:
- The need to put :: before <> in expressions for generic parameters, but not in type signatures.
- Using :: instead of just . even though most of the time the naming convention will delineate the difference anyway
- Error handling with futures
- Self-referential structs
- Passing a shared pointer through multiple move closures IMHO the frustration involved in these things is not typically worth being more explicit. The :: thing may seem a bit pedantic, but it is simply more physical effort (Shift-colon-colon vs dot) and creates more visual noise. If/when all of the things above are addressed, I think Rust could give Python a run for its money. " -- [179]
- "Rust is unpleasant to look at. I know its silly, but that my biggest gripe with it as well, aside from sometimes overly complex type signatures due to generics" -- [180]
- "Compared to Go where closures look just like function definitions, closures in Rust look needlessly complex." [181]
- "I also always find myself wanting to type mut &a instead of the correct &mut a. The former just seems more obvious to me, a reference to a is &a. Mutable a is mut a. So a mutable reference must be mut &a." [182]
- "I have the same experience - I don't know what the hell is going on in the code with Rust. Compared to say F#, its far more verbose and feels too much like C++." [183]
- "Maybe it's my background (Java and JS with a little bit of C++), but I don't find it ((the syntax)) alien at all. I do find it really clunky in a handful of situations, mostly having to do with lots of nested containers (Option<Arc<Box<...), but luckily those are uncommon for most use-cases. " [184]
- "The bit which seems the most cumbersome to me is working with things like str/String and Path - it seems like there's always a lot of finagling and long chains of method calls to get things into the right type representation for a particular use-case. I'm still not so experienced though so maybe it gets better." [185]
- "I think what helps with that is building up a mental model of where things actually live in memory. If you create a new String, you're putting it on the heap. Which makes sense, because strings can be of any arbitrary length. It also makes sense, then, that you can mutate these heap-strings, because they have room to grow. In contrast, the only strings that can go on the stack (so you don't need String::new) are ones whose lengths are known at compile-time. So, a string literal (str). There are many things like this in Rust where if you just follow the compiler errors until you can make it happy, things will end up really convoluted and baffling. They'll also probably not perform as well. To really use Rust you have to absorb what's actually going on underneath its protections, and not just the errors that surface from them. This isn't easy, and is probably the dividing line between people who stick with it and people who decide it's not for them. You get about 50% of this picture for free (thinking in terms of stack, heap, references) if you've done C/C++ before. The other 50% is completely unique to Rust. But the key is to read the Book, follow guides, etc. Rust isn't really something you can learn just by hacking it out on your own, unfortunately." -- [186]
- https://www.brandons.me/blog/why-rust-strings-seem-hard
- "Rust is ill-suited for generic `async` programming, because when you enter `async`, you observe that many other language features suddenly break down: references, closures, type system, to name a few. From the perspective of language design, this manifests a failure to design an orthogonal language." -- [187] and [188]
- " Still no async traits. Still no async closures. Quite painful when you need to move stuff into it. Still no async iterators. Working with Streams is painful, the terminology is inconsistent, many iterator methods are missing. Pin is a huge ball of complexity dumped into the language, and it's basically useless outside of writing async (i.e. if you think it will help with your self-referential/non-movable type, think again). Anything meaningful done with it requires unsafe. At least there are pin and pin_project macros which automate some of it. Basically all fundamental async stuff is still in crates and not in libstd. No way to abstract over executors, leading to ecosystem split and de-facto monopoly of Tokio. If you aren't Google, writing a new executor isn't worth the hell of rewriting the whole ecosystem around it, so Rust could just go with a built-in executor to the same effect, saving people from a lot of pain. No way to abstract over sync/async, leading to ecosystem split and infectious async. Yes, basically the whole ecosystem from libstd upwards needs to be rewritten for async. Even bloody serde and rand. select! macro is a mess. Debugging and stacktraces are useless for async. Generators are still not stable. Personally, for me pretty state machine syntax is like 95% benefits of async, but I'm forced to drag all the mess of executors and async IO with it. Implicit Send/Sync propagation of async fn types is a mess. Lack of async Drop is a huge pain point. Future cancellation in general is a mess. I could go on. Have you seen the async vision project from the async-wg? Basically everything on that list tells how async Rust is inferior to normal Rust, and thus is a gaping debt hole. " -- https://www.reddit.com/r/rust/comments/v3cktw/comment/ib0mp49/?utm_source=reddit&utm_medium=web2x&context=3 (recommended by https://news.ycombinator.com/item?id=31612300 )
- "This API is the only bad thing about Rust! .expect("Could not read file") It’s so unfortunate to have an API that reads .expect("thing we don’t expect") " -- [189]
- " The problem is that async in Rust is, as other people have put it, viral. Example: I have some stuff in my hobby application that is executing in a WASM virtual machine. That's not async, simple and synchronous. But I have two other parts: one that talks to FoundationDB?, and another that receives websocket connections. Both use Tokio & Async. Now I'm in a pickle every time I want to hold or pass around some state in my WASM pieces, because the async stuff ends up pushing its constraints all the way down. Want some piece of mutability? Some piece that doesn't Send or Copy? Good luck. There's ways around it all, but it complicates the design. The 'async' pieces at the front end up propagating all the way down. It's hard to explain fully without you being in my code, but suffice it to say, I agree with others: async in Rust is half-baked. That should be evident enough simply from the fact that you can't even yet put async functions in traits. " -- [190]
- https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md
Suggestions and suggested variants
Rust Features
Ownership types
Links:
Older discussions (Rust has probably changed a lot since these):
Lifetimes (related to ownership types)
- note: in Rust, regions are called 'lifetimes' [191]
Procedural Macros
Rust custom derive and procedural macros: https://doc.rust-lang.org/book/procedural-macros.html
Constant expression
- https://doc.rust-lang.org/reference/const_eval.html#constant-expressions
- " Literals. Paths to functions and constants. Recursively defining constants is not allowed. Tuple expressions. Array expressions. Struct expressions. Enum variant expressions. Block expressions, including unsafe blocks. let statements and thus irrefutable patterns, with the caveat that until if and match are implemented, one cannot use both short circuiting operators (&& and
) and let statements within the same constant. |
assignment expressions
compound assignment expressions
expression statements
Field expressions.
Index expressions, array indexing or slice with a usize.
Range expressions.
Closure expressions which don't capture variables from the environment.
Built-in negation, arithmetic, logical, comparison or lazy boolean operators used on integer and floating point types, bool, and char.
Shared borrows, except if applied to a type with interior mutability.
The dereference operator.
Grouped expressions.
Cast expressions, except pointer to address and function pointer to address casts.
Calls of const functions and const methods."
operators |
casting an array to a slice
Rust Internals and implementations
Core data structures: todo
Number representations
Integers
Floating points todo
array representation
variable-length lists: todo
multidimensional arrays: todo
limits on sizes of the above
string representation
Representation of structures with fields
ABI
Rust compiler implementation
MIR
https://rustc-dev-guide.rust-lang.org/mir/index.html
Upcoming new type checker Polonius
https://github.com/rust-lang/polonius http://smallcultfollowing.com/babysteps/blog/2018/04/27/an-alias-based-formulation-of-the-borrow-checker/ https://rust-lang.github.io/polonius/
Academic research inspiring or about Rust
https://doc.rust-lang.org/1.2.0/book/academic-research.html
Misc Rust implementation
Rust tests
Rust variants
The core language in the RustBelt? paper:
Rust Links