11 ms·
Rust Traits Deep Dive, Part 2
- Animats 8y agoThe text, with examples, is here.[1] Read that, skip the audio. About operators, he writes "So, to grab one of those earlier examples: when you write a + b, that's equivalent to calling a.add(b) or b.add(a) or even Add::add(a, b). The operator is special syntactical sugar for the trait." But which one of those? This matters, because it leads to mixed-mode semantics trouble. Who determines the output type of "a + b"? "a"? "b"? Both? Are there implicit type conversions? This is where C++ gets into strange interactions between type converters and generic selection. C++ resolves this with a rule that, when there are multiple possible instantiations, the selected one must be one notch "better" than the alternatives. Python gets into mixed-mode trouble with numpy arrays vs. built-in lists. In Python, "[2,4] * 2" is [2,4,2,4], while "array(2,4) * 2" is [4,8]. It's a classic problem in language design. If there are no implicit conversions, users complain about having to write "1.0" instead of "1", or worse, having to write "1.0L". If there are too many implicit conversions, you get unexpected semantics. Basically, conversions are there for numbers. Then somebody decides to generalize them. That sometimes ends badly. There's nothing here about what Rust does about conversions, but that may be covered in one of the other many parts of this series. The good news is that the Rust programming book is finally coming out in just 6 more days. So there's something better to read than this. [1] https://newrustacean.com/show_notes/e024/struct.script https://newrustacean.com/show_notes/e024/struct.script
- killercup 8y ago> Who determines the output type of "a + b"? "a"? "b"? Both? Are there implicit type conversions? The Add trait is defined in `std::ops::Add` as: pub trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } (You can find the documentation on <https://doc.rust-lang.org/1.26.2/std/ops/trait.Add.html> https://doc.rust-lang.org/1.26.2/std/ops/trait.Add.html>) Both the type of the `self` parameter and the concrete type of `RHS` determines which trait impl matches. For example: `2 + 2` chooses the `impl Add<i32> for i32 { type Output = i32; ... }` implementation (literal integers are of type `i32`). Something like `2 + &2` would choose `impl<'a> Add<&'a i32> for i32`. > There's nothing here about what Rust does about conversions, but that may be covered in one of the other many parts of this series. With operators, there are no implicit conversions involved, AFAIK. So all Rust can do is try and infer the type you want. For literal numbers, it can do so in a limited fashion. As seen above, if you write `let x = 42;` the type is inferred as `i32`. If you write `foo(666)` with `fn foo(x: u64)`, the literal 666 is inferred to be meant as `666u64`. > The good news is that the Rust programming book is finally coming out in just 6 more days. So there's something better to read than this. You can already read it here: https://doc.rust-lang.org/book/second-edition/index.html https://doc.rust-lang.org/book/second-edition/index.html :)
- red75prime 8y ago> if you write `let x = 42;` the type is inferred as `i32`. It can be influenced by the x's usage. let x = 42; let y = x/2 + 21u32; x is u32 here.
- simias 8y agoYes the rule is that it defaults to i32 only if there's absolutely no way to infer the exact type by use like in your example. So in practice it almost never happens because as soon as you use the value in an operation or function call the compiler knows the exact type. One exception would be code like: fn main() { for i in 0..10 { println!("{}", i); } } Here there's no way for the compiler to know what exact integer type is expected so it defaults to i32. There was a debate about this, I was personally in opposition but in my experience it mostly occurs on small "hello world" examples and code snippets so it doesn't really matter. I've never encountered any issues stemming from this rule so far. At least they got rid of the "int" and "uint" types which is a great thing in my opinion.
- masklinn 8y agoGP is talking about the default value for "unconstrained" literals, yours is constrained by the second expression: assuming just the stdlib, * Add<u32> is implemented for u32 and &u32 * thus x/2: (u32 | &u32) * there's no Div<Output=&u32> so x/2: u32 * Div<Output=u32> only is only "Div<u32> for u32" * thus x: u32
- dorfsmay 8y agoInteresting... I'm a newbie at Rust, and expected y to be a float, because of the division. But it turns out let x = 11; let y = 11/2; ==> y = 5 Given how type safety is important to Rust, I expected this to panic rather than do an automatic cast.
- masklinn 8y agoThere is no type safety issue, and there is no cast. Div on integral types is explicitly implementing as an integer/truncating division: https://doc.rust-lang.org/src/core/ops/arith.rs.html#436-452 https://doc.rust-lang.org/src/core/ops/arith.rs.html#436-452 There would be no type-safety issue or cast if it were implemented as a real division either, incidentally. There is room to argue Div should not have been implemented at all on integral types (as in Haskell where integer division is a separate operation entirely), but that's a completely different issue.
- kjeetgill 8y agoJava can get into some weird cases with generics, super classes, and overriding. Joshua Bloc has some interesting ones in the fantastic (and horrific!) book, "Java Puzzlers: Traps, Pitfalls, and Corner Cases". For what it's worth, I think python's case is fairly simple and easy to remember. Python objects can define two different methods for operator * : __mul__(self, b) and __rmul__(self, b). When you see A * B, try A.__mul__(B) if A has a __mul__. If the call throws NotImplemented (usually due to B's type) of if A did not hava a __mul__ call B.__rmul__(). I think that's actually much simpler that relying on types for dispatch since operators tend to be more "flexible" in their operand types than normal functions.
- masklinn 8y ago> Are there implicit type conversions? Only implicit refs and derefs I think. > It's a classic problem in language design. If there are no implicit conversions, users complain about having to write "1.0" instead of "1", or worse, having to write "1.0L". Rust handles that by type-inferring literals, the literal itself does not impose a type. If there are no constraints at all I think it defaults to i32. The type can also be provided explicitly as part of the literal e.g. 1f32 or 7u8.
- chrismorgan 8y agoNote that integer literals and float literals are still distinct; `1` cannot be interpreted as a float, nor can `1.0` be interpreted as an integer—though `1f32` is actually legal (this surprised me; I don’t know when it became legal), while `1.0u32` isn’t.
- dbaupp 8y ago> But which one of those? The situation is far simpler than that sentence implies; a + b is (always) equivalent to Add::add(a, b). The same trait/impl rules apply to + as to normal trait function calls. > Are there implicit type conversions? No, as people quickly encounter if they do numeric/matrix computation in Rust, all type conversions are explicit. > The good news is that the Rust programming book is finally coming out in just 6 more days. So there's something better to read than this. Even better news: that particular Rust book has been available online for a long time, as the official documentation.
- steveklabnik 8y agoTiny, tiny addendum here: > a + b is (always) equivalent to Add::add(a, b) method call synatx does auto ref/deref, so it may be Add::add(&mut a, b) or Add::add(&a, b) too, etc.
- chrismorgan 8y agoMethod call syntax does, but operators don’t.
- steveklabnik 8y agoAh ha, I didn't realize that. I thought it desugared to the method call, which then gets de-sugared to the fully qualified syntax. That does seem more complicated than skipping the intermediate step, and since we have so many implementations for the various ref or not'd version, it ends up feeling the same. Thanks!
- dbaupp 8y agoYep! That's why it's equivalent to Add::add(a, b), not a.add(b). (This was definitely something I had to check, though, and, as you say, a tiny detail that's almost certainly irrelevant to most code that people write.)
- steveklabnik 8y ago
- chrismorgan 8y agoTo say it was equivalent to b.add(a) was incorrect and unwise. But to say a.add(b) or Add::add(a, b) was correct. It used to be more complex in the distant past when the operators worked by reference, but since some time before 1.0 they operate by value, and so the following are all equivalent: • `a + b` • `a.add(b)` (provided std::ops::Add is imported and the type of a doesn’t have an intrinsic method named add, which would take precedence) • `Add::add(a, b)` (provided std::ops::Add is imported) • `<A as ::std::ops::Add<B>>::add(a, b)`, where A and B are the types of a and b (provided we’re not no_std). (Note that `a.add(b)` would work if a is of type A and &A implements Add but A doesn’t, because of autoref/autoderef in method calls, while the other three approaches would not work, and would instead want you to write &a.) Demonstration of most of this: https://play.rust-lang.org/?gist=c70a8c62fa0f304b1a8dceddf98302f1 https://play.rust-lang.org/?gist=c70a8c62fa0f304b1a8dceddf98...
- cesarb 8y ago> (Note that `a.add(b)` would work if a is of type A and &A implements Add but A doesn’t, because of autoref/autoderef in method calls, while the other three approaches would not work, and would instead want you to write &a.) That implies to me that "`Add::add(a, b)` (provided std::ops::Add is imported)" would be the most didactic explanation.
- majewsky 8y ago> If there are no implicit conversions, users complain about having to write "1.0" instead of "1", or worse, having to write "1.0L". If there are too many implicit conversions, you get unexpected semantics. Basically, conversions are there for numbers. Then somebody decides to generalize them. That sometimes ends badly. In Go, there are special types for integer and float literals. Literals can be implicitly casted into any numeric type large enough to store them, but casts from numeric types into other numeric types must be explicit. So for example: funcThatTakesInt32(1) // works funcThatTakesUint64(1) // works too var x int32 = 1 funcThatTakesUint64(x) // error: type mismatch funcThatTakesUint64(uint64(x)) // works: explicit cast The only thing I'm not sure about is which numeric type gets chosen when a literal is stored in a fresh variable without declaring its type, i.e. x := 42 y := 23.0 I think `x` would be `int` since the following works: for x := 0; x < 10; x++ { fmt.Println(someArray[x]) //subscript requires `int` index } But then what happens when the literal in `x := <literal>` is too large to fit in `int`? And since the size of `int` (AFAIK) is machine-dependent, does that mean that some programs will compile for some target platforms, but fail for other target platforms? Argh. I see what you're getting at.
- smaddox 8y agoI'm a bit surprised this isn't covered in any of the documentation, but it might just be because the semantics are clear once you understand associated types. The `Add` trait has an associated type `Output` that you must define when implementing `Add`. The semantics are then that the type on the left decides the output, but that type can change with the type on the right. It is up to the type author to provide appropriate symmetric implementations to avoid the confusion you describe.
- mlevental 8y agois the script available for only certain episodes? i can't find links and fiddling with the url doesn't work consistently. e.g. https://newrustacean.com/show_notes/e023/struct.script https://newrustacean.com/show_notes/e023/struct.script but not https://newrustacean.com/show_notes/e000/struct.script https://newrustacean.com/show_notes/e000/struct.script
- chriskrycho 8y agoI haven't gotten around to back-porting the scripts for all episodes, but you can find them (early ones less fleshed out; I started scripting top-to-bottom relatively early, though) in the repository, here: https://github.com/chriskrycho/newrustacean.com/tree/master/docs https://github.com/chriskrycho/newrustacean.com/tree/master/...
- mlevental 8y agothanks
- atombender 8y agoThanks for the link to the script. I was about to dismiss this story because it's not at all obvious from that page that there's a text version. This is a web site of presentations/lessons which has been generated from source code by a documentation generator, with each Rust module representing a topic. It's "cute", but really friendly to humans.
- kbenson 8y ago> If there are no implicit conversions, users complain about having to write "1.0" instead of "1", or worse, having to write "1.0L". If there are too many implicit conversions, you get unexpected semantics. There is another way out of this, which is using different operators. Modern Unicode can help greatly with this, but even before that was common, Perl was doing something similar between some of its major types (numbers and strings). Since Perl types are dynamic, and may be used as a string or a number, there would be a lot of ambiguity on comparing two variables for equivalence if the comparison depended on the type. To solve this, Perl has two sets of operators, one for dealing strings (eq,ne,gt,lt,gte,lte, '.' for concatenation), and one for dealing with numbers (==,!=,<,>,<=,>=, + for addition). What these primarily do is interpret both sides as the desired type before the comparison. This greatly reduces ambiguity, but does require more operators be internalized before language proficiency is achieved. This is of course much harder to accomplish and becomes much more complicated as you increase the number of types. That said, it's an important concept and could help quite a bit in circumstances where things can be ambiguous (for example, your Python example).
- lomnakkus 8y agoAnother way would be to use return type polymorphism. That way you could have a trait similar to Haskell's "Num a" + "Integral a" with "fromIntegral :: a" and just have the parser insert a "fromIntegral" in front of every numeric literal. I don't know much about Rust, but I'd assume from the discussion that it doesn't support this? Of course, Haskell's Num is a horribly broken typeclass, so don't do exactly that. :)
- renox 8y agoYour examples show (again) that it is a mistake to conflate aritmetic operators and concatenation operators: the first are commutative the second aren't. D has ~ for concatenation which is nice but it has nothing for "concatenative multiplication" (if memory serves)
- toyg 8y agoI've just started working with Rust, coming from a python/java/web background. Some of the constraints are a bit mind-bending, and the ecosystem is very immature; but compared to C/C++, it's a dream. It only needs a few things to achieve world domination, imho: a good crossplatform GUI toolkit, and a trivial way to interface with higher-level languages like Python.
- steveklabnik 8y agoHave you checked out milksnake or pyo3?
- toyg 8y agoI'd seen milksnake and found it daunting. Pyo3 looks sweet. Thanks for the pointer!
- steveklabnik 8y agoNo problem!
- lmm 8y agoWhat is it that you want from Python? Presumably automated memory management - but if you have GC pauses anywhere in your stack then you can presumably afford to have them everywhere in your stack. Would a language like OCaml (or my own favourite, Scala) give you everything you need: Rust-like safety but Python-like expressiveness?
- toyg 8y agoIt's just that the Python ecosystem is so much bigger, there are much more productive libraries for dealing with things that are not performance-critical. With python you get stuff like QT, nice configuration parsing (rust has one lib and pretty immature too) and so on, available with zero effort. Python is now so huge that only a handful of other communities has anything comparable (Java, PHP, JS and maybe Perl, basically; I would add C# but there you usually have to pay for stuff).
- eslaught 8y agoIn this example: fn collect<B: FromIterator<Self::Item>>(self) -> B where Self:Sized; I thought this was universal, not existential like the podcast claims. It would be existential if you instead wrote: fn collect(self) -> impl FromIterator<Self::Item> where Self:Sized; The latter is what got added in 1.26, whereas the former was already available before that. And really, you wouldn't want to use the latter in this case because the user wants to choose the type that's being generated (this is not a case where you want the callee to choose the type). So the second signature wouldn't make a whole lot of sense even if this API had been stabilized post-1.26.
- chriskrycho 8y agoI may have gotten that wrong! Had a lot of things in flight for this episode and the universal/existential discussion was a late addition from before I split it into this and the upcoming episode focused on object safety, dynamic-vs.-static dispatch, and `impl Trait`.