6 ms·
I had the opportunity to work with Rust >1.0 recently, implementing an image processing algorithm. Knowing that the compiler was looking out for me, a wingman
by fpgaminer 11y ago
I had the opportunity to work with Rust >1.0 recently, implementing an image processing algorithm. Knowing that the compiler was looking out for me, a wingman of sorts, was quite the pleasant experience. When coding in C I have a very paranoid mentality, constantly questioning every line of code and its impact on program state/memory. It results in my C code being almost always free of memory related bugs, but the work is absolutely _exhausting_. Rust was great in this regard, dramatically reducing the amount of mental capacity expended while coding. Either the compiler would catch the bugs, or worst-case a run-time assert would catch it and point me directly to the problem.
The major criticism I came away with, due in part to the type of program I was coding, was for Rust's lack of implicit type casting (more specifically, widening). What I mean is, adding a u8 and a u16 is an error in Rust. Rust will refuse to implicitly cast the u8 to a u16. These situations came up very frequently while implementing my program because I had to do a lot of optimized, low-level math. The scattering of type casts throughout the program resulted in clutter without any obvious benefit.
When I looked into the problem, the arguments I saw against it were often explanations that Rust is meant to be explicit and non-magical. But Rust, for example, already has type inference which I classify as "magical". Implicit type widening is hardly magical. And I don't see how it would be confusing or result in bugs, as long as only safe widening is done implicitly.
I think those involved in the Rust project were just scared off from it because of C's bizarre implicit type casting rules which result in bugs for typical programmers. I can understand that, but it's not like it can't be done better in Rust. Besides, if Rust is meant to be a system programming language, won't math between differing types come up often? And wouldn't handling those cases gracefully be a boon to productiveness in Rust?
- aturon 11y agoI definitely agree, this is a pain point in Rust today, and it's something the core team would like to address. You might glance at the thread here for some of the tradeoffs: https://internals.rust-lang.org/t/implicit-widening-polymorphic-indexing-and-similar-ideas/1141 https://internals.rust-lang.org/t/implicit-widening-polymorp... If we can reach consensus on a design, such a feature can be easily implemented. I'd love to see an RFC on the topic (https://github.com/rust-lang/rfcs https://github.com/rust-lang/rfcs)!
- kzrdude 11y agoHow to address this problem? fn foo(x: u32) { } let x: u16; let y: u16; foo(x * y); `x` and `y` would have to be multiplied before they are widened to `u32`. This creates an overflow bug that may only be visible in the released code (release mode has arithmetic overflow checking off by default).
- deleted 11y ago[deleted]
- jcranmer 11y agoA smart solution could be to have arithmetic operators automatically coerce the types to the type of their result, if the result type is already known.
- fpgaminer 11y agoIndeed, that is my intuition as well. This is the safest default behavior from the compiler, and even solves crazy expressions like: u64 = ((u8 + u16) * u32) / u8; It's hard to reason about what a programmer would want that statement to do. Coercing everything to u64 is the safest option. The idea, though, is to allow the programmer to use explicit casts to define exactly what they want when the need arises. So: u64 = (((u8 + u16) as u16) * u32) / u8; Would mean u16 addition, u64 multiplication and u64 division. So you get the benefit of safe, implicit type widening without losing the ability to micro-optimize when you want to.
- arielby 11y agoThe annoying thing is that (because of type inference) parts of the expression could be within different expressions: fn required_bytes(width: u16, height: u16) -> u64 { let size = width * height; // what's the type of size? size + 12 }
- fpgaminer 11y agoTrue, type inference would make an ideal solution difficult. But I'm totally fine with the compiler failing to apply implicit casting when type inference is involved. This code is just as readable, if not better: fn required_bytes(width: u16, height: u16) -> u64 { let size: u64 = width * height; size + 12 } I just really don't want to have to write this all the time: fn required_bytes(width: u16, height: u16) -> u64 { let size = (width as u64) * (height as u64); size + 12 }
- rthomas6 11y agoAs a hardware guy who has never touched rust, and I know nobody will agree with me: the solution is to not have type inference. You knew you wanted u16 or whatever to begin with. For low level programming I think explicitly defining type makes so much more sense.
- tomp 11y agoI'm not sure that's the solution int8 x = 12; int8 y = 84; int16 z = x + y; // ERROR: compiler is stupid
- continuational 11y agoWait, did you mean int16 z = (x + y) as int16; or int16 z = (x as int16) + (y as int16); ?
- Gibbon1 11y agoI think with C# the addition is mostly or always promoted to an int. So the following gives an error. byte foo = 4; byte boo = 86; byte arglebargle = foo + boo; // cast monkey! cast! I remember old assembly language guys annoyed at the types of math available with high level languages. I think they were a bit crusty but still they had a point about inability to manage precision without big hammers. At least the C# way forces you to think about whether you're loosing precision. But I dislike having to add casts. I use casts a lot, but I distrust the because they hide errors. I find myself really wanting safe operators and unsafe ones. Safe as in, overflow results in a hard fault that I can catch. Unsafe means overflow is silent.
- golergka 11y agoI think that although the commented above used `as`, he meant it as pseudocode and not C#, especially since C# is rarely used for low-level or perfomance-critical stuff where you would use int8 instead of int16 for optimization (in my experience).
- prewett 11y agoHere's why you might not want to have type conversion. I ran into this today: class String { int find(char needle, int startIdx); }; I ended up calling: int idx = find(startIdx, ';'); C++ happily converted my char to an int, and my int to a char, no warning or anything. Surprisingly, the code appeared to work for months, and then when I upgrade Xcode to the beta, it started failing. Weird. I end up casting stuff when I do math, anyway, because I can never remember when 'int op float' and 'float op int' results in an int or a float, and every time I don't it ends up as a truncated int when I wanted a float in [0, 1]. So, a little more painful to have to cast stuff around, but at least you find out quickly about my find() bug and you don't end up with something like if (secondsFromMidnightInFloat / 86400 > .5) {...} always fails. I can't tell you how much time I've wasted tracking something like that down, despite my diligence in adding (float). That and that stupid "->" for pointers drive me nuts. Even more than header files.
- kbenson 11y agoFrom the discussion linked[1] in another reply at your level, I believe the proposed automatic type conversions are only for when there's no data loss, and there needs to be explicit conversions whenever loss of information is possible. That is, it's implicit for i32 -> u32, or i32 -> i64, but not for something like i32 -> i8 (such as the int to char conversion you mentioned), which would need to be explicitly converted. 1: https://internals.rust-lang.org/t/implicit-widening-polymorphic-indexing-and-similar-ideas/1141 https://internals.rust-lang.org/t/implicit-widening-polymorp...
- SloopJon 11y agoAs a Rust newbie wrapping a C API, I ran into a baffling error for something like wstr.s[0 .. wstr.len], where the s field is an array, and the len field is a u16. I like static typing with inference, but I'm still getting the hang of the Rust type system. In this case, a little leniency would have been nice.