fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> {
if x.is_none() || y.is_none() {
return None;
}
return Some(x.unwrap() + y.unwrap());
}
The above looks kind of clunky because of the none checks it needs to perform, and it also sucks that we have to extract values out of both options and construct a new option out of that. However, we can much better than this thanks to Option’s special properties! Here’s what we could do
fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> {
x.zip(y).map(|(a, b)| a+b)
}
Do folks really prefer the latter example? The first one is so clear to me and the second looks inscrutable. fn add2(x: Option<i32>, y: Option<i32>) -> Option<i32> {
Some(x? + y?)
}I prefer the latter now that I understand how all the Result/Option transformations work. As a beginner this would be hard to read but the former looks clunky.
Clippy also got pretty good lately at suggesting such transformations instead of if... blocks. I.e. I guess that means they are considered canonical.
In general I find canonical Rust often more concise than what a beginner would come up with but it does require deeper understanding. I guess this is one of the reasons why Rust is considered 'hard to learn' by many people.
You could actually teach Rust using pretty verbose code that would work but it wouldn't be canonical (and often also not efficient, e.g. the classic for... loop that pushes onto a Vec vs something that uses collect()).
fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> {
match (x, y) {
(Some(x), Some(y)) => Some(x + y),
_ => None,
}
} fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> {
Some(x? + y?)
}Literally everything in programming is inscrutable until you learn it the first time. The latter should be trivial to understand for anyone who's spent even a little amount of time in a language with functional elements.
A day-one beginner doesn't understand a `for` loop. You probably think they're trivial. Bitwise operations are the same. They might be new to you, but `zip` and `map` frankly don't take much more effort to understand than anything else you probably take for granted. `zip` walks through everything in two separate wrappers and pairs up each element inside. `map` opens up a wrapper, lets you do something with what's inside, and re-wraps the result.
For instance, you can do the exact same thing with arrays. Pair up each element inside (like a zipper on clothing), then for every element inside, add them together:
[1, 3].zip([4, 1]).map(|(a, b)| a + b ) # [5, 4]
That said, you can write this specific function even simpler: Some(x? + y?) match (x, y) {
(Some(x), Some(y)) => Some(x + y),
_ => None,
} (Some(x), Some(y)) => Some(x + y)
else => None if let (Some(x), Some(y)) = (x, y) {
Some(x + y)
} else {
None
}
`match` in place of `if` looks weird. IMO example with `zip` is better though. Some(x? + y?)To try with my own words: programming is about shared understanding of a problem, but also the tools used to solve the problem. Code is text, text has a target audience. When it is experts you can use more complex words, or more domain-specific words. When it's intended for a wider audience, taking the time to explain and properly define things, sometimes multiple times, can be necessary.
According to Rust's documentation of Some:
> zip returns Some((s, o)) if self is Some(s) and the provided Option value is Some(o); otherwise, returns None
> zip_with calls the provided function f and returns Some(f(s, o)) if self is Some(s) and the provided Option value is Some(o); otherwise, returns None
Using zip_with seems more appropriate (x.zip_with(y, +) or something) but zip_with is nightly. I also don't like how object chaining makes so that x seems more "fundamental", or "in another category" than y and +, while really x and y are the same, and + is something else. The if solution shows clearly that x and y are the same, by treating them exactly the same. The second solution also introduces a and b from nowhere, doubling the number of variables used in the function. All small things, but I think it can help put words on why precisely the second isn't as readable as it may seem.
It's interesting how much can be said about a simple "add" function.
In other words:
Y is clear if you already understand X. Like how returning early is simple to understand if you understand if-blocks.
That’s the problem with replying to these kinds of questions: the response is so short and context-dependent that it can look curt.
EDIT: the first code is also inelegant in that it effectively checks if both values are `None` twice in the `Some` case: once in the if-condition and once in `unwrap()` (the check that panics if you try to unwrap a `None`).
And doing so greatly increases the likelihood that the compiler can produce perfectly optimal code around them.
By comparison, the first one is straightforward and obvious. It's certainly kinda "ugly" in that it treats Options as very "dumb" things. But I don't need to read any docs or think about what's actually happening when I read the ugly version.
So TLDR: This reads a bit like "clever" code or code-golfing, which isn't always a bad thing, especially if the codebase is mature and contributors are expected to mentally translate between these versions with ease.
The first example, to me, is the worst of all worlds, if you want to be explicit use `match`. Otherwise, having a condition then using `unwrap` just feels like it's needlessly complicated for no reason... Just adding my subjective assessment to the bikeshed.
Yes, as of Rust 1.66, this function compiles to
example::add:
xor edi, 1
xor edx, 1
xor eax, eax
or edx, edi
sete al
lea edx, [rsi + rcx]
retYou contradict yourself. You can’t deride it as “clever” (whatever the quotes mean) and then in the next breath say that it might be a practical style.
And yes, Rust code in the wild does look like this.
https://news.ycombinator.com/newsguidelines.html
We detached this subthread from https://news.ycombinator.com/item?id=34429403.
I assume you’re going to reply that you don’t need to think about all this when writing most Python. And that’s true of most Rust too. Don’t get me wrong, Rust does have some unique complexity that Rust programmers need to learn in exchange for its unique safety guarantees—but method lookup isn’t where the complexity is.
I actually find that simpler than some of the spaghettis you can create with Python’s multiple inheritance :P
In Python, a list of lists has the same behavior.
l = [ [ 1, 2, 3 ] ]
l2 = l.copy()
l[0][0] = 99
print(l) // [[99,2,3]]
print(l2) // [[99,2,3]]It's not just lifetimes, a basic Rust function can expose the ideas of exclusive and non-exclusive ownership, traits, generics with and without trait bounds, algebraic data types, closures, closures that move, async, async blocks, smart pointers, macros, trait objects, etc etc.
There is absolutely a learning curve and it makes me understand why Golang is the way it is. The language authors of Golang saw the business needs of Google, realized that they need to get domain experts that are not programming savants to write code and created a language that would allow the largest number of non programmer domain experts be productive coders.
That being said, the Golang developers went too far and let too many of their biased ideas for what is "simple" infect the language. In what way is it simple for a language to not be null safe. I have a bunch of other complaints too.
But anyway, I don't think Rust is going to be a revolution in writing software unfortunately. I don't think enough people will be able to learn it for companies to be able to hire for Rust positions.
Go is probably the most widely used 'simple' language, and it's primarily used for applications. You can find some examples of using Go for systems-like programming, but that's not where it shines.
It was a real struggle, and I wasn’t enjoying it at all. “Programming Savant” is a great way to put it because all the people I know who like Rust also happen to be the smartest engineers. In the end I gave up because I wasn’t getting any value. I was also trying to find a Rails equivalent, tried Actix and I didn’t like that either.
But if you're working on one who does, you're typically glad to use Rust :)
Some might criticise it for forcing you through a complex process to acquire a firearms license, selling you gun with an efficient safety and then insisting that you wear extremely heavily armoured shoes.
So to use the foot gun analogy, if you don't know the stuff in the article you wont even be able to pull the trigger.
An actual footgun is something like monkeypatching in python or ruby. Or running for_each with an async callback in javascript.
Really annoyed by the borrow checker? Use immutable data structures
... This is especially helpful when you need to write pure code similar to that seen in Haskell, OCaml, or other languages.
Are there any tutorials or books which take an immutable-first approach like this? Building familiarity with a functional subset of the language, before introducing borrowing and mutability, might reduce some of the learning curve.I suspect Rust does not implement as many FP-oriented optimizations as GHC, so this approach might hit performance dropoffs earlier. But it should still be more than fast enough for learning/toy datasets.
It's more complicated than this; sometimes FPish code is super nice and easy and compiles well (see the zip example in this very thread!) and sometimes it's awkward and hard to use and slower.
Rust makes mutations safe, but immutability has benefits outside of safety.
Eep. No. At least, not in anything public. Universal impl trait arguments can't be turbofished, so you're placing an unnecessary constraint on your caller.
What does this mean?
The author then goes on to write an article largely covering the mechanics of the language rather than architecting applications.
Deref is convenient for builtin "container" types where the only thing you'll ever do is access the singular value inside it. But sprinkling it everywhere can get confusing ("why are we assigning a &str to a struct here?")
Classic OOP would use inheritance to achieve this, while something like Deref allows you to use it with all the added behavior - without losing the possibility to assign to it values of the base type.
This is a nitpick on my part, but this part on PhantomData:
> Tells the compiler that Foo owns T, despite only having a raw pointer to it. This is helpful for applications that need to deal with raw pointers and use unsafe Rust.
...isn't quite right: `_marker: marker::PhantomData<&'a T>,` doesn't tell the compiler that `Foo` owns `T`, but that instances of `Foo` can't outlive its `bar` member. `bar` is in turn borrowed, since a pointer is "just" an unchecked reference.
You can see this in the playground[1]: `foo1` and `foo2` have the same borrowed `bar` member, as evidenced by the address being identical (and the borrow checker being happy).
Edit: What I've written above isn't completely correct either, since the `PhantomData` member doesn't bind any particular member, only a lifetime.
[1]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
impl<'a, T> Foo<'a, T> {
pub fn new(bar: &'a T) -> Self {
Self { bar, _marker: PhantomData }
}
}
… AND the bar field is private, then you are ensuring the Foo container doesn’t outlive the original bar pointer. If you don’t constrain creation and access to the bar field then people can just write foo.bar = &new_shorter_borrow as *const T; and then the lifetimes are absolutely unrelated, foo.bar will become invalid soon. A classic example of doing this correctly is core::slice::IterMut, and the slice.iter_mut() method.A short illustration (note how only one of the Foos creates a compile time error): https://play.rust-lang.org/?version=stable&mode=debug&editio...
A short explanation of what you’re telling the compiler with PhantomData (without accounting for the new method/ constraints on creation) is that Foo appears to have a &'a T field, for the purposes of the outside world doing type and borrow checking on Foo structs and their type parameters. That does two things:
(1) Due to implied lifetime bounds, Foo’s generics also automatically become struct Foo<'a, T: 'a>, so this ensures T outlives 'a. That will prevent you accidentally making Foo<'static, &'b str>.
(2) You don’t have an unused lifetime, which is an error. We always wanted Foo to have a lifetime. So this enables you to do that at all.
Edit: and (3) it can change the variance of the lifetimes and type parameters. That doesn’t happen here.
Are you thinking of `mem::forget`?
In fact, mem::drop will accept any value, whether it implements Drop or not.
The author of the article is definitely quite confused about Drop vs mem::drop. mem::drop is not an implementation of Drop.
Is that really what they're called? It seems confusing to me: if it's shared (i.e. many people use it at the same time) how can it also be borrowed (i.e. one person has it)?
* mutable/immutable (this is the way the book refers to it and used to be the 'official' terms but I don't know if they've changed that since I left, partially this is the name because you spell it "&mut".)
* shared/exclusive (this is the name that some people wish we had called mutable/immutable, a very very long time ago thing called the "mutapocalypse.")
Both sets are adjectives for "borrow."
I agree with you that "shared borrow" can feel like a contradiction in terms.
In general, they are duals of each other: it depends if you want to start from a place of "can I change this" or "who all has access to this," making each term more clear depending on the perspective you take.
This is incorrect. The code works fine as written; Rust will let you write .clone() and it will clone the Arc (without cloning the inner value). More generally, methods on a wrapper type are searched before methods found via auto-deref. It’s often considered better style to write Arc::clone(…), but that’s for human readability, not the compiler. There’s a Clippy lint for it (https://rust-lang.github.io/rust-clippy/master/#clone_on_ref...) but it’s turned off by default.
Rust doesn't reason in terms of by-reference vs by-value passing. It doesn't have pervasive expensive copy constructors that need to be avoided, NRVOs, and things like that.
Rust works in terms of owning and borrowing. Moves have a special case of `Copy` types like i32, but this works only for POD types, is at worst a shallow memcpy, and the types have to opt in to being copyable like that. The default is non-copyable, even for trivial structs and integer enums.
Drawing false parallels with C++'s pointer types is a major source of people fighting the borrow checker. References aren't for not-copying, they're for not-owning. `Box<T>` is a pointer that passes T by reference, but there's no `&` involved, because it is owning. OTOH Passing an argument via `&` is not just "by reference", but may require borrowing a value, which needs a location to be borrowed from, may extend scopes of loans, need specific lifetimes, etc. It's way more than just a perf tweak and is a PITA when it's done implicitly (which async fn does to some extent).
i.e.
```
let min_id = if let Some(id) = foo()? { id } else { return; }
...
let bar = if let Some(bar) = baz()? { bar } else { return; }
..
// vs
if let Some(id) = foo()? {
...
if let Some(bar) = baz()? {
..
}}
```
It's nice to also do `if let (Some(x), Some(y)) = ...` but sometimes you need the result of `x` to do a few things before you can get `y` (or maybe don't went to evaluate `y` depending on `x`).
---
I like the `where` syntax more than the example formatting.
```
fn x<T>(t: T) where T: Foo {}
```
let Some(min_id) = foo() else { return };
// continue coding at same indentThe comments here are unnecessary negative. People seem to be upset on things that don't look familiar. Don't let the negative comments get to you. Keep up the good work.
To add an example of my own:
fn do_the_meow(meower: Meower)
seems like you want to take (consume, own) an object implementing Meower, which is correctly explained not possible like this. A suggested solution, fn do_the_meow(meower: &dyn Meower)
is very different - it is now correct with regards to a trait access, however, now you're just taking a reference. Correct replacement would be Box<dyn Meower>. And the final solutions, fn do_the_meow<M: Meower>(meower: M)
fn do_the_meow(meower: &impl Meower)
the first one is correct and equivalent to the original intent - it takes (owns) the value. However, the second variant (which is, again, as correctly stated, the best solution), is different again - it takes a reference (`&`) to `impl Meower`.Coming back to my point - it is important to separate the difference between the ampersand and the impl/dyn part. To suggest an improvement, one could first write all variants of this function taking a reference (`&Meower`, `&dyn Meower`, `&M` and `&impl Meower`), and later introduce the difference between where one can use sized/unsized types, and that Box<dyn Meower> is owned equivalent of &dyn Meower, and why one can't have owned `dyn Meower` just lying around.
> which tells the compiler “I just want something that implements Meow”.
The ‘trait Meower’ also implies same, right? If so, why can’t we use that
Not in the example given. There's nothing wrong with creating an Rc<T> loop; the borrow checker doesn't come into the picture.
> That is, you could mutate the data within an Rc as long as the data is cheap to copy. You can achieve this by wrapping your data within a Rc<Cell<T>>.
T: Copy is only a bound on the .get() method. You can do this even if the data is expensive to copy, so long as you always swap in a valid representation of T. (I sometimes write Cell<Option<T>>, where it makes sense to replace with a None value.)
> Embrace unsafe as long as you can prove the soundness of your API,
In other words: avoid unsafe except as a last-ditch resort.
> &impl Meower
Should be impl Meower, if you want the same behaviour as the explicit-generic version.
> Many tutorials immediately jump to iterating over vectors using the into_iter method
Out of interest, what tutorials? I've never read one that does that!
> Instead, there are two other useful methods on iterators
Methods on many iterables in std. Not on iterators (nor iterables in general).
> You can wrap the following types with PhantomData and use them in your structs as a way to tell the compiler that your struct is neither Send nor Sync.
… You're doing it wrong. €30 says your unsafe code is unsound.
> Embrace the monadic nature of Option and Result types
Er, maybe? But try boring ol' pattern-matching first. It's usually clearer, outside examples like these ones (specially-chosen to make the helper methods shine). I'd go for if let, in this example – though if the function really just returns None in the failure case, go with the ? operator.
> For example, writing a custom linked list, or writing structs that use channels, would typically need to implement a custom version of Drop.
No, you don't typically need to. Rust provides an automatic implementation that probably does what you need already. Custom drop implementations are mainly useful for unsafe code.
> Really annoyed by the borrow checker? Use immutable data structures
No. No. Haskell has all sorts of optimisations to make "immutable everything" work, and Rust's "do what I say" nature means none of those can be applied by the compiler. If you want to program this way, pick a better language.
> Instead, you can define a blanket trait that can make your code a more DRY.
There are no blanket traits. There are blanket trait implementations, and you've missed that line from your example.
All in all, this is a good article. There are some good tips in there, but I wouldn't recommend it to a beginner. I would recommend the author revisit it in a few months, and make some improvements: a tutorial designed when you're green, but with the experience from your older self, can be a really useful thing.
You can replicate iter() with `(&x).into_iter()` and you can replicate iter_mut() with `(&mut x).into_iter()` and obviously `x.into_iter()` consumes the contents.
Skip the whole weak rc nonsense and jump directly to using an allocator (like slotmap) when dealing with cyclic data structures. Wrap the allocator in a a struct to allow for easy access and all the difficulty from rust cyclic datastructures disappears.