Blog coding article

Compilers as Teachers

Lotte, James
Article

Compilers as Teachers

Published on 5 min read

    In the introduction of our trainings, we like to stress that Rust takes its commitment to developer friendliness very seriously, and that the Rust compiler considers confusing error messages a bug. This has been an explicit sentiment of the Rust project since before the 1.0 release:

    During a currently ongoing training session, we accidentally perfectly captured this process within the span of a single training.

    After covering control flow with match and enums the previous day, we started our session with a "fill in the blanks" exercise to help our participants get into the right mind space for the training and to practice what they learned.

    It looked like this:

    #[derive(PartialEq, Debug)]
    enum FarmAnimal {
        Worm,
        Cow,
        Bull,
        Chicken { num_eggs: usize },
        Dog { name: String },
    }
    
    fn what_does_the_animal_say(animal: &FarmAnimal) {
    
        /* TODO: fill in the match statement below  */
        /*        to make this code compile         */
    
        let noise = match animal {
            /* Cow and Bull */ => {
                "moo".to_string()
            }
            /* Chicken      */ => {
                "cluck, cluck!".to_string()
            }
            /* Dog          */  => {
                format!("woof, woof! I am {}!", name)
            }
            /* Worm– or all silent animals?*/ => {
                "-- (silence)".to_string()
            }
        };
    
        /* Bonus task: Give Dogs named Lassie   */
        /* a different output                   */
    
        println!("{:?} says: {:?}", animal, noise);
    }
    
    fn main() {
        what_does_the_animal_say(
            &FarmAnimal::Dog {
                name: "Lassie".to_string()
        });
        what_does_the_animal_say(&FarmAnimal::Cow);
        what_does_the_animal_say(&FarmAnimal::Bull);
        what_does_the_animal_say(
            &FarmAnimal::Chicken{num_eggs: 3}
        );
        what_does_the_animal_say(&FarmAnimal::Worm);
    
        /*
        Output should be:
    
        Dog { name: "Lassie" } says: "woof, woof! I am Lassie!"
        Cow says: "moo"
        Bull says: "moo"
        Chicken { num_eggs: 3 } says: "cluck, cluck!"
        Worm says: "-- (silence)"
    
        */
    }
    

    After one of our participants saw a confusing error message, they shared their screen so we could learn and debug together.

    They had attempted to put what they'd learned about pattern matching enum fields using wild cards into practice, but – like multiple students – tried to use an enum variant that does not have any fields:

    #[derive(PartialEq, Debug)]
    enum FarmAnimal {
        Worm,
        Cow,
        Bull,
        Chicken { num_eggs: usize },
        Dog { name: String },
    }
    
    fn what_does_the_animal_say(animal: &FarmAnimal) {
    
        let noise = match animal {
    -        /* Cow and Bull */ => "moo".to_string(),
    +        FarmAnimal::Cow(_) => "moo".to_string(),
    
    // ...
    

    This made sense to them, as it was the primary way we had taught matching on enums in previous examples. However, this choice of syntax yielded an error message that was difficult to interpret for folks not familiar yet with all of the terminology around flavors of enum variants:

    error[E0532]: expected tuple struct or tuple variant, found unit variant `FarmAnimal::Cow`
      --> src/main.rs:15:9
       |
    15 |         FarmAnimal::Cow(_) => "moo".to_string(),
       |         ^^^^^^^^^^^^^^^ not a tuple struct or tuple variant
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0532`.
    error: could not compile `playground`
    
    To learn more, run the command again with --verbose.
    

    This error message precisely told them what they had gotten wrong, but using language they didn't yet understand, and unlike many other diagnostics, didn't tell them what to do instead!

    Working together with the class, we resolved the problem and confusion, but to demonstrate that misleading error messages really are bugs in rustc, we opened a diagnostics issue about this during the training. At the time, it felt like a great way to show them that Rust tracks these issues, and improves them continuously.

    Watching the compiler improve in real time

    What we didn't anticipate was that we'd get to demonstrate the full cycle of a diagnostics report within the span of our training:

    One day after we reported the issue, it was claimed by @ABouttefeux for implementation. Three days after we reported it, the new diagnostic had been implemented and merged, and now our training participant's interpretation of our warm-up exercise lives in the test suite, with the improved UI:

    error[E0532]: expected tuple struct or tuple variant, found unit variant `FarmAnimal::Cow`
      --> $DIR/issue-84700.rs:15:9
       |
       |     Cow,
       |     --- `FarmAnimal::Cow` defined here
    ...
       |         FarmAnimal::Cow(_) => "moo".to_string(),
       |         ^^^^^^^^^^^^^^^^^^ help: use this syntax instead: `FarmAnimal::Cow`
    
    For more information about this error, try `rustc --explain E0532`.
    

    And because this occurred at the start of an 8-day session, we get to report back to our group today that they've just contributed to the Rust compiler. Even better, in tomorrow's nightly, we can revisit the example to show them the change they helped to impact. All of this before we've even covered Lifetimes and Strings!

    Trainings: where new perspectives and expert knowledge come together

    Implementing these diagnostics are often a challenge, because you need two things at the same time:

    1. Someone who is new enough to the language that will bump into issues that experienced eyes will jump right over
    2. Someone who knows what COULD have been recommended there, to avoid the problem in the first place

    Luckily, when giving these trainings we usually have exactly the right combination of factors to make this magic happen. Sometimes, this leads to us improving our training material, and sometimes it lets us find stumbling blocks that the compiler could be catching.

    During our trainings we lean on the compiler as another teaching assistant, since it often points out exactly what our students need to see. It is unquestionably an invaluable resource for new learners of the language to give contextual information when and where they need it.

    New learners are also an incredible resource for the compiler, Ferrous as trainers, and the folks working on the compiler itself: they show all of us how we can better teach and support everyone, to make building something amazing more accessible for everyone.

    Special thanks to @ABouttefeux for clearing this stumbling block for our students, and helping to make this an amazing teaching moment.

    Interested in Rust Training?

    If you're interested in training for yourself or your development team, check out our training page for the courses we offer, or contact us directly for more details!