Profile Picture

dyslexicsteak (dysl)

Reputation: 4 [rate]

Joined: Feb, 2023

Last online:

Rust is the future and the future is now

Bio

Rust is the future and the future is now

Badges

badge

Etc

Send Message

Threads List
Possible Alts

Activity Feed

Created a new thread : Rust Best Practices 2: Error and Null handling


Error and null handling are two core concepts in any programming language; you can't be sure that everything will be there and working the way you want. In this post, we'll dive into how Rust™ handles them.

 

First, errors:

 

Languages like C tend to use error codes:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098765740145913876/carbon.png

 

Error codes are quite a bad way to handle errors; you can't attach any extra data but the status code itself, you are allowed to ignore the error, and you can't use the integers that the macros occupy as successful return values as the macros have occupied their meaning.

 

In my previous post, I briefly introduced the Result type in Rust; it is an enum which can be in one of the Ok state (success) or the Err state (failure). The enum is generic over two types, T and E, which attach data to the tuple variants Ok and Err, respectively. The type looks like this:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098723906556285019/result.png

 

Let's see how our example looks in Rust:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098727983952449739/rust_error_basic.png

 

Note 1: neither of these examples reflects the reality of HTTP and networking in C or Rust; they're just contrived examples for wanting a number to be within a range.

 

This example is quite basic, but we can see the night and day difference between Rust and C, Err let us attach data to describe the error better, and we used it with yet another enum to make valid only the states of being too large or too small, unlike the C version which could accept any integer.

 

Let's keep exploring error handling in Rust. We looked at returning the error from main and letting Rust handle displaying the error when it occurs. This approach is used commonly in I/O code, combined with something we will cover soon: the "?" operator. The second technique used an inherent method on the Result type: "expect". Expect has the same behaviour as another method called "unwrap", which returns the T "wrapped" in Ok(T) or panics with the Err(E). What "expect" does differently is that it accepts an error message.

 

Here, our T (as in Ok(T)) for "send_http_code" was (), the unit type; having unit as the Ok type means that succeeding yields no information other than the success itself. Let's explore an example where the success case has some data we want and how we can handle getting at those data using a real-life situation from a crate I wrote for the LUNIR project called "lifering".

 

Our problem is as follows:

The most widely accepted and used standard for floating-point numbers is IEEE 754. Within the standard, it is detailed that NaN (Not a Number) can never be considered equal to anything. NaN also has many different bit patterns, so a simple bitwise comparison would fail. As such, floating-point types do not implement the Eq trait in Rust and only implement the PartialEq trait. The Eq trait is a marker trait which asserts that comparisons:

 

1. reflexive: a == a must hold (FAILED)

2. symmetric: a == b must imply b == a 

3. transitive: a == b and b == c must imply a == c 

 

To use a type as a key type in a HashMap, it must implement the Hash trait, which requires the type to be Eq. The result is that you cannot use floats as the keys of a HashMap, which is annoying for people who need that. So, I created the "lifering" crate to remediate the issue. I store the float as its three components in a struct; they are the sign, mantissa, and exponent:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098735788369383574/flpt_components.png

 

I used the Float trait of the "num" crate to be generic over f64 and f32, as the type was large enough to cover both. The constructor is as follows: 

 

https://cdn.discordapp.com/attachments/764779149973782553/1098736529049915412/new.png

 

You might notice a few things; the first and most obvious is the "FloatWrap" type; this is purely due to a limitation of Rust. The second thing is the "NanError" type, "NanError" is a unit struct (a struct that behaves like () [zero-sized, inhabited by one value] but has a name) which I use because I can only fail on the one case of receiving a NaN. "NanError" is defined as follows: 

 

https://cdn.discordapp.com/attachments/764779149973782553/1098738026282237972/nan_error.png

 

Again we derive Debug for the type so it can be displayed. The TryFrom implementation goes as follows:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098738645780926544/trait_impl.png

 

You can see that "Self" takes on two meanings here:

 

1. The type that TryFrom is being implemented on.

2. This implementation of TryFrom in specific.

 

We can see those two meanings together in the return value of "try_from". The first "Self" is the type — "FloatingPointComponents", while the second "Self" is the implementation, and we get at the associated type "Error", which we specified to be "NanError". Inside the body, we check if the float is a NaN, and if it is, we return an Err(NanError); else, we decode the float into its integer components and store them in a "FloatingPointComponents" and return them in an Ok.

 

