Rust by Trial and Error
At this year’s Rocky Mountain Ruby, I mentioned to Steve Klabnik that I was trying (slowly) to learn Rust. He encouraged me to blog about anything I managed to hack together, as the Rust community deserves the kind of enthusiastic blogging that so greatly helped the Ruby community. At the same conference a couple of people mentioned that they really liked my deep dives into http://exercism.io problems.
So, let’s give the people what they want by blogging about doing Exercism in Rust!
I have no Rust experience beyond reading the first bit of of the Rust book, so expect the code in here to be terrible! The advantage of my awfulness, however, is that I’m going to move real slow. Trying to move fast just leads to inexplicable compiler errors.
The first Rust Exercism problem to tackle is the leap year exercise. I’ve done this in Ruby and the solution is pretty straight forward. But in Rust I had no idea where to even begin. Thanks to the provided test suite I knew that I had to implement code like:
Actually implementing that? Not sure! I knew that Exercism provides example solutions in their repos, so I started by stealing theirs.
What is even going on here? I worked it out line by line:
The test suite loads up our library (which is namespaced leap
) and expects it to provide a function named is_leap_year
. We make that work with the pub fn is_leap_year
bit of this line. fn is_leap_year
creates a function with the name is_leap_year
and pub
makes it public so that it can be used elsewhere.
This states that our function takes one parameter, year
and that it must be a i32
, which is an integer. Do you need a signed 32-bit integer to represent a year? No. But that’s what the example code had, so let’s go with it.
Finally, this part says that our function will return a boolean.
If, like me, you’ve spent most of your time with Ruby, then the type-checking in code like (year: i32) -> bool
may seem awkward. And it can be, especially when you’re trying to learn. But it definitely has advantages, otherwise there wouldn’t be a bunch of type-checked languages floating around. I don’t know that we’ll see those advantages in this post, but I’ll get to them eventually.
This line was easier for me to understand once I translated it to Ruby
This code creates a lambda and gives it a name. The lambda returns true if year.modulo(n)
is equal to 0, and false if it doesn’t. The let
is how Rust handles variable bindings.
With all those pieces in place,
is pretty straight forward. If your year is evenly divisible by 4 and it’s either not evenly divisible by 100 or is evenly divisible by 400, then it’s a leap year. This expression will always return a boolean, so we’re satisfying the “return a boolean” part of our function declaration. Note that this line does not end with a semi-colon. Why? I’m not sure! It has to do with Rust being expression oriented and I don’t really get that yet.
Ok! Our code works, our tests pass and I kind of understand what it’s doing. Time to move on to the next exercise? No! Time to refactor! I don’t know if our code is idiomatic Rust or not, but I certainly think it could be easier to understand. But since I don’t know Rust, I don’t actually know how to put my opinions into action…
I know that the first thing I want to do is to take that has_factor
lambda and move it out of my is_leap_year
function. It’s currently trapped in there, of no use to anyone else.
Well, I know how to create a function, so…
And that works. Miracles. I can extend this approach easily:
The hidden lambda is gone, but I now have 3 methods that know how to do the same thing, so let’s dry that up by extracting their common knowledge into a new function.
This may or may not be an improvement. I like that my new functions have descriptive names. I also like that, unlike the lambda, they are accessible outside of the is_leap_year
function. But, being a Rubyist, I wonder where my classes are. The problem I’m trying to solve is about Years, but there’s no clear “Year” concept in my code.
An early section of the Rust book describes how to extract and define concepts like Year by using Structs.
In Ruby this would look like:
In the lines
we create a Year struct, which the Rust docs describe as ‘a way of creating complex data types’. But our Year is not complex, it only has one value, ordinal
, which is still a 32-bit integer. Though Year may not be complex, using a struct lets me give me it a name, which I find to be more important. Thanks to struct, there’s finally a ‘Year’ in all this code that’s purportedly about Years.
In Ruby version of Year, I defined both behavior and attributes within the class
block. But not in Rust. The struct
block defined the attributes. This code:
defines behavior. We added the new
method, which requires the now-familiar i32
and returns a Year
. Inside the method
this code creates a new Year struct and sets its ordinal
value to the ordinal we’ve provided. We can execute the code like this:
Which would print 1992. Very exciting!
There’s no requirement for us to write this new
behavior. It’s purely for convenience. If I removed the impl
block from my code, I could still write:
And it would work exactly the same. In the case of our simple Year, that code is tolerable. But using new
is a big boon to readability and comprehensibility, so let’s stick with it.
Our Year code doesn’t do much. But we can use it and our Exercism tests still pass:
I think it would be nice if my Year could tell me if it is a leap year, instead of me having to pass the year’s data off to some random functions. Reporting whether or not it’s a leap year is new Year behavior. We implement it the same way as new
.
The function body looks familiar, but we have a new parameter: &self
. If I wrote a method like this in Ruby, I could do:
In Ruby a instance method automatically has access to self
. Rust methods don’t. I’m fuzzy on this point, but here’s what I think is going on. In the Ruby code year.is_leap?
, year
is the receiver of the is_leap?
method call. Inside the method the receiver can be accessed by using self
(or leaving self
off entirely, in most cases).
But because of Rust’s focus on proper memory management, we have to tell the method how it’s going to use the receiver so that it can properly handle the memory. Is it a reference, a value or a mutable reference. I’ll be honest. I don’t quite understand what that means. And I might be totally wrong about all of it! But &self
is the most common way of providing self, so that’s what I’m doing.
Now that Year has this behavior, we can change our is_leap_year
function to use it.
Our Year
design is pretty good! But our code still has these is_divisible_by
methods hanging out at the top level. We interact with all of these methods exactly the same way, by passing an ordinal
. We should extract an Ordinal and hang these methods off of it.
This all looks pretty familiar. The one new bit of syntax is in our is_divisible_by
method, which accepts two parameters: &self
and a factor
.
Now we can link our Year and Ordinal together by changing Year’s definition and methods:
We tell the Year struct that its ordinal is now an Ordinal. Then we change the implementation to follow suit. We’ve fully extracted our Year and Ordinal, and our code now looks like this:
In terms of Rust idioms and design, this code is probably pretty far from exemplary. But the process of writing it helped me learn a lot about Rust basics. Hopefully it helped you as well!
Now that my presentation for Rocky Mountain Ruby is done I can again focus my writing time on this site and my newsletter. You can read previous newsletters, or sign up for free. Comments/feedback/&c. welcome on twitter, at ian@ianwhitney.com, or leave comments on the pull request for this post.