This is so excellent, and I love seeing long term, multiyear goals get completed. It isn't just this release, but all the releases in between. The Rust team and community is amazing.
I spent a few years as a scientific programmer and this is exactly the sort of thing that just bites you on the behind in C/C++/Fortran: the undefined behaviour can actually manifest as noise in your output, or just really hard to track down, intermittent problems. A big win to get rid of it.
It would be nice if there were both a saturating-as and a overflow-is-a-bug-as, the later of which is also saturating but in debug builds get instrumentation to panic if it ever actually saturates.
The overflow-is-a-bug-saturating as would be the default, and there would be a separate sat_as for "I know this saturates, it isn't a bug.". ::sigh:: Rust went through this same debate for integers, initially rejecting the argument I'm giving here but switching back to it after silently-defined-integer-overflow concealed severe memory unsafty bugs in the standard library.
Well-defining something like saturation actually reduces the power of static and dynamic program analysis because it can no longer tell if the overflow was a programmer-intended use of saturation or a bug.
Having it undefined was better from a tools perspective, even if worse at runtime, because if a tool could prove that overflow would happen (statically) or that it does happen (dynamically) that would always be a bug, and always be worth bringing to the user's attention.
So now you still get "noise" in your output, but it's the harder to detect noise of consistent saturation, and you've lost the ability to have instrumentation that tells you that you have a flaw in your code.
So I think this is again an example of rust making a decision that hurts program correctness.
`as` is not considered idiomatic in Rust code,. `.into()` for infallible cases and `.try_into()` for fallible cases are preferred in almost all cases (in the case of floats there's still discussion around the correct behavior of `.try_into()` for edge cases).
> So I think this is again an example of rust making a decision that hurts program correctness.
It might not be "idiomatic", but it isn't "unsafe", and so it is now providing a guaranteed behavior that... is it a bug? I certainly think saturation should always be considered a bug... like that's even worse to me than truncation as the semantics of truncation are at least interesting and related to mathematical basis of von neumann computing and sometimes extremely useful as a way to interact with memory-oriented systems, while the semantics of saturation are just subtle corruption that is almost useless to "use" for anything valuable (and are inconsistent, unless they are also doing it for integers? like if I cast a floating point to a 64-bit number and then to an 8-bit number do I get a truncated result instead of a saturated one?... I hope not, even though I reject saturation as a reasonable semantic). But if it is defined legal and guaranteed behavior, what should a tool do in this scenario? It can't say "you have a bug here" as maybe you wanted that behavior.
> But if it is defined legal and guaranteed behavior, what should a tool do in this scenario? It can't say "you have a bug here" as maybe you wanted that behavior.
Rust has a linter called Clippy which absolutely does call out things like that. It can do so because there are other mechanisms in the language which provide the same functionality without ambiguity. I believe casting with `as` is linted against by Clippy.
"unsafe" has a very specific meaning in the context of Rust[1][2]: particularly letting you perform actions that can potentially cause undefined behavior. Defining `as` as saturating is removing UB. That being said, the reason it is considered unidiomatic is because saturating or wrapping might not be what your intention could be, so other ways of turning one type to another is needed and should be recommended (into and try_into). The compiler should help you in going in that direction[3], and even though rustc itself doesn't warn you away from using `as` casts, clippy can[4][5][6][7][8][9][10].
It is "removing" undefined behavior by simply defining it to be something no one would ever ever want ;P. If you are willing to just define behavior arbitrarily then I think you will find the vast majority of (but admittedly not all) undefined behaviors can be made "safe".
Nobody wants undefined behaviour. That's always worse than defining it, whatever that definition is. In any case, there are better options for selecting the behaviour you want instead of using `as`.
Yes it is. It's used all the time. There's a reason as_conversions defaults to Allow in clippy.
Of course, there are lots of situations where `as` is the wrong tool for the job, but I think it's a bit of a stretch to call `as` "not idiomatic". It's perfectly idiomatic in lots of situations.
Whether or not it should be idiomatic is a separate question.
I think the parent poster is jumping the gun, but it is true that the flaws in `as` have been acknowledged for a long time and alternatives for specific use cases have been regularly introduced in order to reduce its use where possible. Giving it dedicated syntax might be one of the bigger warts of Rust 1.0: it may be the most convenient way to cast between primitive types, but it's often not the best way.
IOW, is it currently unidiomatic? No, code reviewers won't generally look sideways at it. But it's certainly getting less idiomatic over time, and that trend doesn't seem likely to stop soon.
Well or being a temporal throwback instead of gun jumping. The initial handling of integer overflow was another such decision in my view, but it was ultimately reversed.
Your claim that “as” being used is incompatible with it being unidiomatic is false.
The docs, the reference, and the RFCs all regards “as” as a mistake that should not be used, and _many_ features have been introduced over the years to reduce the cases in which “as” is the only alternative.
There are cases in which there is currently no alternative, so people “must” use “as”. This does not imply that it is idiomatic to do so.
Undefined behavior is memory safety; at least from Rust POV. A program with UB is not memory safe because the compiler makes no guarantees about the behaviors of those programs.
I'm not sure I understand this. Does it not produce a run time error? Why not?
This looks very dangerous, because it essentially does the "nearest to right" thing. Say, you cast 256 to a u8, it's then saturated to 255. That's almost right, and a result might be wrong only by 0.5%. Much harder to detect than if it is set to 0.
> I'm not sure I understand this. Does it not produce a run time error? Why not?
It’s not supposed to. Type casting with ‘as’ is supposed to be lightweight and always succeed; there is no room in the type system to return an error. In case lossless casting is not possible, some value still has to be returned. Until now, this was outright UB — meaning the compiler is not even obligated to keep it consistent from one build to another. Saturating, while still not optimal, is at least deterministic.
> This looks very dangerous, because it essentially does the "nearest to right" thing.
That’s why the intention is to introduce more robust approximate conversion functions and eventually probably deprecate ‘as’ casts altogether. There has been a number of discussions about this; current disagreements seem to be about how to handle the various possible rounding modes.
> meaning the compiler is not even obligated to keep it consistent from one build to another.
Way worse than that. The compiler wasn't obligated to act like anything at all. It would be totally legal to compile it so that the first time the value was accessed you got 0, the next time you got 1 - within the same program execution, with no mutation of the value. That is the sort of thing that is observed behavior of UB in the worst cases, and why it's so terrible to just pretend that UB is innocuous.
Way worse than that, even. UB poisons every state of the program that eventually results in UB. For example, the optimizer is well within its rights to remove as dead code any branch that, if taken, would provably lead to UB at some arbitrary future point of execution.
Way worse than even that (you might be noticing a theme here...). Once the optimizer has removed as dead code any branch that, if taken, would provably lead to UB at some arbitrary future point of execution, it can conclude that the other branch is now the only possible execution, and call it unconditionally, even if that leads to removing all your files (the classic example is https://kristerw.blogspot.com/2017/09/why-undefined-behavior...).
main()
x = get_from_some_external_data_source()
if x:
print("Hello World")
trigger_ub()
You might expect this code to always print if x is true but the optimizer can look at this and say "welp, if x is true then it would trigger ub, therefore it must be false, and since x must always be false we can just remove that entire branch."
My favorite example along these lines (in C) is "Cap'n'Proto remote vuln: pointer overflow check optimized away by compiler"[1] which was covered here a few years back and shows all of these "theoretical" compiler behaviors coming to a head in a real bug which is thoroughly explained.
Yes, it would. However, it rarely does because of how the incentives are: it's useful for optimization to pretend that UB never happens, and more optimization leads to faster binaries, which are what the compiler engineers often pursue.
Right, but given that panicing was already allowable behavior, why was the new behavior chosen to be one likely to introduce subtle bugs? It seems much better to loudly proclaim the existence of an erroneous precondition, which is consistent with how things like array indexing behave.
I guess I’ll have to go dig up the RFC discussion on this one; it should make for interesting reading.
1. Panicing is not in line with how `as` casts are supposed to act. (e.g. `u32value as u8` does not panic but just takes the "lower" one byte.)
2. This might (I haven't profiled it) introduce performance regressions in ways which should not happen.
3. Besides in some usages around `dyn` other usages of `as` get increasingly more alternatives. It's just a question of time until `as` (for int/float casts) is recommended to not be used at all, maybe even linted against.
4. Given precedence of many other programming languages people don't expect a "simple" float to int cast to be failable. (The new methods replacing `as` make the fallibility clear, as it's e.g. `u64::try_from(bigf64)`).
5. It's udef-ness is only detected/handled in llvm, _I don't know_ if llvm provides similar well integrated mechanisms for this as it does for integer overflows. If not that would be another problem.
I came here with same question as davrosthedalek "Why doesn't it just panic?" and yours is an excellent and very convincing answer. Thanks for writing it up.
That phrasing seemingly implies that there was a generally-agreed intended behaviour for that case that was unfortunately neither documented nor actually exhibited. But that was not the case. The overflow behaviour of float-to-int casts was undefined, full stop. There was some consensus as to what such casts should not do, but no agreement on what the actual behaviour should be. Eventually, the discussion settled on defining casting overflow to saturate, and the implementation was altered to match.
> Right, but given that panicing was already allowable behavior, why was the new behavior chosen to be one likely to introduce subtle bugs?
Because casting to floats is not UB in the Rust spec, it's UB in LLVM. That's the whole reason this was an issue in the first place.
Now, Rust could have chosen to define the behavior to panic, but so far it's been a hard and fast rule in Rust that as casts do not panic. You would have to have a much better reason to change that then "well, it was UB before" since (1) nobody wanted it to be UB before, and (2) the actual implementation never panicked (and people absolutely rely on the fact that casts don't panic in unsafe code).
I thing you are right about that, I'd prefer panicking too. Also, reading the RFC will surely clear up the motivation.
Without knowing this case, I'd wager a guess: it's about performance. Panicking introduces a branch and side-effects which, again, affects negstively optimization potential and performance. The saturating cast affects performance too, but less. If some old code has a lot of number crunching containing these operations, a big performance regression would be nasty.
It looks like ‘as’ is generally defined as a truncating cast operator for other numeric types. Since it doesn’t panic for other overflow situations (like u64->u32), they chose consistency with those cases.
I think the long-term plan is to deprecate `as` entirely (probably in a future edition). You will then be free to pick between a function that panics, one that gives you a Result, or that saturates, etc. I believe most if not all of these functions already exist.
Compilers are better at eliding array index checks than about eliding numeric overflow checks; an ordinary function that operates on arrays will often be able to reasonably determine both the length of the array and the scope of the indexing variable (Rust's iterators are very good at this), whereas an ordinary math function often has almost no information that usefully limits which values it might be getting called with.
I don't disagree that making more obvious errors into panics would be nice, but the performance implications are often quite unforgiving.
They aren't unforgiving, because if you need performance you can use another function that does not do the check. Same as for arrays.
It seems strange to have a cast as a safe operation and yet return results that are almost surely a bug. This means I will avoid casts altogether in my code, and I do hope they get rid of them at some point as some have suggested.
> They aren't unforgiving, because if you need performance you can use another function that does not do the check
Rust tries hard not to cause regressions in users without good cause, which includes avoiding runtime performance regressions. One of the reasons that this fix took so long was that benchmarking prospective solutions revealed unacceptable performance regressions in users, even those who were already "doing the right thing" by manually upholding the proper invariants (no NaN, and value within range). The tension is that something still needed to be done, because at the end of the day the Rust creed is still "no undefined behavior without `unsafe`".
> This means I will avoid casts altogether in my code
Indeed, this is hardly discouraged wherever possible. For converting, say, a u64 to a u8, use `foo.try_into()` in order to get a conversion that follows the usual Rust conventions around Result-based error-handling (which didn't exist back when `as` was first conceived, or when this bug was originally filed). Casting floating point types to integers is already a rather rare use case in the first place.
Seems a pity they didn't add a built-in at the same time that's a bit more nuanced. It could maybe return an enum with Success(value), Underflow, Overflow and NaN. That way it's up to the coder to decide whether they want a saturating cast or to check the result explicitly.
For consistency you would also expect `257u32 as u8` to panic (which it doesn't, and never has); the `as` operator has always been about fast-and-lossy conversions, with the standard library ideally providing methods for more principled conversions.
For better or worse, Rust 1.0 released with the philosophy that the `as` operator is for "fast and loose" conversions where accuracy is not prioritized; e.g. casting a u32 to a u8 would always risk silently truncating in the event the value was too large to represent. Over the years the language has added a lot of standard library support for bypassing the `as` operator entirely, and I think the prevailing opinion at this point might be that if they had the to do it all over again they might not have had the `as` operator at all, instead making do with a combination of ordinary error-checked conversion methods and the YOLO unsafe unchecked methods as seen here.
Which is to say: it's not that they technically couldn't have gone with the panic approach, but (performance implications aside) I think they'd rather just start moving away from `as` in general wherever possible.
It seems to me (FWIW as someone curious abt Rust but not seriously using it yet), that "as" should require a warning label. If "unsafe" means something else, then "unsound" could be used. If the types are always safe to convert, "as" is fine, but if the types involved allow for a lossy conversion in any case, the compiler would require you to write "unsound as". You are both acknowledging that you are aware of the issue and notifying future users of that code of a potential problem. Rust could go ahead with the saturating conversion to remove the UB AND require the "unsound" warning label.
If required to an an "unsound" label, that might be enough push for some developers to add their own explicit "if" guards or use another conversion method without having to deprecate "as".
"unsound" has a particular definition that is separate from what people have issue with here; in Rust parlance, there's nothing unsound about the `as` operator choosing to saturate or overflow etc., because that has no ability to break the guarantees provided by the type system.
As for requiring an additional speedbump (like e.g. a "lossy" label) here to guard against misuse, I think this proposal is overlooking something: Rust can't just abruptly break all code that currently uses `as` in order to demand that something like `lossy as` be used instead. Any removal would have to first have a very long period where `lossy as` is syntactically valid and where the compiler instead warns for people using raw `as`. But if the compiler is already emitting a mere warning for `as` that suggests a better alternative, then it could just as easily suggest a method like `.try_into()`, which exists today. And once you're having the compiler warn about changing `as` into something else, that's already indistinguishable from deprecating `as` in those instances, so there's no point trying to avoid it.
I don't know about the next edition; `as` is also used for a small number of non-numeric conversions, like turning a reference into a raw pointer or for creating a trait object. In order to completely remove `as`, you would need to first introduce alternatives to those (the trait object use case specifically might be a bit weird as a method rather than a keyword, but I don't know if that's a big problem).
Alternatively one could just deprecate `as` for numeric conversions, although there are still some library holes that would need to be patched up (e.g. other than `as` I don't know of an existing single method to say that you want a fast integer conversion routine that merely truncates a u64 into a u32; right now the alternative is `try_into`, which does a runtime check and returns a Result). It might also exacerbate tensions with some people who for quite a while have been grumpy that Rust requires frequent explicit numeric conversions when doing things like indexing (which always requires a `usize` type, so you see `foo[bar as usize]` unless people want to always work with usizes directly); would these people be happier if that were `foo[bar.as_usize()]` instead, or would this all be blocked on a discussion about being more lenient with numeric conversions?
Most floating points aren't integers anyway, so if you think of it as casting 256 to the nearest u8 it's correct, same as rounding 0.25 to 0.
NaN to 0 is a bit more concerning, but inconvienence and compatibility need to be weighed against catching every error (no type system will catch every bug).
Fortran predictably overflows the result, in contrast to fptoui which gives a poison value. I agree that even a predictable overflow can bite you on the behind. But it's better than undefined behavior. I'm not a fan of saturated cast, but having a defined behavior is for sure a great improvement.
When working with audio as an example, saturation is much less obnoxious than wraparound. You can potentially destroy speakers and your hearing that way.
For those not in the know, computer controlled Civs would be assigned behaviour attributes based on historical personality. Each attribute would be 1-10, stored in an 8 bit integer.
These attributes change in response to events in the game. When a civilization would reach the modern era, it's aggression attribute would be reduced by 2. Gandhi's aggression was reduced from 1 to 255 (underflow). He would rain down nukes on everything in sight, somewhat uncharacteristic of him.
Fans liked this so much that Firaxis maintained the same behaviour in later games.
It's a cool story, but I don't think there is evidence that it actually happened (in Civ 1, certainly in later games as a homage to this potentially urban legend). The YouTube channel People Make Games did an investigation[1] about this a year ago, including interviews with the original developers.
Saturated addition is indeed the correct way to add two PCM streams together.
Especially back in the 1980s and 1990s you'd get awful code that did things like averaging the two streams because wrapping sounds awful and the authors were ignorant of the theory and/or unaware that saturated addition is a thing.
You can tell when somebody did this because it means playing silence makes everything else quieter, or worse there's an arbitrary limit on how many streams are played and playing any one thing is very quiet because it's attenuated so as to never wrap.
Haiku the 1990s-style operating system did this for years, as did various Amiga music software.
At least on x86, saturated adds didn't become a thing until MMX was released. Doing saturated adds was simply too expensive on older machines, and usually required a branch.
They're not math errors, it's the most sensible thing to do: when you overload a speaker, you get clipping, not some weird thing where the cone snaps to the opposite side of the range.
I’m talking about the basis of the analogy, not the analogy. If you’re processing audio signals, saturation (i.e. clipping) is of course preferable to the alternative.
“Destructive”, regardless how minor, should be intentional or at the very least conscious. This is one of the various reasons why serious audio processing is done as 32 bit float: no (practical) risk of unexpected overflows.
Standard? True, undefined. Compilers, however, happily overflow unless you tell them to catch it at run time. So, my original statement is at least half false. :)
It's work in progress, however you may want to take a look at https://rust-lang.github.io/unsafe-code-guidelines/. As far thread safety goes (threads, locks, atomics), memory model matches C++ specification.
I believe it's safe to assume that Rust's memory model is (a subset of) C++11's memory model, since that's what LLVM implements. At this point I don't see how any specification could deviate significantly from that without breaking tons of code.
The Rust memory model is deviating from it a little in order to enable some norestrict-based optimizations that aren't really done for C, even though (as you know) LLVM can't really take advantage of them yet.
Sure, or with C++ TBAA it can do that too, but those rules work differently from the kinds of guarantees that Rust's type system provides. The C equivalent is (sort of) restrict, but it's used very rarely--so no one has really worked out what its semantics would be like if it were used at the same scale it is in Rust, and there are a lot of bugs.
It is nice that they have defined behavior for that now.
Though I try to always scrutinize any floating-point / integer conversions during code reviews. The default casting of a floating point value to integer is frequently not what you want, however. In the code we do, for example, you will usually round to the nearest integer instead, we don't normally need the more fancy rounding schemes.
Out of interest, with (my instance of!) 1.44, "let i = 257.0; i as u8" casts to 1. "let i = usize::MAX as f64 + 1.0; i as usize" does likewise. If you assign i as an integer DIRECTLY, however, it saturates.
To be honest... struggling to see why you'd do the former, outside of situations where you're happy with saturation, though I haven't thought about it a lot. Agreed that a consistent behaviour is a big help - I can work with "Rust does x in this scenario".
Again, UB means the compiler is allowed to do anything at all with code that triggers UB. It’s not just that the value you get is undefined, the behavior of the whole program becomes suspect. Including anything that happens before the UB is actually triggered, not just after that.
In Fortran, there is the ieee_arithmetic intrinsic module that can very nicely and robustly handle undefined behavior. If people do not use it, it is their problems.
I keep seeing more and more news about Rust, and figure that perhaps it is time that I learn something new.
99% of my development work these days is C with the target being Linux/ARM with a small-ish memory model. Think 64 or 128MB of DDR. Does this fit within Rust's world?
I've noticed that stripped binary sizes for a simple "Hello, World!" example are significantly larger with Rust. Is this just the way things are and the "cost of protection"? For reference, using rustc version 1.41.0, the stripped binary was 199KiB and the same thing in C (gcc 9.3) was 15KiB.
That is a bit extreme but it demonstrates the lower bound.
There's a lot of things you can do to drop sizes, depending on the specifics of what you're doing and the tradeoffs you want to make.
Architecture support is where stuff gets tougher than size, to be honest. ARM stuff is well supported though, and is only going to get better in the future. The sort of default "get started" board is the STM32F4 discovery, which has 1 meg of flash and 192k of RAM. Seems like you're well above that.
That is not entirely correct for Rust. I know because I helped a friend of mine debug the porting process. (Apple did not see fit to give me access to the hardware, alas.)
(Also, not to be super pedantic, but this (among other things) is partially why I said "and is only going to get better in the future," that is, the support is generally good (I know, I am literally doing some of that in another tab right now) but not flawless.)
Yes there's a fixed cost because of the std library, panics unwinding code (i.e. clean recovery instead of aborting when something goes wrong), and the tendency to statically compile everything in adds some more, but in embedded context (a.k.a. "no_std") you can observe C and Rust code are very comparable.
Especially in your case you'll find Rust to be a joy to use: you'll have way more confidence in your code being able to run for months without segfaults or memory leaks. And if you have a good understanding of the C memory model using Rust will be a breeze.
If you're running Linux with MBs of RAM, then you're well within Rust's comfort zone. If you're on embedded ARM (cortex-M) with KBs RAM, then Rust will be really nice and it does work today, but there's still a few missing features to make it nice to use.
The following is a very good resource, going from safe/practical ways to reduce the size (like `strip`) to more advanced and unpractical builds (up to 8KiB hello world, removing... a lot).
> I've noticed that stripped binary sizes for a simple "Hello, World!" example are significantly larger with Rust. Is this just the way things are and the "cost of protection"?
Some simple firmware I'm writing (controls some lights, takes rotary encoder input, prints stuff on serial) is currently 8K. It's written in Rust and targets the stm32f1xx and stm32f4xx chips.
I've been playing with Rust myself in my free time, and if you use Rust with the standard library a stripped executable should be smaller and more comparable to C than what you're seeing with the standard library included. Depending on your use case, you might be able to get away with just using the core library
Dunno if it's apples and oranges for you, but I've seen Zig [0] being thrown around here previously as a "safer" embedded C alternative, albeit more minimal than Rust. May be worth comparing (I haven't tried either language).
I think it makes sense to get your foundation and ergonomics correct before piling on features. Otherwise you end up building features that may need to be completely restructured/redone later.
I recently switched from old sync versions of hyper and postgres to the new async versions. [1]
It wasn't hard, but yeah it was not fun either.
I can only imagine it's worse if you're actually writing the libraries and not just a CRUD app like I am
[1] Apparently the postgres crate was a wrapper around tokio_postgres all along and I didn't notice. So to remove a dependency I switched to using tokio_postgres directly
They've really only spent about a year or so on async support in Rocket. They were waiting for async/await to stabilize and the async runtime story to centralize a bit (though they picked Tokio as the default).
Features like CORS are available via third party fairings, but I could see them being incorporated in the future in the `rocket_contrib` portion. I think the goal is to keep the overall framework pretty light and put more things into the `rocket_contrib` portion.
When I started learning Rust over a year ago now, Rocket was extremely appealing to me. I never subscribe to any GitHub threads that I'm not involved in, except for Rocket on stable.
In general, stable proc macros is an awesome step for Rust.
> [...] it's not even 1.0 yet. Thus, it isn't stable enough for production use.
API stability (i.e. how the API will change in future) is largely unrelated to the question of whether you can trust it to work in production.
Maybe for some people API stability is a "must have" for production use, for the sake of minimizing churn when upgrading dependencies, but that's far from a universal principal.
I think people often get confused about different meanings of "stable". I've worked with plenty of libraries with stable APIs that are buggy piles of hacks. And I've worked with plenty of libraries with unstable APIs that are rock-solid in production. They're different concerns, but people seem to conflate them a lot.
So, to clarify, any statement like "not even 1.0 [...] isn't stable enough for production use", made without qualification, is a non sequitur.
Well I guess I have to expand the symbols for you, here we go:
No api stability heuristically imply no production readiness does not imply that api stability imply production readiness.
As you snarkily imply, the contrapositive should be common sense and not need to be stated. Yet your brain failed to see that your comment on gets() is exactly such a needless truism.
"not even 1.0 [...] isn't stable enough for production use"
This reasoning is as many others a probabilistic one but it make sense.
Being production ready (technically everything can be with enough churn, and duplicate work production) here means that it is at least decently competitive with others offering regarding productivity, performance and feature completeness, translation: you won't get fired/risk your product by choosing this lib for production use on a medium scale project.
If we agree on the definition, then it generally follow that being productive (e.g ergonomic API), optimised (polishing work), not having rough edges, and having implemented the many required features (a server framework actually require a LOT) all those steps are done AFTER the foundational work of pre 1.0
Those after work will probably sometimes break API stability as we are not omniscient and forward compatibility is non trivial hence the needs for production ready frameworks to have had many breaking change releases so more like 3.x than 1.x
Hence, it follows that the temporary but general API stability guarantees from a 1.0 are insufficient and general means that is can begin to be used, not that it should be used.
Rocket and any other rust server framework are not production ready as soon as you go beyond trivial use cases.they have dangerous foundational bugs and show stopper missing features.
I have built a startup product with actix web (but studied rocket too) and those are dangerous economic bombs.
The Rust ecosystem typically has much more conservative version numbers than other ecosystems though, and higher quality standards. There are several very high quality crates with 0.x version numbers.
A fairer statement might be that the rust ecosystem has inconsistent version numbers. I've seen both flawless v.14s and awful v.7s. There are a few ways to figure out the difference like sorting by downloads on crates.io to find packages that are commonly used.
There is a curious parallel to the rise of Go which had no versions in their ecosystem as the language was adopted.
One could argue that if you are relying on magic number schemas to decide if a library is stable enough for your usecase, it is you that is doing it wrong.
It's no longer magic number once you follow semver
https://semver.org
Of course it only offer limited information but this information can tell you whether you should be not confident in it. Pre 1.0 is such a signal.
By any chance if anyone is in the Paris area and is interested to teach Rust at university next year during the first semester. Please get in touch :).
Is this undergrad? Genuinely curious, how will you get someone to understand what ownership helps avoid without them having experienced the pain on the other side?
I guess with younger and younger kids learning programming these, may be there can handle more? I am not sure if my son would understand all of the intricacies in his first semester.
University of Maryland teaches Rust for a single lecture as a part of their CMSC330 (Organization of Programming Languages) [1] undergraduate class. The lecture slides are available online [2].
One section of CS3210 Design of Operating Systems at Georgia Tech in the Spring was experimentally using Rust[0]. I'd never used it before, but by that point I had a decent amount of experience with C from previous classes. I personally didn't struggle with the borrow checker too much, because I did have some experience with what happens when there isn't ownership. I took another C and C++ heavy course at the same time (CS4290) and wished I could use Rust, since most of my trouble with projects came down to debugging pointers not being in the right place. I'm in the Systems concentration though, so I don't know how people only experienced in, for lack of a better term, higher-level languages would fare. Still, I think that students interested in taking a Rust class would have enough experience with other systems languages to be able to see what ownership can do for you.
Maybe the professor for this class could assign a non-trivial project in C at the start of the semester then the same project again at the end of the semester except in Rust.
Yes it is a course aimed at undergrad students, in their second year at university.
Of course they won't be able to grasp everything that Rust has to offer, but that is true of any language. I think Rust will expose them to many theoretical and practical CS concepts that they will be glad to have at least heard of during their studies.
In our degree, the first year students learn to program with Python, Racket (or OCaml depending on which teacher they get), C, Prolog, Bash, … Each of these language have way more to offer than what they can grasp. But each of them offers a different approach to programming and help the students to actually learn to program (rather than learning to write Java code, for example).
The course in question is actually called "Advanced programming". I want to experiment with a Rust course in second year as a kind of followup to both the functional programming course (the Racket/OCaml one) and the imperative programming course (the C one) that they have during the first year. If it really doesn't work, we'll change for something else or simply swap back to it being "Advanced C programming" for instance. But first, let's try to make the Rust experiment work. I really think it can benefits our students!
sorry, when you said first semester, I assumed you meant first semester of the 4 yr course, as in intro classes. But you probably meant first semester of this year. Either way, it is good to experiment. Thank you for doing that.
> Genuinely curious, how will you get someone to understand what ownership helps avoid without them having experienced the pain on the other side?
This is an eternal debate about Rust. I don't think it's required though. Can you appreciate functions without understanding assembly and calling conventions? I believe the answer is yes :)
I actually find that people who aren't that familiar with programming are pretty quick to accept the idea that aliasing xor mutable is an okay rule... not because it's been exhaustively explained, but just because people have no real expectations at all about how things should work when they're starting out. A lot of language rules seem arbitrary at that point. Afterwards, when using other languages, they may even find it surprising that you can mutate stuff without this rule... it's all about what you're used to.
From someone who is self taught and learned C as a second language, is teaching rust at this stage a great idea? there is a lot of news about rust lately and it very much seems like people are seeing rust as a hammer and the world as nails.
I don't know. To be honest, having taught a lot of programming stuff, I don't think there is any one best approach to learning programming, and it's very hard to figure out what will be easy or hard for someone just learning. People also learn languages for different reasons--for many people, for example, the main reason they're learning is to get a job in the industry, and if that's the case learning Rust is not really a good idea regardless of any of that stuff, since there aren't that many Rust jobs compared to, say, Python or JavaScript jobs.
I'm building an embedded project that currently runs a python script for automatic brightness. It takes a brightness value from a sensor over I2C, applies a function to get an appropriate LCD brightness value and then sends that to the display driver over a serial port. Would this be an appropriate project to write in Rust to learn the basics of this language?
Yup, I've got a small little rust component that translates a couple industrial sensors on modbus + rtl_r443 over to an influx database and it's been happily running along for a few months now.
As someone who hasn't used Rust, I am curious about why Rust has macros.
I use C++ at work, which admittedly isn't the language I use most, and macros are used quite a bit in the code base. I find they just make the code harder to read, reason about, debug, and sometimes even write. I don't see them really living up to their claimed value.
Is there something different about Rust's macros that make them better?
There is almost no intersection between the kind of things that can be done with the C++ macro system and the kind of things that can be done with the Rust macro system. They are not related. You can see them as another feature that is not available from C++.
You can get quite close to the use case for Rust macros by considering C++ template-based metaprogramming. Of course the biggest difference is that Rust macros have been designed from first principles, not as a clunky afterthought.
>I use C++ at work, ..., and macros are used quite a bit in the code base.
Ouch. Modern C++ has alternatives to C macros. Is it an old code base, or is it just written in the C++98 style?
Today people will typically use constexpr instead of #define. While macros can possibly do some funky things constexpr can't, you'd be hard pressed to find those things. (C++ supports constexpr if, constexpr functions, constexpr lambdas, math, and so on.)
If you have the time and effort, it may be worthwhile to slowly start modernizing the code base, bit by bit.
The advantage of modern day C++ is it catches errors at compile time that older versions did not and would crash while the software was running. You might improve the stability of the code base if you help out.
Writing macros (in any language) is a way of creating an abstraction. Creating abstractions is a way of automating the job of programming, making it ideally more efficient and less error prone. This is why people usually prefer java to basic.
Marcos are a way of creating abstractions that are particular suited to be "concreted" by the compiler, making them a ideal match for programming languages that seek to be "close to the metal" like C, C++ and rust.
C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them. IMHO the real macros of the C++ language are the templates and constexpr, althou they are limited in other ways. E.g. its hard to extend the syntax using them or do certain things like making the calling function return. They grow ever more powerful, with their own type system (concepts) and things like std::embed and static refection so they finally feel like a real language, alsbei a clumsy, pure functional language that feels alien a C++ programmer without exposure to haskell.
Rust macros are actually meant to feel like Rust, not some ad-hoc bolted on language.
> C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them.
I think this may be why I'm having a hard time appreciating them. Probably half the macros I see could just be a function call. The majority of those that don't are hiding a conditional return or goto, which I find to be a net negative.
I'll probably have to use a language with good macros before I can appreciate them.
> Probably half the macros I see could just be a function call.
Yes, that particular breed of C macros would likely manifest in Rust as people just defining a new function. In Rust, you tend to see macros in places where "just make a new function" doesn't suffice for whatever reason; for example, maybe you need to define a dozen different structs that only differ by the type of one field, so instead of actually defining the struct a dozen times, you could just define the struct inside the macro and then `define_my_struct!(u8); define_my_struct!(u16);` and so on.
You also can't use Rust macros to "redefine" other unrelated pieces of code, so that's one less thing to worry about.
The first thing I would say is that regular inline macros are hygienic and type safe, just like the rest of the code you write. That way they're not just text expansion but smart code expansion.
When I say hygienic, I mean that when a macro uses a variable that isn't in the parameters or isn't static, it will be a compile error as it cannot know the scope. Any variables defined inside are scoped. Only parameters parses in can be referenced from outside the scope of the macro
They're also notated different to regular functions and they can't appear anywhere so it's obvious when you see a macro that it will expand into some code that won't break any of the other code in your function.
Of course people can write really terrible macros but typically macros serve a single very simple task and should be well documented and in my experience they often are.
As for procedural macros. They are just ast in, ast out functions, but written as a library in regular rust rather than some language thrown on top. That makes it easy to reason about the code and make it safe. If you write them well, they can be very good at reporting errors in usage to users.
The std lib also provides some very straightforward yet useful macros to act as inspiration. String formatting is an inline macro. vec is a macro to quickly define Vectors. Derive debug, partial equality, default values are all procedural macros and they are very straightforward yet tedious tasks to do on your own all the time.
Given the macros people publish, I would say they've done a good job at securing them as a useful feature. I've not seen many instances in actual code bases of messy macros. For examples of great macros, see serde[0], clap[1], inline-python[2]
I’m not a rust programmer, reading your explanation here it’s not anymore clear why some macros exist (println for instance). They very much look similar to functions in most of the cases I’ve seen (again from the perspective of trying to find a reason besides safety to learn rust) so why exactly do these exist? They are a great source of confusion for those of us with C/C++ experience (or at least me) which is the target audience.
println is a macro in Rust because 1) the language currently lacks support for variadic functions and 2) being a macro allows the format string to be parsed at compile time.
Long-term, with const generics, and VG (or a macro that creates a (a, (b, (c, ()))) nesting like the hlist crate), we could maybe replace format_args! and friends.
However, there is one more thing a function can't do: borrowing arguments. Formatting never moves arguments because the format_args! macro generates references to them, which it then creates std::fmt::Argument out of.
Most languages end up needing a way to step outside that language, and if you don't have macros you end up using something even worse. Rust in particular lacks "do notation" or proper support for higher-kinded types, so it needs to use macros as an ad-hoc replacement for things like sequencing async operations or proper propagation of errors. To be honest I'm surprised they didn't find a way to make a web framework in plain rust though.
In my experience, Cargo is much easier and more reliable than the systems I've used across Java/C++/Scala/Python/Go/JavaScript. It is such a pleasure to use.
Here are the problems I've had with other build systems, as a noob, that I have not had with Cargo:
- Having to learn weird syntax and constantly look up the reference manual (CMake)
- Having to manually add source files (qmake)
- Sometimes it just needs a clean and nobody knows why (Visual Studio)
- Having to remember to set up debug and release builds and decide your directory layout for everything and figure out what 'a shadow build' is and who gives a crap since it all takes too much HDD space either way (qmake, CMake, make)
Also having tests built-in is really nice. Rust is the only language where I bother writing tests. Everything else makes it too hard, as if entry points into your binary are supposed to be rare and expensive.
The new API to cast in an unsafe manner is:
let x: f32 = 1.0;
let y: u8 = unsafe { x.to_int_unchecked() };
But as always, you should only use this method as a last resort. Just like with array access, the compiler can often optimize the checks away, making the safe and unsafe versions equivalent when the compiler can prove it.
I believe for array access you can elide the bounds checking with an assert like
assert!(len(arr) <= 255)
let mut sum = 0;
for i in 0..255 {
sum += arr[i];//this access doesn't emit bounds checks in the compiled code
}
I'm guessing it would work like this with casts?
assert!(x <= 255. && x >= 0);
let y: u8 = x as u8; // no check
If the array length is known to be strictly less than 255 then there is definitely an out-of-bounds access inside the loop, but since this is a panic rather than undefined behavior it could matter how many loop iterations are executed before the out of bounds access occurs, so the check can't be omitted.
If the array size is definitely greater than or equal to 255 then all the array accesses in the loop will be in bounds and no further bounds check is required.
But the loop goes up to 255. So if len(arr) == 10, then assert!(len(arr) <= 255) woulds succeed, but you'd get an out-of-bounds access if you tried to access arr at 11.
I would say that my personal take of the temperature is "vaguely pro but not a slam dunk", at least from the opinions I've seen. Only one way to find out.
Rust has great libraries to make life easy, eg https://docs.rs/fixed/1.0.0/fixed/ (Note: I haven't benchmarked the 4 or 5+ fixed precision libraries Rust offers to see which is best.)
>All you need for algotrading is to query an api. Rust would be a poor choice for that anyways, like using a semi truck to carry your bike around.
That's not really true. It's more pulling from a database or csvs as backtesting is the most important part, which is also why the person you were replying to was asking about backtesting specifically.
Most firms roll out their own programming language, because before Rust existed there wasn't really a language that was a good choice for algo trading. Algo trading needs a few things:
1) It needs a financial number data type. That is, base 10 precision. Floats and doubles will not cut it when dealing with money.
2) The language needs to not implicitly do type conversions, so your types do not accidentally get converted to doubles.
3) You want provability. That is, you want guarantees that your program will run exactly the way you intended or you could lose a lot of money.
4) You hopefully want it to go fast, or backtesting could take ages. Historically super computers have been preferred, but that is probably not the case today. (This isn't even for HFT, just scalping and swing trading.)
Most in house languages in the industry are functional programming paradigm, because it allows guarantees. What you see is what you get. It allows one to write in a more mathematical way.
Today Rust takes the cake, as it is the only mainstream language that meets all the criteria, despite not being a functional programming paradigm language.
Performance is super critical in high frequency trading, so Rust sounds like reasonable choice. Having your code run a millisecond faster means beating out a competitor with the same algorithm as you, getting you a better price.
Be aware that Rust gives you the tools to be fast, it is not necessarily fast by default, although a lot of constructs it guides you towards usually help with that. You still need to profile your code to see what you need to optimize, whereas other languages with fewer knobs will perform optimizations that you otherwise need to manually annotate in your code in Rust. I prefer this approach, but it can be surprising to people used to the alternative.
Do you have examples of this? I'd be curious to know if so. (I've played w/ Rust a little bit -- I implemented a Boggle board scorer + high-scoring board generator; Rust outperformed my C++ code! I was impressed.)
These three different fn definitions have two different behaviors and affect both the speed of the code and the speed of compilation and it depends entirely on how they are called.
The first one is what the language calls generics: they are always monomorphized, which means that if you have three calls to `foo` with different types (that implement Trait) the compiler will expand three different functions with different types (code expansion).
The second one is a separate syntax level feature (impl Trait) which was mainly added to introduce a new feature which is static opaque types, where the function determines what the underlying the return type will be, but the caller can only interact with it using the trait's API.
[Aside] This is useful for cases like the following:
The more types you nest the more the benefits come into play. [end of aside]
Now, with that out of the way, the type of an impl Trait in argument is decided by the caller (not the function), so they are implemented internally exactly the same as type generics. The only difference is arguable nicer syntax in the definition and not being able to specify a type using the turbofish. For all intents and purposes, those two are the same feature.
The third function is different, it uses a virtual table, with everything that implies: there's type erasure, there's only a single function in the expanded code (which makes compilation faster because the compiler doesn't need to do work), calling this function can be slower because the final executable has to perform some pointer chasing to call methods, instead of directly knowing where to call them.
All of this to say: if you use `fn foo<T: Trait>(_: T)` or `fn foo(_: &Trait)` affects compilation and execute time, so you have to be aware of their distinction. This means that if you're not aware you might have slower code than you would with a compiler (like Swift, for example) which relies on heuristics to decide to do static or dynamic dispatch, but it also means that your code's performance characteristics won't change all of a sudden because you modified a tangentially related part of the code and suddenly crossed some threshold.
Another example can be `.clone()`: is it slow? The answer is always "it depends". You might be cloning an `Arc`, which is cheap, you could be cloning a 10MB string, which is slow. But because we train ourselves to see clone as slow we might be worried or annoyed by `Arc`. We could make it `Copy`, but if we did that then you have less control over where the `Arc` gets copied which would make it harder to keep track of where the RC gets incremented. The language also doesn't automatically implement `Copy` for small structs, even though it could, which would make it easier to learn that part of the language (you don't learn to add derives early on), at the cost of baffling behavior (you might add a field and suddenly your struct isn't considered "small" anymore).
Yet another example, you also have access to `Cow<'_, str>`, which lets you deal with both static and heap allocated strings in the same way in your code, but it pollutes your code, where the naïve thing to do would be to use `String` everywhere.
My personal wish is for Rust to remain explicit as much as possible, but use lints to emit suggestions for the cases where a more "magic" language would change the emitted code. That way the code documents its behavior with fewer surprises.
The monomorphization-vs-dynamic dispatch thing feels natural from a C++ perspective, as it completely mirrors the choice of achieving 'polymorphism' via templates or virtual methods (though of course the Rust syntax is way nicer!, using traits for both, whereas in C++ you have either a class definition or...nothing, just ungodly compile errors ("compile-time dynamic typing")).
That's interesting re Swift. It seems similar in a way to using heuristics to decide whether to inline a function or not.
I _think_ C# does monomorphization for value types ("struct") and vtables for reference types ("class"), though I wouldn't bet on it...
> fn it() -> impl Iterator<Item = i32> {
One of the things that impressed me w/ rust was being able to write really concise code using ".map()" and friends and finding that it all ended up running just as fast as raw loops.
(The thing that has most impressed me about rust was the crossbeam crate + type system + derive stuff, which let me parallelize board search in an incredibly easy fashion. I found it much nicer to work w/ than Go channels, which is supposedly one of Go's big tricks!)
I have no idea what you’re talking about. Rust is not faster than c or anything else necessarily that is not garbage collected. It is almost as fast, and it has memory safety and as is made apparent in this thread many advantages in terms of usability and undefined behavior. Being the quickest i s not something rust is known for. So you wouldn’t be saving milliseconds. And calling an api is pretty simple so the security aspect wouldn’t be of much use. So you are wrong and the guy you are responding to is right.
You would have to use unsafe and you'd be better off with C++. Also people who care about latency do not bother with the OS network stack. It's not a simple matter of picking language and magically going faster.
It's rare that I've run into such issues when using Python for building trading algorithms - most of the overhead (for me) seems to be building a strategy using the data that you have available. Numerical computing libraries such as NumPy, Pandas, and so on in the Python ecosystem make this much easier to do in Python.
I feel like static typing would get in the way there, making code more verbose and more difficult to prototype while running possibly thousands of backtests (e.g, to tune hyperparameters of your model) but if that your preference, by all means, Rust should be usable for that.
I think in the specific case of casting a float to an int, more instructions will be added, but it doesn't have to be a branch. Here it looks like rustc emits a conditional move: https://godbolt.org/z/1cfqof
> Just like with array access, the compiler can often optimize the checks away, making the safe and unsafe versions equivalent when the compiler can prove it.
Can it "often" solve the halting problem as well?
The hope that this kind of optimization will happen sounds a bit fanciful for any non-trivial part of a program.
You would be surprised, at least with array access stuff. And, if it doesn't, you can often help it understand with a bit of work. Sometimes an assert before a loop or re-slicing something can take a check in the body of a loop and move it out to a single one.
I ported a small C function to Rust recently that involved some looping, and all of the bounds checking was completely eliminated, even once I took the line-by-line port and turned it into a slightly higher level one with slices and iterators instead of pointer + length.
Rust 1.45 will be the Rocket Release. It unblocks Rocket running on stable as tracked here https://github.com/SergioBenitez/Rocket/issues/19
This is so excellent, and I love seeing long term, multiyear goals get completed. It isn't just this release, but all the releases in between. The Rust team and community is amazing.