Great, now let's say we're consumers of this crate; what do we do? Lifering defines a simple macro called "lifering" due to the verbosity of manually creating a "FloatingPointComponents". Let's see how it handles the error:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098740398974849104/lifering.png

 

Aha, it unwraps inside, which is excellent for conciseness but bad for custom handling. Let's explore how we can customly handle errors from the "Lifering" crate.

 

First, let's look at the "?" operator. If the value succeeds, the operator will unwrap it, and if it's an error, the operator will return it. Let's see an example:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098746211785244772/question_mark.png

 

This is nice and concise. It functions like an unwrap, too, but what if we want to use our own error message? Pattern matching:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098751606624882769/patterns.png

 

Patterns can get arbitrarily complex and are valid in many more places. You can read further about this here: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html

 

We can use several mechanisms of pattern matching to achieve this same goal of panicking with a custom message, first, "if-let":

 

We can use "if-let" as a statement like this:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098753631202836522/if_let_statement.png

 

Or as an expression like this:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098754240517767278/if_let_expression.png

 

This is a bit more verbose than just using a match, so let's move on to our second technique, match:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098754661172924526/match.png

 

Relatively recently introduced to Rust are refutable patterns with "let .. else", our final technique:

 

https://cdn.discordapp.com/attachments/764779149973782553/1098755271481905282/let-else.png

 

Next, nulls:

 

Much of the same applies to how Rust handles no value with the Option type, so showing the signature is enough for you to understand how to use it. It also implements Try, and like Result, is in the standard prelude (it and its variants are in scope by default).

 

https://cdn.discordapp.com/attachments/764779149973782553/1098755668527288370/option.png

 

I think this post has become big enough already, so I will push iterators to Rust™ Best Practices 3.

 

---

 

Note: I've changed the image tool because ray.so is garbage.

Note: The ™ symbol is being used for now as the Rust Foundation is finally securing its trademarks. They have not been registered as of the writing of this post.

 

Don't fight or I'll lock. The next post will be about Iterators.

Commented to thread : My Programming language rating list


Rust isn't overrated, it can't be; you can never have enough safety.

Replied to thread : Is rust worth learning?


@allennova

 

Oh that's cool, I'll take a look at it every now and then, if it's satisfactory by the time that I finish up my giant pile of LUNIR work then I'll contribute, else I'll make my own. Thanks for telling me.

Replied to thread : Is rust worth learning?


@allennova

 

I'm gonna be writing a distributed compiler for Rust, but it's currently on hold as I have a lot of LUNIR work to do.

 

@yvneEnvy

 

100%, Rust is the future and the future is now. You should learn it because it will teach you 2 key things that apply everywhere:

  1.  All code is security critical code.
  2.  Best practices are not optional.

Personally., after learning Rust, I found myself having a hard time in other languages with unhelpful compilers and weak type systems, and it took some readjusting to be able to write code in all the langauges which I needed to write code in. Rust is an incredibly empowering langauge which will open many doors for you, as it runs both at the lowest and the highest of levels. It can run on bare metal and embedded systems and also on the browser at the frontend. The crate and tooling ecosystems are unmatched, and the community is extremely helpful. Despite all the talk of the learning curve, I think it's not that bad now with the presence of the new experimental interactive Rust book, the abundance of visuals serves to flatten the curve significantly comparing the mainly text-based format of the standard book (do keep in mind there are still things that need to be ironed out in this version of the book, and you should report any bugs or issues you encounter on their GitHub issues, or propose a fix via a pull request). 

 

That's just an overview, so let's get technical:

  • Rust uses OBRM, Ownership Based Resource Management, where any datum has a singular "owner", this datum is only removed "dropped" when the owner goes out of scope.
  • References have strict aliasing rules, only 1 reference that can modify the underlying data xor many references that can only read the underlying data can exist at one time, this solves a lot of problems.
  • Highly advanced (still not as advanced as it could potentially be) type system which allows the creation of ergonomic APIs and powerful static analysis of programs.
  • A mix of the familiar object-oriented principles with procedural and functional paradigms, resulting in concise and intuitive code which is open to growth and modification. 
  • "Fearless concurrency" via the Send and Sync traits, along with fine grained control over atomics and other synchronisation primitives.
  • Full and easy compatibility with existing C.
  • Powerful compile time features with declarative and procedural macros (those can access the internet too).
  • Expression-based syntax simplifies many tasks and reduces the cases where you need to bend over backwards to acheive certain dances of control flow.
  • A small and useful standard library with mostly well designed APIs and readable source that helps in learning and understanding how things work. 
  • Safe code and unsafe code is distinct, and certain operations are restricted to unsafe code only, which helps in identifying when problems might lie by eliminating all code outside of unsafe blocks and code that does not invoke unsafe blocks.
  • Strong async-await support which allows for automated concurrency of tasks.
  • Drop-in concurrency with the rayon crate which allows for easy parallelisation of iterative tasks.
  • Clean inline assembly, access to SIMD versions of primitives.
  • RAII as a rule.
  • No nulls and no exceptions.
  • Fine-grained control over panicking.
  • Easy and forced error handling, your code won't compile if you do nothing about an error.
  • Choice between static and dynamic polymorphism made clear.
  • Documentation is generated for all crates, and documenting is easy using doc comments which generates HTML on docs.rs.
  • Extremely high performance as a bonus.
  • And more which I'm forgetting.

