as a beginner:
top
- package manager and build tools
-
Option
andResult
- community
- std
- error messages
flop)
- overly strict borrow checker for beginners
- implicit returns (really hard to find that line without semicolon in a 100 line function)
as a beginner:
top
Option
and Result
flop)
Top: As someone who comes from a C++
background, rust feels like it was expressly designed to address its flaws while keeping a similar performance model:
C++
for years, and that was reliant on documentation to express the lifetime relationships between objects (with the possibility for bugs, or some tradeoffs with copies when the risk is too high). but memory safety in rust is so much more than that. No longer do I have to take extreme care with "rule of five" shenanigans because my class implements RAII. I no longer suffer from fields uninitialized by a buggy constructor. I don't accidentally pass a temporary instead of a reference to a class because I typo'd and left out the &
in the return type of a getter (that particular segfault was painfully stupid). Where it matters, I can mark functions and traits as unsafe to signal clients that they should look for additional preconditions in the documentation should they use them. These parts of the code where memory safety isn't enforced by the compiler stand out with unsafe blocks.std::experimental::optional
(C++14
) extensively, but the ergonomics suffers from the lack of match
, and the presence of nullable pointers is redundantC++
's broken error story, where the ecosystem is split between exception users and non-users (the replacement being error codes, or various home-made error systems). I rolled up my own result implementation in C++
, but it is home-made, suffers from the lack of std::variant
in C++14
, has worse ergonomics and I still have to use whatever my dependencies chose for error handling when interacting with them.rust
enums all the time. I wonder why the simple concept of "can be A or B" took until C++17
to come, and only as a library type.C++
for various reasons (such as the compilation model) and is unified with runtime polymorphism through trait objects.Flop: The language is young, inertia in this space is high (billions of lines of code have been written in C++
). As a result, I find myself missing some dependencies in rust (qt, an equivalent to boost::icl::interval_map
), and if we wanted to integrate rust to our existing codebase, we would have to rely on FFI while our C++
codebase makes extensive use of templates and so isn't ffi friendly. This without accounting for the cost of building the same level of expertise in rust as in C++
... Also as a result of its youth, I find myself missing some features (some of which may land this year): const generics (parameterising a template by an enum), variadic templates (which we use for implementing services with compile time checked arbitrary signatures).
I don't mean to pick on C++
, I still think we can build excellent software using that language (hopefully, this is what we are doing!), but Rust just happens to fix many of the hindrances I encounter in my usage of C++
.
Top: I do not have to worry. The compiler kicks me in the face until I get it right. I feel comfortable writing thousands-of-lines applications without having to worry about a thing, just get the damn job done.
Tops
Error handling is excellent. Yes, there's a lot of it, but I see it as bringing up to the surface assumptions that are usually glossed over. Some sort of function-local way to handle common checks may be nice though, I guess.
First C & C++ rival that "gets it" (100% GC-free, bare-metal aspirations, common-sense features like closures and type inference, immutable-by-default, if let etc) and actually seems to catch on.
No silly idealistic ideas hampering the language (cough Go cough)
A relatively rich ecosystem of libraries.
Flops
Generic & lifetime syntax can get really ugly. A concise way to define and express generic and lifetime constraints, to air out the definitions a bit, would be nice: constraint MyConstraint = A + B where ā¦;
macro_rules tangles up fragment type declarations with the invocation syntax definitions. I truly hate reading macro patterns because of this.
Case patterns
Implicit returns are only natural in one-line fns and lambdas, coding guidelines be damned. Explicit returns are required if you want an early exit, which makes the code look inconsistent once you end up using both flavors of return.
Speaking of coding guidelines being damned, I really don't like snake case. Warning about opinionated conventions by default is too much imo. The extra underscores make the code wider than I'd like, and I read camel case just fine.
Tooling and documentation have ways to go. Last I checked, there was no comprehensive A-to-Z guide (only attempts) on everything to consider when doing C interop, only bits and pieces of info spread across ebooks, articles and stack overflow reccomendations.
Turbofish
Modules, use
syntax, project file structure etc.
Top: The borrow checker thinks about the hard stuff so I don't have to.
Flop: The build takes too long.
Curious, are there reasons why you consider them flops?
Top: Be explicit and the compiler checks everything else for you (except out of bound issues).
Flop: Sometimes not sure how to convert between different struct
and enum
without reading the whole documentation.
Beginner here (haven't even made it all the way through the book yet)
Top: the community is great. Super helpful, lots of resources available, lots of blog posts, New Rustacean podcast.
Flop: with two very young kids and a full time job, it's hard to find the time to put in to learn rust properly. I've played around with it a little, but rust is kind of like vim in this regard. It's worth the investment, but hard to convince people to make the investment in learning it.
Random thought: I'm super interested to see if the Xi editor ever fully makes it off the ground. Given that it's frontend agnostic, I feel like we could finally have an open source version of sublime text (aka easy to set up and use, super fast).
Looks like my "flops" will be a bit of an unusual opinion here, but I think expanding anonymous types (i.e. impl Trait) is one of the biggest mistakes Rust made. Lambdas were bad enough, but at least they couldn't cross api boundaries originally. The problem with impl Trait, or anonymous types more generally, is that it is viral - once you use an anonymous type, every use forever after must also be anonymous, unless you wrap it in a boxed trait object. Anonymous types just don't work in a language which was designed around the assumption of explicit type signatures. And instead of fixing the underlying issue, it seems like people are just piling hack onto hack.
Another issue is the choice of default integer types when values are underconstrained by type inference, combined with unchecked overflow by default. This has caused bugs in real world code I've worked on.
That being said, Rust remains the only issue when you want code that is both fast and correct. My top is that Rust is the first language to make systems programming practical and enjoyable.
in Cargo.toml
:
[profile.release]
overflow-checks = true
(and whenever you want performance since your op cannot overflow (or is allowed to), you just use n1.wrapping_add(n2)
instead of n1 + n2
and expect overflow-checks
to be disabled)
use ::std::fmt::Display;
fn print_impl (x: &'_ (impl Display + ?Sized))
{
println!("{}", x);
}
fn print_generic<T : Display + ?Sized> (x: &'_ T)
{
print_impl(x);
}
fn print_with_added_runtime_method_resolution (x: &'_ dyn Display)
{
print_impl(x); // or print_generic(x)
}
So impl Trait
is not viral, and can always downgrade to dyn Trait
when needed, and not the other way around: thus the viral thing here are trait object definitions; and since those do have a runtime cost (vtable dynamic method resolution), impl Trait
seems like a far saner default than trait objects.
The one legitimate complaint here, imho, is the implicit : Sized
bound on generic types, though, which makes it actually incompatible with trait objects unless it is explicitely overriden with : ?Sized
.
The turbofish is a matter of personal taste, wrt how it looks.
As for the others, they simply don't click as easily as I'd hope (maybe because I tend to think of imports in terms of implicitly relative paths by default but I may be wrong).
I'm not sure why you think you are disagreeing with me, since your code example proved my point perfectly.
The issue is that as soon as you return a value with anonymous type, every subsequent use must either also use anonymous types (hence referring to them as "viral"), or wrap it in a trait object (Dyn Trait) which incurs runtime overheard, as you noted. The problem is that anonymous types are not first class citizens in Rust.
Or use it as a generic parameter, is this bad too?
Top:
The strong type system means if it compiles, it works (modulo logic bugs) and you can make invalid states unrepresentable. That means you'll write better quality, more robust code out of the box compared to other languages, and the average crate on crates.io tends to have a higher average quality.
Flop:
Asynchronous programming. I've been using Rust for 2 or 3 years now, but I use C# for my day job. Rust's way of doing async
is nowhere as nice as async
in C# (or JavaScript for that matter). And as @skerkour mentions, the current callback hell is only amplified by Rust's memory model and our pursuit of zero-cost abstractions.
Some further notes...
In my view, this is a key aspect of being an expression oriented language as is common in other ML-like languages. In other words, "implicit" return is really the function body (or more generally: a block) evaluating to an expression. This makes sense if you consider things like const X: u8 = { let mut x = 0; x + 1 };
We could highlight the expression oriented nature of Rust even further by allowing e.g. fn foo() -> u8 = 42;
.
See trait aliases
for this basic idea.
In the sense that you would like to not write the ::
part of .collect::<Vec<u8>>
? This is discussed in Make the turbofish syntax redundant by varkor Ā· Pull Request #2544 Ā· rust-lang/rfcs Ā· GitHub.
Can you elaborate on what you mean?
This is addressed by https://github.com/rust-lang/rfcs/pull/2515 which is hopefully making progress soon. The key is to give names to unnameable things by e.g. writing type Foo = impl Trait;
.
Rust's path & module system changed recently in Rust 2018. See [1] and [2]. Imports now work in a relative fashion by default thanks to "uniform_paths
". Hopefully that should make things better for you.
Speaking as someone else who also sees it as a wart, I am perfectly aware of the fact that the turbofish cannot be removed from Rust without introducing syntactic ambiguity. The rest of the syntax mandates its existence.
That doesn't make it not a wart. The language could have avoided it, but that would've required more changes than that to still be able to distinguish chained comparators and generics. It's a "local maxima", since there are no nearby improvements but there are better places far away.
Ignoring existing constraints, I would have preferred it the other way around:
let x = ....collect() :: Vec<u8>;
.
It would feel less nested (And Haskell does something similar iirc) and the ()
would not be so far from the function name. Or maybe have some entirely new syntax that instead of the collect call will be something that puts the new collection front-and-center: ....=>Vec;
Also, seconding notriddle, it may be a necessary wart, but it's still a wart.
Range
and RangeInclusive
types.