Evans Levans11.1k33 gold badges3535 silver badges4444 bronze badges Rust is a very interesting language to compare to Haskell.
We'll look at things like syntax and building small projects. In this first part, we'll do a brief high level comparison between Haskell and Ru's, and examine some basic syntax examples.
Rust has a few key differences that make it better than Haskell for certain tasks and criteria. One of the big changes is that Rust gives more control over the allocation of memory in one's program.
The programmer does not control when items get allocated or reallocated. It will go through all the allocated objects, and reallocate ones which are no longer needed.
This simplifies our task of programming, since we don't have to worry about memory. It helps enable language features like laziness.
But it makes the performance of your program a lot less predictable. But the specific example I suggested was a self-driving car, a complex real-time system.
But the performance unknowns of Haskell make it a poor choice for such real-time systems. With more control over memory, a programmer can make more assertions over performance.
Besides this principle, Rust is also made to be more performant in general. A larger community correlates to certain advantages, like a broader ecosystem of packages.
Companies are more likely to use Rust than Haskell since it will be easier to recruit engineers. It's also a bit easier to bring engineers from non-functional backgrounds into Rust.
Both languages embrace strong type systems. They view the compiler as a key element in testing the correctness of our program.
Both languages also use immutability to make it easier to write correct programs. We can call a print statement without any mention of the IO monad.
We see braces used to delimit the function body, and a semicolon at the end of the statement. We'll see that type names are a bit more abbreviated than in other languages.
Note these can represent Unicode scalar values (i.e. beyond ASCII) We mentioned last time how memory matters more in Rust.
Other types with variable size must go into heap memory. Like “do- syntax in Haskell, we can declare variables using the let keyword.
This will become more clear as we start writing type signatures in the next section. We can change this behavior though by specifying the but (mutable) keyword.
We have type signatures and variable names within the parentheses. In this example, we no longer need any type signatures in main.
This means the actual return type is (), like the unit in Haskell. They inform our program what a function “is”, rather than giving a set of steps to follow.
But it's a little more common to mix in statements with your expressions in Rust. Block statements enclosed in braces are expressions.
One interesting effect of this is that arrays include their size in their type. You should now move on to part 2, where we'll start digging deeper into more complicated types.
Methods are similar to functions: they’re declared with the FN keyword and their name, they can have parameters and a return value, and they contain some code that is run when they’re called from somewhere else. However, methods are different from functions in that they’re defined within the context of a struct (or an ENIM or a trait object, which we cover in Chapters 6 and 17, respectively), and their first parameter is always self, which represents the instance of the struct the method is being called on.
To define the function within the context of Rectangle, we start an imply (implementation) block. Then we move the area function within the imply curly brackets and change the first (and in this case, only) parameter to be self in the signature and everywhere within the body.
We’ve chosen self here for the same reason we used rectangle in the function version: we don’t want to take ownership, and we just want to read the data in the struct, not write to it. Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.
That is, we want to be able to write the program shown in Listing 5-14, once we’ve defined the can_hold method. The method name will be can_hold, and it will take an immutable borrow of another Rectangle as a parameter.
We can tell what the type of the parameter will be by looking at the code that calls the method: rect1.can_hold(&rect2) passes in &rect2, which is an immutable borrow to rect2, an instance of Rectangle. These makes sense because we only need to read rect2 (rather than write, which would mean we’d need a mutable borrow), and we want main to retain ownership of rect2, so we can use it again after calling the can_hold method.
When we run this code with the main function in Listing 5-14, we’ll get our desired output. They’re still functions, not methods, because they don’t have an instance of the struct to work with.
For example, we could provide an associated function that would have one dimension parameter and use that as both width and height, thus making it easier to create a square Rectangle rather than having to specify the same value twice: There’s no reason to separate these methods into multiple imply blocks here, but this is valid syntax.
We’ll see a case in which multiple imply blocks are useful in Chapter 10, where we discuss generic types and traits. Structs let you create custom types that are meaningful for your domain.
But structs aren’t the only way you can create custom types: let’s turn to Rust ’s ENIM feature to add another tool to your toolbox.