10 ms·
The author mentions the following: fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> { if x.is_none() || y.is_none() { return None;
by kris-s 4y ago
The author mentions the following:
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.
- edflsafoiewq 4y agoFirst one isn't idiomatic anyway with return. You can just do Some(x? + y?) though.
- adwn 4y agoBoth are rather ugly. This is much more idiomatic: match (x, y) { (Some(x), Some(y)) => Some(x + y), _ => None, }
- kzrdude 4y agoThis has been a thing since Rust 1.0. Just use the beautiful properties of match (or the "later" `if let`, of course). I prefer this and wish I could say it was idiomatic, but some tools like clippy push users over to helper methods instead of simple applications of match.
- dullcrisp 4y agoDon’t know Rust, but wouldn’t this have to be: (Some(x), Some(y)) => Some(x + y) else => None
- deleted 4y ago[deleted]
- steveklabnik 4y agoYou're correct, except that "else" is a keyword and so cannot be used there. You'd want _ => None, instead, which is the "catch all" arm. (For those that didn't catch it, the parent code is trying to use None, but it's actually a tuple, and there's four different cases here, not two. So the catch-all arm is better than spelling each of them out in this case.)
- adwn 4y agoYou're right – fixed. That's what I get from writing code in a simple text area.
- deleted 4y ago[deleted]
- 0x457 4y agoPretty sure clippy will tell you to rewrite it as: 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.
- steveklabnik 4y agoClippy will not complain about the parent's code. It's not really in place of an if; there's four cases there. To be honest, I find 'if let... else None' to be worse looking than the match, though I'm unsure if I truly prefer the zip version to the Some(x? + y?) version.
- stouset 4y agoSome(x? + y?)
- rom-antics 4y agoI think the idiomatic way to write that is: fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> { Some(x? + y?) }
- Zababa 4y agoThe first one is very clear, I agree. However if I wrote Rust daily, I would probably be familiar with the second one and would prefer it. Here's an article kind of related to that, in this case talking about APL, that I think explains very well the tradeoffs: https://beyondloom.com/blog/denial.html https://beyondloom.com/blog/denial.html. 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.
- h4x0rr 4y agoI'd prefer a match approach
- runevault 4y agoOnce you know what map does with an option I'd say it is mostly pretty readable. Basically map (when run against an Option value) is a way to say "if the value passed in has a value run this function on it otherwise return None.
- woodruffw 4y agoUsing `zip` feels excessively clever to me. I'd probably prefer something in between, like `and_then` followed by `map`, or just matches.
- trevor-e 4y agoYea the latter code is unreadable to me and feels obfuscated, like the author is trying to force functional programming where it doesn't belong.
- deleted 4y ago[deleted]
- Gigachad 4y agoI've done a non trivial amount of functional programming and know what zip and map do but I can't off the top of my head work out how that example works.
- lilyball 4y ago`x.zip(y)` evaluates to `Option<(i32, i32)>`
- deleted 4y ago[deleted]
- FridgeSeal 4y agoOptions can be mapped/zipped/“iterated” over (traversed might be a better word?). So in this case it’s using that fact to form a tuple of non-null values whenever the option is not null, and then acting on that. I think it’s kinda neat, but I wouldn’t personally use zip in this case, I’d have gone with map or and_then depending on whether the output of my operation is T or Option<T>.
- stouset 4y agoYou might choose to believe that, but `Option` and `Result` are practically purpose-built in Rust to work extremely well with functional approaches. And doing so greatly increases the likelihood that the compiler can produce perfectly optimal code around them.
- trevor-e 4y ago
- jxcl 4y agoYou do get better about using the functional operators as you use them, and they can be incredibly powerful and convenient in certain operations, but in this case he's missing the simplest implementation of this function using the `?` operator: fn add2(x: Option<i32>, y: Option<i32>) -> Option<i32> { Some(x? + y?) }
- deleted 4y ago[deleted]
- bkolobara 4y agoWait! The ? operator works with the `Option<T>` type? I assumed it was only reserved for `Result<T,E>`.
- misnome 4y agoI also (written a few tools in rust, dabble now and then) was under the impression that it only worked with Result. Is this recent or just never encountered?
- deleted 4y ago[deleted]
- steveklabnik 4y ago? is able to be used with Option as of Rust 1.22, released in November of 2017 https://blog.rust-lang.org/2017/11/22/Rust-1.22.html https://blog.rust-lang.org/2017/11/22/Rust-1.22.html
- misnome 4y agoWell, I guess this counts as a concept I wish I learned earlier!
- yccs27 4y agoIt currently works with `Result` and `Option` types, and with RFC#3058 will work with all types implementing the `Try` trait.
- ryanianian 4y agoAs someone new to Rust, I look at the latter and see `.zip()` (apparently unrelated to python zip?), and then a lambda/block which intuitively feels like a heavyweight thing to use for adding (even though I'm like 90% sure the compiler doesn't make this heavyweight). 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.
- kaba0 4y agoIt is kinda common pattern for some FP languages (Haskell), so it doesn’t seem too clever to me.
- edflsafoiewq 4y agoIt's the same zip, just think of an Option as being a list of length at most 1. [] = None, [1] = Some(1), etc.
- steveklabnik 4y ago> (even though I'm like 90% sure the compiler doesn't make this heavyweight). 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] ret
- civopsec 4y ago> 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. You 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.
- leshow 4y ago
- virtualritz 4y agoTLDR; learning Rust through canonical code in tutorials often requires the student to learn bits about the language that are more advanced than the actual problem the resp. tutorial tries to teach how to solve in Rust. ;) 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()).
- mprovost 4y agoThis is very true - to fully explain a "hello world" program you'd have to dive into macro syntax... When writing my Rust book I often start by showing the naive solution, and then later move to more canonical code once I've introduced more syntax that enables something more elegant. But I'm aware that I'm showing something non-optimal to start. Maybe that loses some trust, like people think they're learning something only to be told later on that it's wrong? On the other hand if you start with the optimal solution you have to teach so much syntax that it's overwhelming. I expect that some folk want every example to be perfect, but I'm going with an approach where you iterate through each chapter and as you progress through the whole book the examples get better and more idiomatic.
- steveklabnik 4y agoThis is basically an eternal battle when teaching. Personally I prefer to try and stay to only idiomatic code if at all possible, but there's pros and cons to each approach. (This is one reason why I'm glad other people are also writing books! Not everyone likes my style!)
- 4y ago
- nicoburns 4y agoI'd probably write this fn add(x: Option<i32>, y: Option<i32>) -> Option<i32> { match (x, y) { (Some(x), Some(y)) => Some(x + y), _ => None, } }
- Yoric 4y agoAgreed. That's way more readable.
- deleted 4y ago[deleted]
- dgb23 4y agoI like this the best. It is dumb and clear. Anyone can read this even with minimal to no Rust experience but it still flows elegantly.
- stouset 4y agoI'm sorry, but this perspective is absolutely bizarre to me. We should not avoid language features that reduce boilerplate and drastically increase comprehension for for people who have experience in a language in order to cater to people who have minimal or no experience in that language. Should we not use `?` in Rust because it might be obscure to someone who's never used the language? Should we not use any of the `Iterator` functions (including `zip` and `map`) because they might be confusing to C programmers who only know `for` loops? Functional concepts are everywhere these days. Most of them are not hard. `zip` and `map` do not require understanding of homotopy type theory to understand, they are essentially trivial functions. They are available across every type you could possibly iterate over in Rust, and if you understand what they do on one of those you essentially understand what they do on all of them. This is a toy example, but virtually every piece of hard evidence we have in this field shows that—within reason—more concise code has fewer bugs and is easier to comprehend than longer, more verbose equivalents. Writ large across a project, doubling or tripling the amount of code to bend over backwards accommodating complete novices is lunacy.
- 4y ago
- civopsec 4y agoWhat’s there to say? It works the same way as `zip` does for an iterator over a `Vec`. So if you understand `zip` as applied to an iterator over `Vec`, then you might understand `zip` applied to `Option`. 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`).
- deleted 4y ago[deleted]
- stouset 4y ago> Do folks really prefer the latter example? The first one is so clear to me and the second looks inscrutable. 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?)
- lmm 4y agoThe latter is a lot clearer and simpler. The former requires me to reason about control flow, if, and early return, a whole bunch of magic concepts. The latter is just an expression made of normal functions; I could click through and read their implementation if I was confused.