Created a new thread : Rust Best Practices 1 — Case, Enums, and Argument Flexibility


Hi everyone. I want to talk about best practices in Rust.

 

Rust is an incredibly designed language with influences from across the board. These influences have resulted in a unique set of idioms that define what idiomatic Rust code (known as rusty code) looks like. For people coming from the object-oriented or imperative world, many of Rust's idioms are likely to be non-obvious or unnatural, and they will find themselves writing unidiomatic code that is closer to home: while this works, you will encounter resistance and pull requests from the Rust community if your library is written in this way because you are undermining Rust's benefits.

 

---

 

First, let's begin with what is quite a "turn-off" for people looking to use Rust:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089084526371815464/230abc963298c730.png

 

The compiler is pedantic about the case you use, enforcing a specific style. Many people see this as having their freedom stripped from them, but that is arguably a good thing; the crate (Rust term for a package) ecosystem having a sane, unified, and readable style makes the development of programs much easier as you always know what to expect, directing the compiler to allow this warning may be tempting to some. Still, I assure you of the benefits of just switching. You can dismiss the compiler's non_snake_case with an allow directive for FFI purposes, as not all languages have the same naming convention as Rust. 

 

I advise everyone to use rustfmt (accessible through "rustfmt" or "cargo fmt") on your code, you might not like some things, but it's in everyone's benefit that you do so. If there is something that it's proposing that is objectively bad such as verticalising an enum with many variants, which results in it spanning hundreds of lines, you can place the rustfmt::skip attribute on it.

 

---

 

Second, let's see how Rust can help us get our programs on rails:

 

Let's say we have a single function that lets us interact with our pet, which can either be a dog or a cat, and we can feed the pet or play with it. A naive implementation looks like the following:

 

First, let's write a struct and a constructor:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089084785705619466/6158043d9becb4ff.png

 

Next, let's write the interact function, which takes a String argument to specify what kind of interaction we want to do and a u8 of the amount:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089085142590570506/a1690a78025aca8e.png

 

I don't know if you see it, but this is awful, but let's act like we don't know any better. Someone who knows a little bit more than us about Rust would then suggest this refactor:

 

First, he changes the struct definition and uses larger, signed integers to reduce the chance of an overflow:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089085667415425095/376412f1237b3e6f.png

 

Next, he uses the Result type in Rust, which represents success or failure without the use of exceptions. Its definition looks like this:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076159566812827728/image.png

 

He then uses the guard clause pattern to extract out the conditions and remove nesting, and ends up with his code, let's look at his new function first:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089085847262986250/3fe61426833507fd.png

 

The story is similar in his interact function, where he needs to cast between integer types to calculate the result:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089086141703131166/1f28a8f5abd1940d.png

 

What a ride, that was bad. It was really bad. Any of these functions could fail for no good reason, some people suggest using a boolean value or an integer, but our scenario is an utterly non-obvious use of said types; there has to be a better way, and there is.

 

First, I sort the fields alphabetically, then revert them to u8. You'll see why that isn't an issue later. Note the type PetKind; no, it isn't an alias for bool. You'll see in a second:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076157489919311912/image.png

 

Aha, see, I created my enumerated types (tagged unions) for both pet and interaction kinds:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076157364723515412/image.png

 

As a result, we now have a beautiful one-liner infallible constructor that uses an implicit return:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076161706356654152/image.png

 

And an infallible interact method that uses saturating arithmetic to prevent overflowing (the values will max or min out at 0 or 255 and stay there until you add or subtract, respectively):

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076158456840597535/image.png

 

---

 

Finally, let's look at function arguments:

 

People need to have more flexible function arguments. Look at this example:

 

