Rust Traits Deep Dive, Part 2
newrustacean.com
newrustacean.com
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.
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>)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 :)
It can be influenced by the x's usage.
let x = 42;
let y = x/2 + 21u32;
x is u32 here. 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.* 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
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.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.
(I’m still simplifying this a little, because specialisation is a thing which does allow overlapping implementations, but it must be opted into by the broad implementation by writing `default fn` instead of `fn`.)
http://play.rust-lang.org/?gist=566c7ddb5ec90836004a2549aa78...
Because `impl Add<Bar>` and `impl Add<Baz>` are two clearly different impls there is no ambiguity (RHS is known at compile time).
What specialization would solve is this kind of ambiguity:
http://play.rust-lang.org/?gist=34142671953f75f0448b03bb804f...
Because `impl<T: Debug> Add<T>` and `impl Add<Baz>` are conflicting, since Baz is `:Debug` so both impls apply for a Baz RHS.
Beyond autoref and autoderef, anyway.
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.
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.
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.
> 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.
• `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=c70a8c62fa0f304b1a8dceddf98...
That implies to me that "`Add::add(a, b)` (provided std::ops::Add is imported)" would be the most didactic explanation.
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.https://newrustacean.com/show_notes/e023/struct.script
but not
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.
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).
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. :)
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.
For example, i have a program that accepts data over the network, filters it according to some rules downloaded from a config server, and sends matching data somewhere. The packet handling, which needs to be fast and reliable, is in Rust; the downloading and parsing of the rules, which can be slow and just needs a sanity check at the end, is in Python.
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.