I heard about Rust’s unusual approach to memory management and thought have a look. Programming Rust from O’Reilly is chunky at about 700 pages and has taken a while to get through but does seem to cover all the bases. I’ve not gone as far as coding in this language so this will just be my opinion looking in from the outside.
Rust is a modern general purpose programming language. If your familiar with C++, C# or Java then you’re going to see a lot of familiar things here: data abstraction and encapsulation, generics, closers and libraries. However there’s a big difference: variable ownership and lifetimes.
Ownership and lifetime
Rust doesn’t have explicit deletion of pointer objects nor does it have garbage collection.
Instead the ownership of a variable is tracked throughout it’s lifetime.
This ensure that the variable destroyed or “dropped” when appropriate and
that nothing can access that variable when it is unsafe to do so.
The language enforces strict rules about access to each variable or it’s references.
Either a single mutable references can be “borrowed” or multiple constant references, but never both at once.
References to a variable can only be borrowed by something with a greater lifetime,
dangling pointers cannot occur.
Sometimes Rust can infer the lifetime of variables but
you may need to add explicit markers to ensure certain variables have matching lifetimes.
It looks a bit like adding template parameters to something.
Variables are allocated on the stack by default but, if you want to use the heap,
you can create a Box<T>
similar to
std::unique_ptr
.
Assignment is a move operation by default which transfers ownership.
This is all about making code safe by default. Concurrency is much easier because lots of unsafe options are just disallowed. Errors occur at build time and must be fixed to compile the code. I can see this is beneficial in safety critical systems where you have to get it right the first time. However it also make everything that might be an error a build time error including situations that will never occur in practice. It feels like you will always have to go the extra mile to make correct code even if you don’t want to. I think this could be a problem if your goals and how to achieve them is unclear or if things change along the way. I am normally for doing things correctly but this makes me wary. Balancing things needs both sides to be available to make choices. For all the good that it brings it also seems to remove choice.
Crates and modules
Crates and packages are Rust’s answer for sharing code between projects. These can contain both fully compiled code and generics that must be combined with other code before compilation. Modules are it’s answer to organising source files internally and separating private and public access. Unlike other languages I’ve seen it makes the simplifying assumption that everything within a module can access everything else within a module, even if it is otherwise private. This means that, say, any test code in the module has free access. If you want things to stay private then put it in another module.
Testing and documentation systems are built in.
Test functions just have to be marked up with an attribute, #[test]
, to be included in the unit-test.
Documentation is marked with a triple slash, ///
.
By default all code snippets within documentation will also be run as unit-tests to ensure correctness.
This does make things simple but it could mean a lack of alternative systems.
Matching and patterns
We’re all familiar with the switch-statement. Rust doesn’t have those, it has match-statements.
match point { Point { x: 0, y: 0 } => println!("At origin"), Point { x: 0, y } => println!("On x-axis, y = {}", y), Point { x, y: 0 } => println!("On y-axis, x = {}", x), Point { x, y } => println! ("x = {}, y = {}", x, y) }
A match-statement can do everything that a switch-statement can do and more. It uses “patterns” to check for specific combinations of types and values. Partial matches mean you can capture variables and then use those in the response code. It works with literals, ranges, variables, slices, structs and can include arbitrary guard conditions. I think this could be a great alternative to some complicated compound if-statements.
Pattern can be used elsewhere, accepting and potentially checking values.
let Track { name, artist_id, .. } = song; ... if let Some(artist) = artist_map.get(&artist_id) { ... }
In simple assignment you can pick out some fields from a struct and ignore the rest. In a function argument you can decompose a struct into it’s fields. To me decomposing a struct into individual values can be overused. It makes sense when you have an unnamed tuple as you are adding value. However a normal struct already has perfectly good names which can be referenced.
In an if-statement you can check the value is in the expected form.
This is far more interesting and fits well with how Rust reports errors.
Functions that can fail either return std::option
or
std::result
both of which have a value if successful and
the latter the reason behind any failure.
Generics and traits
There is the support for generic classes and functions you’d expect to see. There is also support for “traits” which are similar to C#’s interfaces. A single class can implement multiple traits allowing it to be used anywhere that trait is required. On the other side you can implement a function and require a specific set of traits be available on the input parameters.
Looping and iterators
Only while-loops and for-loops are available. The first combining neatly with patterns. The second made easy to use with direct support for numeric ranges and a range of collections and iterator adapters:
- map: Transforms into another type.
- filter: Selects for given property.
- flatten: Concatenates nested iterators.
- take, take_while: Use the first part of a list.
- skip, skip_while: Use the last part of a list.
- peekable: Gives any iterator a peek method.
- fuse: Gives any iterator a standard end-of-list behaviour.
- rev: Reverses a list.
- chain: Go from one iterator to another.
- enumerate: Give each item a position.
- zip: Combine two iterators.
- cycle: Repeat iterator endlessly.
- count, sum, product: Iterator maths.
- min, max, min_by, max_by: Iterator limits.
- any, all: Iterator logic.
- position: Value search.
- fold: Folds every element into an accumulator using an operation.
This is going to cover a lot of the simple iterations with collections without hand coding it. C++ has recently gotten a ranges library but it feels clunk in comparison.
Macros
Rust macros fit better into the language than C’s preprocessor macros but do something similar.
There’s a example in the book of building a json!
macro which allows properly formatted json
to be used inside Rust file.
These disappointed me.
Most of them seem so similar to functions that I wish they just were functions not an extra set of rules to learn.
They obviously let some more advanced initialisation go on but
that could probably be done with some simpler initialisation specific system.
Safe and unsafe
By default the Rust compile ensures you write safe code but you can use the
unsafe
to mark code blocks or functions.
This reveals that raw pointers are available and multiple systems can change a variable.
Instead of the language taking care of things it’s now the developer’s responsibility.
It’s not that unsafe
code is necessarily unsafe, it’s just that it can be.
In the end
Despite a largely presentation it’s not something I expect to use.
It comes down to something really simple.
I have my own opinions on how to name things and
this language does things differently.
Some of the basic keywords are abbreviated, e.g.
fn
,
impl
,
pub
,
dyn
.
While I can guess the meanings for these it’s a bad start.
I don’t want to have guess meanings for keywords, types or functions.
I’m aware that other languages use abreviations but some of these just seem so unnecessary.
On the other hand if you need safe code, are willing to deal with more build errors and don’t mind abbreviations then this probably has something for you.
Leave a Reply