This is the 10th entry of my learning Rust journal...
It captures some of the learning points while going through chapter 15 of the Rust Book. You can read other posts in this series by following the label learning rust.
I enjoyed reading this chapter. I found it particularly interesting because it was about concepts I usually do not need to think about when working with the other programming languages I have used before now. Apart from that, it also allowed me to invalidate some wrong assumptions I had picked up along the way and also consolidates some of the concepts I have been learning.
One of the assumptions I had, which I found out was wrong while going through this chapter has to do with Stack vs Heap. For some strange reason, I had thought that Structs and Enums are always on the Heap. I suspect my familiarity with Java is to blame for this wrong assumption, since if you squint hard enough a struct looks like an Object in Java and usage of new keyword always means allocating memory on the heap. But this is not the case in Rust. A struct or an enum does not automatically mean heap memory allocation.
Sunday, March 01, 2020
Saturday, February 29, 2020
Rust Ownership Rules
If you have been following this blog, then it would have been obvious that at the beginning of this year, I started learning Rust. This blogpost is a breakaway from the journal style of capturing the main points I encountered while reading the Rust book. It instead captures my understanding thus far of Rust ownership rules.
One of Rust's main differentiator is that it provides memory safety. This it does by providing compile-time guarantees that flag code that potentially could lead to memory bugs as a compile-time error. The compile-time guarantees enforce what is normally referred to as ownership rules. In this post, I took the opportunity to re-summarise what I consider to be the essence of this ownership rules in Rust. The key points can be outlined as follows:
One of Rust's main differentiator is that it provides memory safety. This it does by providing compile-time guarantees that flag code that potentially could lead to memory bugs as a compile-time error. The compile-time guarantees enforce what is normally referred to as ownership rules. In this post, I took the opportunity to re-summarise what I consider to be the essence of this ownership rules in Rust. The key points can be outlined as follows:
- Values are owned by variables.
- When the owning variables go out of scope, the memory the value is occupying will be deallocated.
- The values can be used by other variables, they just need to adhere to certain rules that are enforced by the compiler.
The ways that other variables make use of value can be grouped into 4 categories and these ways of usage will dictate the rules to be adhered to:
- Clone: Here the value is copied to the other variable. The other variable gets its own ownership of the copied value, while the original variable keeps the ownership of its value.
- Move. Here the ownership is handed over to the other variable that wants to make use of the value. The original variable no longer has ownership.
- Immutable Borrow. Here no ownership transfer occurs but the value can be accessed for reading by another variable. The memory is not de-allocated if the borrowing variable goes out of scope, since the borrowing variable does not have ownership.
- Mutable Borrow. Here the value can be accessed for both reading and writing by the other variable. The memory is also not de-allocated if this borrowing variable goes out of scope since the borrowing variable does not have ownership.
Tuesday, February 04, 2020
Learning Rust - Day 9 - Closures and Iterators
This is the 9th entry of my learning Rust journal...
It captures some of the learning points while going through chapter 13 of the Rust Book. You can read other posts in this series by following the label learning rust.
This chapter did not present any new or mind bending concepts. Most modern languages nowadays have the concept of functions as first class citizens, closures and iterators. So the chapter was about taking note of how these concepts are encoded in Rust.
I did not really enjoy how this particular chapter was written, especially section 13.01. I think the pedagogy can be improved. The section spends way too much time in motivating an example, that in my opinion, clouds the essence of what is being explained. It so happened that I found another book on Rust Introduction to Rust which ended up being really well written: concise and well explained. I personally enjoyed the chapter on closure from this book than I did reading the Rust Book itself.
So to the content of this chapter, here are some of the things that stood out:
It captures some of the learning points while going through chapter 13 of the Rust Book. You can read other posts in this series by following the label learning rust.
This chapter did not present any new or mind bending concepts. Most modern languages nowadays have the concept of functions as first class citizens, closures and iterators. So the chapter was about taking note of how these concepts are encoded in Rust.
I did not really enjoy how this particular chapter was written, especially section 13.01. I think the pedagogy can be improved. The section spends way too much time in motivating an example, that in my opinion, clouds the essence of what is being explained. It so happened that I found another book on Rust Introduction to Rust which ended up being really well written: concise and well explained. I personally enjoyed the chapter on closure from this book than I did reading the Rust Book itself.
So to the content of this chapter, here are some of the things that stood out:
Sunday, January 26, 2020
Learning Rust - Day 8 - An I/O Project: Building a Command Line Program
This is the 8th journal entry of my learning Rust journey. It captures some of the learning points while going through chapter 12 of the Rust Book. You can read other posts in this series by following the label learning rust.
Not much new concepts was introduced in this chapter. The aim was to apply the material presented in the book up till that point in building a trivial command line tool.
While working through the chapter though, I observed a couple of different strategies for dealing with errors via the Result<T,E> type.
Turn error to boolean via is_err
This seems handy when you want to convert the result to boolean based on whether the result was a success of not:
Get success or run some logic via unwrap_or_else
This allows getting the success value, and in case of error, allows passing a callback function to process the error.
Ignore success and only run some code on failure via if let syntax
This seems to be useful in cases where you only want to do something in case calling a function returns an error.
These are by far not all the available error handling strategies when dealing with Result in Rust. These were only the ones that I picked on while reading through chapter 12.
Another thing worth nothing, which is more or less like a culture shock, is the practice of putting unit tests in the same file as the code being testes. All languages I have used before now had the practice of having the tests external to the code being tested; but it seems in idiomatic Rusts, the tests go together with the implementation. This would require some getting used to!
That was it for this chapter. I now look forward to exploring the Functional Language Features in Rust as I proceed to chapter 13..
Not much new concepts was introduced in this chapter. The aim was to apply the material presented in the book up till that point in building a trivial command line tool.
While working through the chapter though, I observed a couple of different strategies for dealing with errors via the Result<T,E> type.
Turn error to boolean via is_err
This seems handy when you want to convert the result to boolean based on whether the result was a success of not:
let x: Result<i32, &str> = Ok(-3); assert_eq!(x.is_err(), false); let x: Result<i32, &str> = Err("Some error message"); assert_eq!(x.is_err(), true);
Get success or run some logic via unwrap_or_else
This allows getting the success value, and in case of error, allows passing a callback function to process the error.
fn count(x: &str) -> usize { x.len() } assert_eq!(Ok(2).unwrap_or_else(count), 2); assert_eq!(Err("foo").unwrap_or_else(count), 3);
Ignore success and only run some code on failure via if let syntax
This seems to be useful in cases where you only want to do something in case calling a function returns an error.
if let Err(e) = run(config) { eprintln!("Application error: {}", e); process::exit(-1) }
These are by far not all the available error handling strategies when dealing with Result in Rust. These were only the ones that I picked on while reading through chapter 12.
Another thing worth nothing, which is more or less like a culture shock, is the practice of putting unit tests in the same file as the code being testes. All languages I have used before now had the practice of having the tests external to the code being tested; but it seems in idiomatic Rusts, the tests go together with the implementation. This would require some getting used to!
That was it for this chapter. I now look forward to exploring the Functional Language Features in Rust as I proceed to chapter 13..
Friday, January 24, 2020
Learning Rust - Day 7 - Writing Automated Tests
This is the 7th journal entry of my learning Rust journey. You can read other posts in this series by following the label learning rust.
In this session, I read through Chapter 11 of The Rust Book. It was about writing automated tests in Rust. This chapter was a breeze; for obvious reasons. In fact, the most interesting things I learnt was not even about testing in Rust, but about another feature in the language: Attributes.
I have always noticed things like #[derive(Debug)], #![allow(unused_variables)], etc being used in the language, but I never stopped to actually read up on what they are. I know they were a facility of providing some form of metadata in the language; sort of like @annotations in Java, I just never took the time to find out what they are official called in Rust.
It ended up being that the testing mechanism in Rust revolves around the use of this language feature: notably #[cfg(test)] and #[test], so I took the opportunity to find out what exactly these things were.
They are Attributes and they are:
Attributes can also be classified into the following kinds: Built-in attributes, Macro attributes, Derive macro helper attributes, and Tool attributes. So far, so good, I find the Built-in attributes to be the most interesting ones, because, based on my knowledge of Rust, they are the ones I have mostly encountered. A list of these Built-in attributes can be found here.
Apart from learning more about Attributes, there were a couple of things I picked up about testing in Rust that are worth noting:
In this session, I read through Chapter 11 of The Rust Book. It was about writing automated tests in Rust. This chapter was a breeze; for obvious reasons. In fact, the most interesting things I learnt was not even about testing in Rust, but about another feature in the language: Attributes.
I have always noticed things like #[derive(Debug)], #![allow(unused_variables)], etc being used in the language, but I never stopped to actually read up on what they are. I know they were a facility of providing some form of metadata in the language; sort of like @annotations in Java, I just never took the time to find out what they are official called in Rust.
It ended up being that the testing mechanism in Rust revolves around the use of this language feature: notably #[cfg(test)] and #[test], so I took the opportunity to find out what exactly these things were.
They are Attributes and they are:
...a general, free-form metadatum that is interpreted according to name, convention, language, and compiler version...They can exist in two forms. Outer attributes: the ones that starts with #! and Inner attributes: the ones that starts with only #. The outer attribute is placed outside something - i.e. before a struct definition, function definition, module definition etc - it applies to the thing that follows the attribute. The inner attribute is placed inside something. e.g when placed in the (root of a) crate in other to apply an attribute to the crate - it applies to the item that the attribute is declared within.
Attributes can also be classified into the following kinds: Built-in attributes, Macro attributes, Derive macro helper attributes, and Tool attributes. So far, so good, I find the Built-in attributes to be the most interesting ones, because, based on my knowledge of Rust, they are the ones I have mostly encountered. A list of these Built-in attributes can be found here.
Apart from learning more about Attributes, there were a couple of things I picked up about testing in Rust that are worth noting:
- Use #[cfg(test)] attribute on module that contain test functions. Use #[test] attribute on test functions within the test module.
- assert!, assert_eq! and assert_ne! are macros that can be used for asserting test conditions.
- favour assert_eq! and assert_ne! over assert! because they provide more useful messages in case of test failures and allow specifying of custom error messages on test failures.
- cargo test --help displays options that can be used with cargo test while
cargo test -- --help displays the options you can use after the separator --. It looks like that latter can only be ran in the root of a rust project. - You can’t use the #[should_panic] annotation on tests that use Result<T, E>. An Err value would need to be returned to signify an expectation of error.
- Run the tests with cargo test -- --test-threads=1 to prevent concurrent execution of tests
- Use #[ignore] attribute to ignoring some tests unless specifically requested.
- To specifically run a single test, pass the name of the test function to cargo test. That is
cargo test name_of_test_function - Use cargo test -- --nocapture to also see the outputs (if any) from succeeding tests. By default outputs are shown only for failed tests
- If there is setup code to be shared across different tests, then make sure to put them inside of tests/common/mod.rs instead of tests/common.rs. Not doing this would make the setup code appear in the test results.
- Unit tests are placed in the same file as the module, while integration tests go into tests/integration_test.rs and they do not need #[cfg(test)] attribute; only the #[test] attribute is needed.
That was it for learning about writing automated tests in Rusts. I would be taking on Chapter 12 next. Which is: An I/O Project: Building a Command Line Program. It Looks like a chapter that would help solidify some of the concepts presented in the book thus far. Looking forward!
Thursday, January 23, 2020
Learning Rust - Day 6 - Generic Types, Traits, and Lifetimes
This is the 6th journal entry of my learning Rust journey. You can read other posts in this series by following the label learning rust.
In this study session I went through chapter 10 of the Rust book, which covers Generic Types, Traits, and Lifetimes. Generic types and Traits were easy to digest, as they are concepts I am already familiar with from other languages. It was Lifetime that proved to be the difficult nut to crack. Just like when going over the section about Modules, I had to consult other sources outside the book in other to be able to wrap my head around the concepts. I would not say I have it 100% locked down, but I think I now know enough of the general gist to proceed with the rest of the book.
In this study session I went through chapter 10 of the Rust book, which covers Generic Types, Traits, and Lifetimes. Generic types and Traits were easy to digest, as they are concepts I am already familiar with from other languages. It was Lifetime that proved to be the difficult nut to crack. Just like when going over the section about Modules, I had to consult other sources outside the book in other to be able to wrap my head around the concepts. I would not say I have it 100% locked down, but I think I now know enough of the general gist to proceed with the rest of the book.
Sunday, January 12, 2020
Learning Rust - Day 5 - Common Collections and Error Handling
This entry captures the highlights of reading through chapter 8 (Common Collections) and chapter 9 (Error Handling) of The Book . It marks my fourth entry in the Learning Rust journey. For more entries like this, see the posts with learning rust label.
I hang out in beginners section of Rust channel on Discord, and every now and then I see folks come around and say something like "I have been fighting with the borrow checker". I think that phrase came to life for me in this learning session. The last section of chapter 8 has 3 exercises, and while trying to work on those exercises, I found myself in situation where it feels like I am wrestling with the borrow checker! - I know, I know, the borrow checker is just trying to save me from my own stupidity - but right now it feel more like a struggle! 😂 And I guess these early encounters with the borrow checker signifies the beginning of my initiation rights towards becoming a Rusteacean?
Anyways, in these post, as I do, I pen down some of the things that stood out for me.
First thing was that + has a wired API when used for concatenating strings. It requires the second operand to be a borrow, while basically making the first string unusable after concatenation.
The + has to be the most natural API for concatenating strings in previous languages I have worked with. Not sure the explanation for this unique incarnation in Rust. It seems, in other to get the more familiar behaviour, the format! macros should be used, and that basically provides string interpolation functionality:
I also learnt that strings in Rust does not support indexing. Which means one cannot access an individual characters in a string by referencing it by index. That is, this would lead to compile time error:
This is due to the fact that Rust stores string as an array (vector) of bytes, plus it supports UTF-8 by default. This means that a character could actually be represented by more than 1 byte, and in such a case, indexing that only returns a byte would make no sense. As the example that was given in the book shows:
The above might look like being composed of 12 characters, and hence be represented by Rust with a vector with 12 slots. In reality this is not the case. The above is backed by a 24 long vector. Hence providing a facility for allowing indexing into a string to return a character does not makes sense.
What Rust provides, instead is ability to retrieve slices from a String. For example:
And also the ability to turn a String into a unicode scalar values, or bytes, which can then be iterated over.
Also learnt Rust has a thing called deref coercion although this was not covered in this chapters. It would be in chapter 15.
While reading through the section on Hash Map, the or_insert caught my attention. The form it appeared in the book is similar to the following:
I do not know enough Rust to know what is idiomatic or not, but the I find the UX around the API a little odd. The fact that an operation that inserts a value to a key, when that key is absent, also returns an handle to the value which can be used to mutate the value, feels overloaded. Also, one can assume if map.entry("1").or_insert(0) returns the value which can be later mutated, then map.entry("1") should also do the same, with the step of trying to create it if it is absent removed. But no, the API does not have that level of symmetry.
Also, I know mutation in Rust is safe because of the borrow checker, I am still not sure if that is a license to use mutation over the place. It might be easier for the compiler to ensure that mutation does not lead to bugs, not sure if that level of mutation would also ease a human in understanding the logic in the code. I personally still have preference for the Functional programming approach of treating everything as immutable values, and deal with effects instead of mutation and side effects, as I think it also add to the simplicity and elegance of the code. Fingers crossed, and mind always open, as I learn more and use Rust, I get to understand its idiomatic use and philosophy.
While going through the chapters for this study session, I had to revisit some of the information provided in Chapter 4 regarding ownership, slice types etc. I realised there are some cogent points I did not include in my Learning Rust - Day 2 - Getting Acquainted with Ownership post, about ownership/borrowing etc, so I am including them here. It is just some laws that governs how ownership et al. works. The items in bold are a verbatim copy from the Rust book.
Turning to the Error handling chapter, I learnt about the panic = 'abort' that can be added to the Cargo.toml file. It has the effect of switching off the memory clean up Rust does when panic occurs. The memory would then have to be freed up by the operating system. Doing this has the added effect of reducing the size of the generated Rust binary. And by the way, it seems panic, is Rust lingo for exception.
I also learnt about the RUST_BACKTRACE environment variable that can be used to list the full path and actual portion of the user code that triggered the panic instead. You can see this as just having proper stack trace. interesting enough, this week I learnt this would no longer be needed, as recent changes to the Rust compiler now makes the panic messages to now pointing to the location where they were called, rather than core's internals
Up until now, I never encountered the return keyword. But I now know that Rust has a return keyword, and that it is mostly use for explicit signifying of a return out of a function, especially where there could be multiple return path. Its usage is closely related to the elvis operator: ? which seems to embody the spirit of the do notation in languages like Haskell and PureScript. So far, I have only seen how to use it with Result type, but not the Option type.
As I mentioned in the beginning I had to fight with the borrow checker while implementing the exercises listed at the end of chapter 8. But what I found astonishing was amount of these errors were lifetime related!
Here is a list of some of the errors I remembered to capture:
I hang out in beginners section of Rust channel on Discord, and every now and then I see folks come around and say something like "I have been fighting with the borrow checker". I think that phrase came to life for me in this learning session. The last section of chapter 8 has 3 exercises, and while trying to work on those exercises, I found myself in situation where it feels like I am wrestling with the borrow checker! - I know, I know, the borrow checker is just trying to save me from my own stupidity - but right now it feel more like a struggle! 😂 And I guess these early encounters with the borrow checker signifies the beginning of my initiation rights towards becoming a Rusteacean?
Anyways, in these post, as I do, I pen down some of the things that stood out for me.
First thing was that + has a wired API when used for concatenating strings. It requires the second operand to be a borrow, while basically making the first string unusable after concatenation.
let hello = String::from("Hello"); let world = String::from(" world"); let hello_world = hello + &world; // world has to be a borrow
println!("{}", hello_world); println!("{}", hello); // hello can't be used anymore due to move
The + has to be the most natural API for concatenating strings in previous languages I have worked with. Not sure the explanation for this unique incarnation in Rust. It seems, in other to get the more familiar behaviour, the format! macros should be used, and that basically provides string interpolation functionality:
let hello = String::from("Hello"); let world = String::from(" world"); let hello_world = format!("{}-{}",hello, world); println!("{}", hello_world); println!("{}", hello); // still usableprintln!("{}", world); // still usable
I also learnt that strings in Rust does not support indexing. Which means one cannot access an individual characters in a string by referencing it by index. That is, this would lead to compile time error:
let hello = String::from("Hello world"); println!("{}", hello[0])
This is due to the fact that Rust stores string as an array (vector) of bytes, plus it supports UTF-8 by default. This means that a character could actually be represented by more than 1 byte, and in such a case, indexing that only returns a byte would make no sense. As the example that was given in the book shows:
print!("{}", String::from("Здравствуйте").len());
The above might look like being composed of 12 characters, and hence be represented by Rust with a vector with 12 slots. In reality this is not the case. The above is backed by a 24 long vector. Hence providing a facility for allowing indexing into a string to return a character does not makes sense.
What Rust provides, instead is ability to retrieve slices from a String. For example:
println!("{}", &hello[0..1])
And also the ability to turn a String into a unicode scalar values, or bytes, which can then be iterated over.
Also learnt Rust has a thing called deref coercion although this was not covered in this chapters. It would be in chapter 15.
While reading through the section on Hash Map, the or_insert caught my attention. The form it appeared in the book is similar to the following:
let mut map: HashMap<&str, i32> = HashMap::new(); let r = map.entry("1").or_insert(0); *r += 10; *r += 10; println!("{}", map.get("1").unwrap()) // prints 10
I do not know enough Rust to know what is idiomatic or not, but the I find the UX around the API a little odd. The fact that an operation that inserts a value to a key, when that key is absent, also returns an handle to the value which can be used to mutate the value, feels overloaded. Also, one can assume if map.entry("1").or_insert(0) returns the value which can be later mutated, then map.entry("1") should also do the same, with the step of trying to create it if it is absent removed. But no, the API does not have that level of symmetry.
Also, I know mutation in Rust is safe because of the borrow checker, I am still not sure if that is a license to use mutation over the place. It might be easier for the compiler to ensure that mutation does not lead to bugs, not sure if that level of mutation would also ease a human in understanding the logic in the code. I personally still have preference for the Functional programming approach of treating everything as immutable values, and deal with effects instead of mutation and side effects, as I think it also add to the simplicity and elegance of the code. Fingers crossed, and mind always open, as I learn more and use Rust, I get to understand its idiomatic use and philosophy.
While going through the chapters for this study session, I had to revisit some of the information provided in Chapter 4 regarding ownership, slice types etc. I realised there are some cogent points I did not include in my Learning Rust - Day 2 - Getting Acquainted with Ownership post, about ownership/borrowing etc, so I am including them here. It is just some laws that governs how ownership et al. works. The items in bold are a verbatim copy from the Rust book.
- Each value in Rust has a variable that’s called its owner.
- This variable can either be a stack variable, that is, it points to data that resides in the stack or a heap variable, that is, it points to data on the heap.
- When it points to the heap, then you can see the variable as containing the pointer, length, capacity etc: basically things you can see as ownership related data
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
- When passing data on the heap to functions, instead of making a copy of the data, to pass the function, a pointer to the location of the data is passed instead. This is a reference. This act is called borrowing.
- At any given time, you can have either one mutable reference or any number of immutable references.
Turning to the Error handling chapter, I learnt about the panic = 'abort' that can be added to the Cargo.toml file. It has the effect of switching off the memory clean up Rust does when panic occurs. The memory would then have to be freed up by the operating system. Doing this has the added effect of reducing the size of the generated Rust binary. And by the way, it seems panic, is Rust lingo for exception.
I also learnt about the RUST_BACKTRACE environment variable that can be used to list the full path and actual portion of the user code that triggered the panic instead. You can see this as just having proper stack trace. interesting enough, this week I learnt this would no longer be needed, as recent changes to the Rust compiler now makes the panic messages to now pointing to the location where they were called, rather than core's internals
Up until now, I never encountered the return keyword. But I now know that Rust has a return keyword, and that it is mostly use for explicit signifying of a return out of a function, especially where there could be multiple return path. Its usage is closely related to the elvis operator: ? which seems to embody the spirit of the do notation in languages like Haskell and PureScript. So far, I have only seen how to use it with Result
As I mentioned in the beginning I had to fight with the borrow checker while implementing the exercises listed at the end of chapter 8. But what I found astonishing was amount of these errors were lifetime related!
Here is a list of some of the errors I remembered to capture:
- these two types are declared with different lifetimes...
- ^^^ borrowed value does not live long enough
- ^ expected lifetime parameter
- help: consider giving it a 'static lifetime: `&'static`
Subscribe to:
Posts (Atom)