We're trying to display a death message when some skrub dies, so we sip mountain dew and write this:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076169444759437312/image.png

 

That obviously (or not so obviously) doesn't compile because string literals are &str (&'static str), and the function wants a String. Someone smart will say that it's better to accept &str because you can coerce &String into &str like so:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1089086604225822760/10b30b453a79c507.png

 

But now we can't take String itself, so what we can instead do is use the Into trait and accept anything that you can convert into a String, then call Into::into on it like so:

 

https://cdn.discordapp.com/attachments/1075783615671189534/1076172558795284611/image.png

 

This last example is not good Rust because you shouldn't be very generous in spreading Into around your code. The point is that if you want something specific, ask for it directly. For this example, requesting &str is the sweet spot: your game should not have many different string types floating around; accepting a reference is okay because we do not intend to modify, so why not accept &str to accept literals directly as well and to take advantage of the coercion and not an explicit call to Into::into. The generic also monomorphises, increasing code size as the number of types used with the function grows.

 

---

 

Don't fight, or I'll lock. The upcoming best practices post will be about iterators and error handling.

 

Replied to thread : Introducing: LUNIR


rust is the future and the future is now

Replied to thread : Kotlin Coding Section


Rust is the future and the future is now, Kotlin is copium and copium is now (Kotlin, Swift, Zig, Carbon, Elixir, etc). Vouch.

Replied to thread : rust is sucks dont start lol


@Rexi he didn't even start it, I did, he was just the vessel by which it reached you.

Replied to thread : rust is sucks dont start lol


@Rexi I've not spammed you have I

Replied to thread : rust is sucks dont start lol


@Rexi @Shintayo is that all you guys have to say about him? If so, you're being incredibly petty. Not to mention the huge exaggeration about "spamming" and "forcing"; it's the internet, and you can't force anyone to do anything, we're just spreading the word.

Replied to thread : rust is sucks dont start lol


@Rexi

 

Rust doesn't need variadics. If you want a homogenous unspecified number of items you get a slice, in this way, the allocation of the stack is predictable and indirection is explicit. If you want a heterogenous unspecified number of items you make a macro which is able to understand that and generate code that can turn it back into the Rust way.

 

Also, sorry, I'm not familiar, what's multiplied inheritance?

Replied to thread : rust is sucks dont start lol


@Sina I'm sorry for that on ARSON's behalf, he's screwing around but that was inappropriate.

 

@Rexi

 

I decided to start using Rust of my own accord 2 years ago, and I liked it so much that I felt the need to spread it for the good of the human race.

 

The users of C++ are also fighting quite hard to get very rusty features into C++23 such as the rust model of pattern matching and an increased expression-based-ness with "do". Rust is simply the superior language as of right now, and everyone knows it, we aren't trolling, we're trying to spread something truly wonderful.

Replied to thread : rust is sucks dont start lol


@Sina ok yea that's fair, a lot of people do attack Rust for no good reason and just make up things to say about it, or repeat made up things about it. 

Replied to thread : rust is sucks dont start lol


@Sina look at the title of this thread xd

Replied to thread : rust is sucks dont start lol


@Rexi Rust comes with first-party support for many thing and easy integration with third-party software that provides a user experience very similar to the first party one, you can see this on display most obviously with Cargo subcommands. The library space in Rust is also expanding rapidly, and for most C++ libraries with no pure Rust equivalent, there are bindings through the C API the C++ library exports.

 

Rust in IDEs is competitive with C++ as it has essentially first class CLion support in the case of fully-featured IDEs, a few extensions to VS Code do the trick as well, I used to use VS Code and I can testify that the experience is good, and CodeLLDB was quite helpful aside from its C orientedness (interpreting u8 as a C unsigned char which showed ASCII instead of the numbers which I wanted to see). Personally, I've left VS Code to use Helix, it's much more simplistic but I've found that it doesn't limit me in any meaninful way.

 

The fact that you bring up C interop as a benefit of C++ shows ignorance concerning Rust and the wider world in general; since all major operating systems communicate with a C ABI, all programming languages are forced to speak C as well, Rust being one of the best (repr(C), extern "C", core::ffi and by extension std::ffi). 

 

The performance gap between Rust and C++ is not wide enough to warrant mixing the 2 languages, all you'd achieve is a suppression of Rust's safety and performance benefits. 

 

Interop between Rust and C++ occurs through a C shaped hole because C++ has no proper stable ABI and advanced constructs in C++ do not translate well to other languages,