
Learning Rust via Advent of Code - forrestthewoods
https://www.forrestthewoods.com/blog/learning-rust-via-advent-of-code/
======
dtolnay
_> I feel like there should be a helper for_ simple _parse operations._

 _> parse!("#{} @ {},{}: {}x{}", id, x, y, w, h);_

 _> This would be a clean inverse of println!._

The text_io crate does this exactly.

[https://crates.io/crates/text_io](https://crates.io/crates/text_io)

    
    
        #[macro_use]
        extern crate text_io;
    
        fn main() {
            let id: u32;
            let x: u32;
            let y: u32;
            let w: u32;
            let h: u32;
            scan!("#{} @ {},{}: {}x{}", id, x, y, w, h);
        }

~~~
nixpulvis
I wonder if we'll ever see `scan!` in the STD lib...

~~~
josephg
I hope not.

With a healthy crate ecosystem, std should be as small as possible (without
being too small).

It makes sense for std to contain:

\- Types that can't easily be put in a module (eg the `Fn` trait)

\- Types needed for cross-project interoperability (like `TcpStream` or
`Future`). Having these in std helps prevent ecosystem fragmentation.

\- Stuff that gets used in most non-trivial projects (eg `Box`, `Vec`,
`println!`)

Everything else belongs in a crate, where it won't bloat up the size of rust's
standard library for everyone in perpetuity. `scan!` looks great; but I expect
it'll be used in less than 1% of projects. There's no shame in keeping it in a
crate - thats what they're for!

------
Dowwie
If you decide to learn Rust using AoC 2018, I highly recommend comparing your
work with that of BurntSushi, who is known to write high quality code.

[https://github.com/BurntSushi/advent-of-
code](https://github.com/BurntSushi/advent-of-code)

You develop Rust skills by writing your own and then hone your skills by
learning from idiomatic Rust.

~~~
reificator
I learned Go first and then went to Rust, which was convenient because it
appears sushi went the same route.

~~~
Dowwie
I suspect his current route includes both Rust and Go. :)

~~~
reificator
Same with mine.

------
WakiMiko
For sorting sequential fields you can chain comparisons with .then():

    
    
        struct Date {
            year: u32,
            month: u32,
            day: u32,
        }
        
        let mut vec: Vec<Date> = Vec::new();
        
        vec.sort_by(|a,b| {
           a.year.cmp(&b.year)
            .then(a.month.cmp(&b.month))
            .then(a.day.cmp(&b.day))
        });
    

Alternatively you can derive PartialEq, PartialOrd, Eq and Ord for your
struct, which will produce a lexicographic ordering based on the top-to-bottom
declaration order of the struct's members:

    
    
        #[derive(PartialEq, PartialOrd, Eq, Ord)]
        struct Date {
            year: u32,
            month: u32,
            day: u32,
        }

~~~
masklinn
Or you can use sort_by_key and extract the relevant sorting key as a tuple (or
any other Ord structure) e.g.

    
    
        vec.sort_by_key(|d| (d.year, d.month, d.day))
    

sort_by is more flexible as it works fine with borrows, but when sorting on a
series of integer values or references sort_by_key is great.

> Alternatively you can derive PartialCmp for your struct, which will produce
> a lexicographic ordering based on the top-to-bottom declaration order of the
> struct's members:

Do you mean PartialOrd? partial_cmp is the method. And `sort` requires
absolute ordering (Ord) not just partial.

~~~
WakiMiko
I quite like this tuple approach.

And yeah, I meant Ord (and the required other Traits), I edited my post.
Thanks for pointing this out.

------
mcguire
" _Learning a new language via Advent of Code turned out to be a great idea. I
highly recommend it._ "

I'll second this: Using a set of progressive problems is an excellent way to
get used to actually writing code in a language.

" _My first stumbling block was parsing. Almost every AoC problem starts with
parsing lines of text from an input file._ "

Scheme. Guile. The PEG parsing module. (There's no kill like overkill.)

~~~
masklinn
> I'll second this: Using a set of progressive problems is an excellent way to
> get used to actually writing code in a language.

IME AoC isn't really progressive though, the problems ramp up and down pretty
dramatically.

> Scheme. Guile. The PEG parsing module. (There's no kill like overkill.)

Rust has several pretty good parsing libraries e.g. nom, lalrpop, pest, …

~~~
Twirrim
I've been using Kattis problems as a way to work on my Rust skills. The
problem there is you're limited to the standard library. Rust's ultra-minimal
stdlib makes a number of those kattis problems remarkably hard to implement,
where I've managed to whip up something in short order in Python using the
stdlib.

I keep thinking that Rust has gone a little too far on the "keep it minimal"
front. It also really disturbs me just how often with rust I'm having to rely
on third party hosted crates. That's one huge amount of trust going on there.

~~~
steveklabnik
Part of what we do to balance this out is to have the team make some of those
third party packages; the regex crate is a good example of that. It’s still
maintained by the team even if it’s not in the standard library itself.

~~~
TotempaaltJ
So then why isn't the regex lib part of the standard library?

~~~
richardwhiuk
The standard library is locked to Rust - you can't issue a new major version
of stdlib without having a major version of Rust.

Besides the impact of not being in stdlib is very low except for discovery.

~~~
Twirrim
> The standard library is locked to Rust - you can't issue a new major version
> of stdlib without having a major version of Rust.

How often do you see that actually being a problem? Rust releases new versions
on a regular cadence. Just how often do you imagine the regex crate actually
needs to be updated? Or how about the random number generator crate?

> Besides the impact of not being in stdlib is very low except for discovery.

I always find this an interesting argument. We've seen node.js etc. packages
be compromised many times, or even completely vanish (left-pad)? How much
confidence can I have that any individual package hasn't been compromised
somehow? How much confidence can I have that random dependencies aren't
suddenly going to enter my build chain. That left-pad situation was classic.
So many things got broken, not because they'd picked up left-pad, but because
dependencies of dependencies of dependencies relied on it (and so on down the
line...)

There was that situation just a month or so ago where a developer just didn't
want to maintain their package any more, had someone volunteer, who then
compromised it with a crypto-miner.

Now on top of that licensing gets to be a whole bunch of fun as soon as you
step outside the stdlib. For every crate you add, you need to do a licence
audit, and for each and every one of its dependents, and its dependents
dependents. Amazon, for example, has a black list of licenses. You can't use
any software licensed under one of them, for whatever reason the lawyers have
about each one.

~~~
shepmaster
> How often do you see that actually being a problem?

See my sibling comment about backwards compatibility

> Just how often do you imagine the regex crate actually needs to be updated?

You can see the frequency of updates to the regex crate if you are interested:
[https://crates.io/crates/regex/versions](https://crates.io/crates/regex/versions).

Sometimes a release in a few days, or a few a month.

> Or how about the random number generator crate?

Even more interesting, because `rand` hasn't even reached 1.0 yet!
[https://crates.io/crates/rand/versions](https://crates.io/crates/rand/versions)

Specifically, the authors are still deciding the right way to architect the
library for the myriad of uses that Rust has.

> or even completely vanish (left-pad)

In 99.99% of the cases, you cannot remove a crate from crates.io; you can only
prevent new projects from adding the crate as a dependency. The other 0.01% is
because of legal reasons, and there's not much to be done about that.

> For every crate you add, you need to do a licence audit

[https://github.com/onur/cargo-license](https://github.com/onur/cargo-license)
claims to show you the licenses of every dependency. It's required to have a
license to publish to crates.io.

------
masklinn
Some comments having flown through the article

> Helper Lambdas

> I like lambdas in C++11. I use them regularly for small helpers that exist
> solely within a function.

`move` lambdas might work better? The default tries to infer based on usage
but often fails. A `move` lambda lets you do something similar to C++'s
capture lists (though more verbose / cumbersome).

> BinaryHeap

> The standard library provides a max-heap. I regularly needed a min-heap. I
> got one by making a custom struct with custom compare function.

std::cmp::Reverse

> I wonder if there is a nice macro crate to help with this? I'd love to
> write: compare!(a, b, year, month, day, hour, minute);

sort_by_key?

> I wish there was an f16 half-precision float. There's a good internals
> thread on minifloats that makes me think f16 will happen eventually.

A problem's there is there are at least two "standard" f16 (IEEE and ML)

> There is NonZeroU32. It's similar-ish, but only works for zero. There isn't
> a Non255U8 or, thankfully, Non4294967296U32.

They're built from the unstable `NonZero` (and the unstable and unsafe
ZeroAble).

You could build a struct on top of NonZero which swaps zero and your sentinel
value.

~~~
floofy222
The #[rustc_layout_scalar_valid_range_start(...)] and
#[rustc_layout_scalar_valid_range_end(...)] attribute allows you to specify a
niche, so you can build a Non255U8 if you want. For example:

#[rustc_layout_scalar_valid_range_end(254)] struct Non255U8(u8);

------
erentz
Slightly OT: this seems like a good way to learn to program. My wife has been
trying to learn to program with python but all the tutorials even programming
101 tutorials these days actually seem more advanced than I would expect. For
example they assume someone gets what a string is and how it works with
surprisingly very little explanation, then overload the student by comparing
the difference between string objects in Python 2 vs Python 3 and Unicode va
non-Unicode! Woah object, Unicode, what?

Are there any good resources for progressively learning Python by doing
something like AOC but starting from nothing and as a tutorial and explaining
the basics of what a string is etc as it goes?

~~~
preordained
codingbat.com, the Python exercises. They have all the background information
needed in nearby links (e.g. I have an exercise with strings how do I use
those), and build experience nice and easy

------
general_pizza
I also thought regex seemed overkill and didn't feel like adding an extra
crate to my Cargo.toml. Luckily for the days I attempted I was able to get by
on just the split method for str's[0] which was nice and concise. So for your
regex example you could also do:

    
    
        // #1 @ 916,616: 21x29
        let parts: Vec<&str> = l.split(['@', ',', ':', 'x'].as_ref()).collect();
        let x = parts[1].trim().parse::<i32>().expect("x as i32");
        let y = parts[2].trim().parse::<i32>().expect("y as i32");
        let w = parts[3].trim().parse::<i32>().expect("width as i32");
        let h = parts[4].trim().parse::<i32>().expect("height as i32");
    

[0] [https://doc.rust-
lang.org/std/primitive.str.html#method.spli...](https://doc.rust-
lang.org/std/primitive.str.html#method.split)

~~~
terhechte
This is awesome. How did you figure this out? The documentation on `split`
says:

"The pattern can be a &str, char, or a closure that determines the split."

But in your case it is an array of chars and it splits for each of them. I
don't see this documented at all.

~~~
steveklabnik
Would you be willing to file a docs bug so we can clarify this?

It’s possible that it used to be only those types, but was expanded. We should
fix this!

~~~
terhechte
Yeah, will do

~~~
steveklabnik
Awesome, thank you!

------
gmfawcett
I did this year's AoC in Ada. (Well, part of it, I didn't do the whole
challenge.) I'd never written any serious Ada code before, and this was a
great way to get a better feel for it. Overall a positive experience! Ada can
be a bit verbose at times, but it wasn't nearly as bad as I had expected it to
be... Once you learn not to fight against the grain of the language, it's just
a matter of decomposing the problems in an Ada-ish way.

Suprisingly I found it very constructive to work on "two puzzles at once" \--
how to use the new language, and solving the AoC itself.

------
k3lsi3r
This is slightly off-topic, but someone mentioned that it would be a good idea
to check out BurntSushi's solutions after trying it yourself which seems like
a fantastic idea.

It would be cool if there was some resource that linked well-written,
idiomatic solutions for other languages as well. Anyone know if this has been
done?

~~~
w4tson
[https://github.com/Bogdanp/awesome-advent-of-
code](https://github.com/Bogdanp/awesome-advent-of-code)

Not all repos are idiomatic, there’s at least one that isn’t (mine).

I found some of the repos useful for bettering my rust skills.

------
wtmt
@forrestthewoods You have an incorrect link under the "Advent of Code"
section. The first link reads

    
    
        https://www.forrestthewoods.com/blog/learning-rust-via-advent-of-code/(https://adventofcode.com/
    

It should just be [https://adventofcode.com/](https://adventofcode.com/)

~~~
forrestthewoods
Fixed!

------
hocuspocus
I'm trying to do the exact same thing (learning Rust through solving the AoC)
but I'm not done yet.

Regarding parsing, a lot of times the input format is unnecessarily complex.
In "real-life", wouldn't you start by sanitizing your input? For me,
search/replace and column-edits have been enough to avoid regexps entirely.

~~~
K0nserv
Agreed. Most of my solutions[0] use something simple like

    
    
        input
        .lines()
        .map(|s| s.trim())
        .filter(|l| l.len() > 0)
        .map(CustomType::from)
        .collect::<Vec<_>>();
    

or for grid-like puzzels

    
    
        input
        .lines()
        .map(|s| s.trim())
        .filter(|l| l.len() > 0)
        .map(|l| l
                 .chars()
                 .map(CustomType::from)
                 .collect::<Vec<_>>()
        )
        .collect::<Vec<_>>();
    
    

0: [https://github.com/k0nserv/advent-of-
rust-2018](https://github.com/k0nserv/advent-of-rust-2018)

------
tlent
As someone who also completed Advent of Code in Rust this year this was very
interesting. I agree it is a great way to learn more about a new language.
Thanks for sharing!

It seems like we ran into a lot of the same pain points like parsing, lack of
a min-heap, etc.

Regarding HashMap initialization the best way I'm aware of to do what you are
looking for is with lazy_static[0]. Maybe not very clean or idiomatic though.

For sorting I found the best solution to that problem was using sort_by_key
with a tuple like (a.year, a.month, a.day, a.hour, a.minute).

[0]: [https://github.com/rust-lang-nursery/lazy-
static.rs#example](https://github.com/rust-lang-nursery/lazy-
static.rs#example)

------
jeffdavis
I use a different approach to learning new languages: I try to find out why
the language really exists. Often, there is a very good reason that justifies
all the work of creating a new language.

Often, this has more to do with the environment, runtime, etc., and the
features of the language merely exist to serve that purpose. If that were not
true, someone probably would have used an existing language and changed the
runtime.

For rust, it seems to exist to bring modern, safe language features to a
minimal C-like runtime existence by doing all of the work in the compiler. C
has continued to thrive all of this time because it has a minimal runtime and
a stable ABI defined on nearly every platform. Rust has a minimal runtime and
supports the C ABI very well, so it has a great chance to have a big impact in
areas where C/C++ might otherwise be used less safely and less productively.
So my focus when learning rust is how to use the C FFI while still writing
idiomatic rust.

It's harder to learn that way, but I find it more satisfying, especially in
cases where I don't have an immediate project that's going to use the
language. The fact is, no matter how many programming puzzles I solve in rust,
it's not going to make it any more likely that I will rely on it for anything
but a toy or hobby project. But learning the rust FFI does make it more likely
I will rely on it in the future. Puzzles can be solved in any language; weird
corner cases in the FFI cannot.

------
tpavel
Hey, I did the same thing this year! At least I tried to do AoC in Rust until
I reached the problem which required using doubly-linked lists... Turns out
it's not quite as easy as a Rust newbie would think.

Regarding input parsing, I pretty quickly converged to using regexes and never
looked back. I guess it's an acquired taste, but rubular.com is my best friend
when it comes to it.

~~~
forrestthewoods
In my sibling post on performance I go into details on the doubly-linked list
problem. [https://www.forrestthewoods.com/blog/solving-advent-of-
code-...](https://www.forrestthewoods.com/blog/solving-advent-of-code-in-
under-a-second/)

You're right that Rust doesn't make doubly-linked lists easy. For that problem
I somewhat skirted the issue. For a later problem I built an Octree. I used an
Rc<RefCell<OctreeNode>>. That feels pretty gross, but I'm still not sure what
the idiomatic pattern is. :(

~~~
saghm
In practice, most data structures like that in Rust written to be efficient
for real-world make judicious use of unsafe. For something like Advent of
Code, `Rc<RefCell<_>` is probably the correct way to go.

------
steveklabnik
Thanks a lot for writing this up! A lot of good stuff to digest here.

------
robostac
I used this years AoC to learn rust and had similar issues with input parsing.
I found a fairly simple parsing crate ([https://docs.rs/scan-
rules/0.2.0/scan_rules/](https://docs.rs/scan-rules/0.2.0/scan_rules/)) which
helped a lot with the simpler inputs (day 24 was still a pain). It pretty much
exactly matches the format you described.

------
nbeleski
I used AoC to try and learn a bit of Nim as I liked the idea of writting
simple scripts that could run many times faster than Python. I somewhat regret
it though.

I actually considered Rust given its popularity. I program C++ for a living,
but I'd love to find some use for Rust.

Rust will probably my language of choice for this year's challenge.

~~~
hnbroseph
what kind of regrets do you have?

~~~
nbeleski
I haven't used Nim for anything else since then. If I had to give a simplified
critique, the language in itself is still springing to life and the user base
and documentation are small.

I currently don't have the time to sink into something that could end up being
a gimmick only, while there is some hardcore optimization needed in one of our
services that could be approached through a better parallelization using Rust.

While from my point of view Nim is only interesting, Rust on the other hand
seems promising. (I don't mean to compare different purpose languages, the
problem stands on my spare time to study and the company needs, if that makes
sense.)

~~~
dom96
Hey! Nim core dev here. I was going to offer my support when I saw your
original comment but based on your critique so far there isn't that much that
I can do, other than keep doing what I'm doing already (evangelizing Nim as
much as I can).

This kind of thing does really worry me though, it's almost like a self-
fulfilling prophecy, you aren't going to use Nim because you think there isn't
enough people using it :(

Do you have any ideas of how we can make you and people like you change their
mind?

~~~
nbeleski
Hey mate, awesome you messaged! I am sincerely not skilled enough to comment
on the minutia of the language. I can say I really love the idea behind it and
as far as I can tell the implementation is solid.

From a end user point of view, I thought the official website and
documentation felt a bit lackluster, not because it was bad or anything (the
Nim tutorials part I & II are a godsend), but I instinctively compared it to
languages like Python. Again, this is not a fair comparison, more like a
impression. I'd love to see more blog posts like this one [0] or maybe the
occasional roadmap.

Ah I understand where the 'self-fulfilling prophecy' is coming, but if
anything I talked about Nim to dozens of people already, even if I can't
currently find utility for it on my day job (I do ML, Embedded and Image
processing, so it is actually really hard to use anything other than c++)

Again, commenting from the outside, it seems it is a hard position because
today upcoming languages such as rust and go have some solid enterprise
backing.

If anything, I don't think you need to change my mind! The Nim development is
something I keep a close eye on and I hope I will progressively incorporate it
on my daily tasks. The mentioned regret on the first post has really more to
do on how little spare time I have.

[0]: [https://nim-lang.org/blog/2018/06/07/create-a-simple-
macro.h...](https://nim-lang.org/blog/2018/06/07/create-a-simple-macro.html)

------
thekingofh
I do appreciate the rule set that Rust forces the programmer into, however I
keep coming back to it felt to me that I was being asked to conform to the
language. Maybe I'm looking at it all wrong. When I was also learning other
languages it felt to me some of them conformed to me more than others, and
therefore were easier to write logic towards. Is low level safe systems
programming the main/only use case or is there a reason to use Rust in other
domains?

~~~
the_duke
Rust is a bit like Haskell: a very strong but also complicated type system
that enables you to encode a lot of the logical restrictions into the API.

This is most notable immediately around the Result and Option types with their
many modifiers (map, map_err, and_then, ...) and how strings work.

This can feel pretty awkward and cumbersome compared to other languages and
that could be what you are describing.

It's not that way just for fun though: Most languages hide the underlying
complexity from you and 'punish' you at runtime if you violate invariants by
eg throwing exceptions. Rust forces you to deal with the invariants at compile
time.

This makes you write safer code and prevents many errors, even "business
logic" type errors if the API is right.

This is why "move fast and break things" aka prototyping is hard with Rust.
(Note you can also circumvent many of these restrictions by using "unwrap()"
et al a lot)

On the other hand, it gives you a high degree of confidence for writing and
refactoring code: if it compiles, it probably works. (Assuming you were
writing idiomatic code)

Rust is certainly not a good fit for every domain: the manual memory
management is often overkill, and in very business logic heavy code like your
typical CRUD apps, where most errors happen in logic that can't really be
encoded in the type system, the value is limited.

~~~
justwalt
“If it compiles, it probably works.”

This has been so true in my experience. I find that it does take longer
overall to write something in Rust, but I almost never have to root out
strange errors from my code. In shorter files, I almost never have to use
‘cargo run --debug’ twice.

~~~
davidhyde
I second that! I wrote a small service for a friend the other day. It had to
read some bytes off a tcp stream in a certain format and route them onto an
obscure protocol called a modbus. Given a screenshot of some sample input data
from an excel spreadsheet and some other minimal information about the target
destination format and address I was able to make the service without having
access to a real input tcp stream or the destination hardware required for the
modbus. However, to our amazement the software ran without fault the first
time it was executed on the target server. Sure I struggled to compile it
because I am still a beginner but that has just never happened to me before in
my professional career with c#. There is always some side case I haven’t
thought about no matter how many unit tests I write! Maybe I got lucky but it
definitely had nothing to do with my skills with rust which are still basic.

------
initium
What is HN-users view of learning a programming language through this kind of
problem-solving?

Is it limiting?

Should this be used as a complement to a larger toy project?

~~~
didymospl
It depends on the things that you want to learn and amount of extra work you
want to put into it. For instance, I used Go for the most of AoC 2018 problems
and I had always been using C/C++ in algorithmic contests when execution time
mattered. And yet, I wouldn't say I know any of these languages good enough to
write a production-grade system because I only learned usage patterns common
for small, write - run once - throw away applications.

On the other hand, the author of that post seems to have used a lot of more
complex language features, did profiling, debugging, made use of many
libraries etc - a lot of extra stuff that wasn't necessary to give correct
answers but is crucial in 'real world' programming.

------
nprateem
I haven't tried rust in a few years, but I remember it was kind of annoying
that you couldn't tell from a function name which parameters would invoke
borrow checker stuff. At that time there wasn't great IDE support, but is
there a common/idiomatic way of hinting the borrowing of function parameters?

~~~
Hortinstein
documentation is much better, here are a few links that should answer your
question

[https://doc.rust-lang.org/book/ch04-02-references-and-
borrow...](https://doc.rust-lang.org/book/ch04-02-references-and-
borrowing.html) [https://doc.rust-lang.org/book/ch10-03-lifetime-
syntax.html](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html)

------
Insanity
I used AoC to brush up on my python skills. Which was quite good - as soon I
am participating in a programming competition and my team is using Python.

In general AoC is just great for exploring new langs or brushing up on ones
you haven't used for some time.

------
Teknoman117
FWIW perf in Linux has rust symbol demangling.

