ExamplesBy LevelBy TopicLearning Paths
088 Intermediate

088 — Allergies

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "088 — Allergies" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Given a score (up to 255), determine which allergens a person reacts to by decoding a bitflag. Key difference from OCaml: | Aspect | Rust | OCaml |

Tutorial

The Problem

Given a score (up to 255), determine which allergens a person reacts to by decoding a bitflag. Each of the eight allergens maps to a power-of-two score. Implement is_allergic_to(allergen, score) -> bool and allergies(score) -> Vec<Allergen> using bitwise AND. Compare with OCaml's land operator and list filtering.

🎯 Learning Outcomes

  • • Use bitwise AND (&) to test individual bits in a compact integer representation
  • • Map each enum variant to a power-of-two score with a match or a shift expression
  • • Use Allergen::ALL constant array to iterate all variants for allergies
  • • Understand why a u32 score (not u8) avoids overflow when scores combine
  • • Map Rust's bitflag pattern to OCaml's land (logical AND) integer operator
  • • Recognise the bitflag design pattern used in permissions, flags, and sets
  • Code Example

    #![allow(clippy::all)]
    /// Allergies — Bitflag Decoding
    ///
    /// Ownership: Allergen is Copy. Score is a simple u32.
    /// No heap allocation needed.
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Allergen {
        Eggs,
        Peanuts,
        Shellfish,
        Strawberries,
        Tomatoes,
        Chocolate,
        Pollen,
        Cats,
    }
    
    impl Allergen {
        pub const ALL: [Allergen; 8] = [
            Allergen::Eggs,
            Allergen::Peanuts,
            Allergen::Shellfish,
            Allergen::Strawberries,
            Allergen::Tomatoes,
            Allergen::Chocolate,
            Allergen::Pollen,
            Allergen::Cats,
        ];
    
        pub fn score(self) -> u32 {
            match self {
                Allergen::Eggs => 1,
                Allergen::Peanuts => 2,
                Allergen::Shellfish => 4,
                Allergen::Strawberries => 8,
                Allergen::Tomatoes => 16,
                Allergen::Chocolate => 32,
                Allergen::Pollen => 64,
                Allergen::Cats => 128,
            }
        }
    
        pub fn name(self) -> &'static str {
            match self {
                Allergen::Eggs => "eggs",
                Allergen::Peanuts => "peanuts",
                Allergen::Shellfish => "shellfish",
                Allergen::Strawberries => "strawberries",
                Allergen::Tomatoes => "tomatoes",
                Allergen::Chocolate => "chocolate",
                Allergen::Pollen => "pollen",
                Allergen::Cats => "cats",
            }
        }
    }
    
    pub fn is_allergic_to(allergen: Allergen, score: u32) -> bool {
        score & allergen.score() != 0
    }
    
    pub fn allergies(score: u32) -> Vec<Allergen> {
        Allergen::ALL
            .iter()
            .copied()
            .filter(|&a| is_allergic_to(a, score))
            .collect()
    }
    
    /// Version 2: Using bit position instead of match
    pub fn allergies_bitpos(score: u32) -> Vec<Allergen> {
        Allergen::ALL
            .iter()
            .enumerate()
            .filter(|&(i, _)| score & (1 << i) != 0)
            .map(|(_, &a)| a)
            .collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_eggs_only() {
            assert_eq!(allergies(1), vec![Allergen::Eggs]);
        }
    
        #[test]
        fn test_peanuts_and_chocolate() {
            assert_eq!(allergies(34), vec![Allergen::Peanuts, Allergen::Chocolate]);
        }
    
        #[test]
        fn test_everything() {
            assert_eq!(allergies(255).len(), 8);
        }
    
        #[test]
        fn test_none() {
            assert_eq!(allergies(0), vec![]);
        }
    
        #[test]
        fn test_is_allergic() {
            assert!(is_allergic_to(Allergen::Peanuts, 34));
            assert!(!is_allergic_to(Allergen::Eggs, 34));
        }
    
        #[test]
        fn test_ignore_high_bits() {
            assert_eq!(allergies(257), vec![Allergen::Eggs]);
        }
    }

    Key Differences

    AspectRustOCaml
    Bitwise ANDscore & allergen.score() != 0score land allergen_score allergen <> 0
    All variantsconst ALL: [Allergen; 8]let all = [Eggs; …] (list)
    Filter.iter().filter(…).collect()List.filter (fun a -> …) all
    Score typeu32int
    Variant copy#[derive(Copy)]Value type by default
    Score mappingmatch self { Eggs => 1, … }function \| Eggs -> 1 \| …

    The bitflag pattern is efficient and compact: eight boolean attributes stored in a single byte. The same technique underpins Unix file permissions, network flags, and capability bitmasks. The enum-to-power-of-two mapping with a const ALL array is a clean Rust idiom for this use case.

    OCaml Approach

    OCaml uses land (bitwise AND): score land allergen_score allergen <> 0. allergies score is List.filter (fun a -> is_allergic_to a score) all where all is a manually defined list. The logic is identical; the differences are syntactic (land vs &, list vs array) and minor.

    Full Source

    #![allow(clippy::all)]
    /// Allergies — Bitflag Decoding
    ///
    /// Ownership: Allergen is Copy. Score is a simple u32.
    /// No heap allocation needed.
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Allergen {
        Eggs,
        Peanuts,
        Shellfish,
        Strawberries,
        Tomatoes,
        Chocolate,
        Pollen,
        Cats,
    }
    
    impl Allergen {
        pub const ALL: [Allergen; 8] = [
            Allergen::Eggs,
            Allergen::Peanuts,
            Allergen::Shellfish,
            Allergen::Strawberries,
            Allergen::Tomatoes,
            Allergen::Chocolate,
            Allergen::Pollen,
            Allergen::Cats,
        ];
    
        pub fn score(self) -> u32 {
            match self {
                Allergen::Eggs => 1,
                Allergen::Peanuts => 2,
                Allergen::Shellfish => 4,
                Allergen::Strawberries => 8,
                Allergen::Tomatoes => 16,
                Allergen::Chocolate => 32,
                Allergen::Pollen => 64,
                Allergen::Cats => 128,
            }
        }
    
        pub fn name(self) -> &'static str {
            match self {
                Allergen::Eggs => "eggs",
                Allergen::Peanuts => "peanuts",
                Allergen::Shellfish => "shellfish",
                Allergen::Strawberries => "strawberries",
                Allergen::Tomatoes => "tomatoes",
                Allergen::Chocolate => "chocolate",
                Allergen::Pollen => "pollen",
                Allergen::Cats => "cats",
            }
        }
    }
    
    pub fn is_allergic_to(allergen: Allergen, score: u32) -> bool {
        score & allergen.score() != 0
    }
    
    pub fn allergies(score: u32) -> Vec<Allergen> {
        Allergen::ALL
            .iter()
            .copied()
            .filter(|&a| is_allergic_to(a, score))
            .collect()
    }
    
    /// Version 2: Using bit position instead of match
    pub fn allergies_bitpos(score: u32) -> Vec<Allergen> {
        Allergen::ALL
            .iter()
            .enumerate()
            .filter(|&(i, _)| score & (1 << i) != 0)
            .map(|(_, &a)| a)
            .collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_eggs_only() {
            assert_eq!(allergies(1), vec![Allergen::Eggs]);
        }
    
        #[test]
        fn test_peanuts_and_chocolate() {
            assert_eq!(allergies(34), vec![Allergen::Peanuts, Allergen::Chocolate]);
        }
    
        #[test]
        fn test_everything() {
            assert_eq!(allergies(255).len(), 8);
        }
    
        #[test]
        fn test_none() {
            assert_eq!(allergies(0), vec![]);
        }
    
        #[test]
        fn test_is_allergic() {
            assert!(is_allergic_to(Allergen::Peanuts, 34));
            assert!(!is_allergic_to(Allergen::Eggs, 34));
        }
    
        #[test]
        fn test_ignore_high_bits() {
            assert_eq!(allergies(257), vec![Allergen::Eggs]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_eggs_only() {
            assert_eq!(allergies(1), vec![Allergen::Eggs]);
        }
    
        #[test]
        fn test_peanuts_and_chocolate() {
            assert_eq!(allergies(34), vec![Allergen::Peanuts, Allergen::Chocolate]);
        }
    
        #[test]
        fn test_everything() {
            assert_eq!(allergies(255).len(), 8);
        }
    
        #[test]
        fn test_none() {
            assert_eq!(allergies(0), vec![]);
        }
    
        #[test]
        fn test_is_allergic() {
            assert!(is_allergic_to(Allergen::Peanuts, 34));
            assert!(!is_allergic_to(Allergen::Eggs, 34));
        }
    
        #[test]
        fn test_ignore_high_bits() {
            assert_eq!(allergies(257), vec![Allergen::Eggs]);
        }
    }

    Deep Comparison

    Allergies — Comparison

    Core Insight

    Bitflag decoding maps cleanly between both languages. The pattern — enumerate variants, assign power-of-2 scores, use bitwise AND to test membership — is universal. The difference is syntactic, not conceptual.

    OCaml Approach

  • land operator for bitwise AND
  • • Variant list [Eggs; Peanuts; ...] as the universe of allergens
  • List.filter to find matching allergens
  • function keyword for concise pattern match
  • Rust Approach

  • & operator for bitwise AND
  • const ALL array on the enum for iteration
  • .filter().collect() with iterator chain
  • 1 << i alternative using bit position
  • Comparison Table

    AspectOCamlRust
    Bitwise ANDland&
    Enum listlet all = [...]const ALL: [Allergen; 8]
    FilterList.filter.filter().collect()
    Score typeintu32
    String nameManual functionMethod returning &'static str

    Learner Notes

  • • Rust has no land/lor — uses C-style &, |, ^ operators
  • score & (1 << i) is an alternative to explicit score matching
  • • Both languages guarantee exhaustive matches — adding an allergen forces updates
  • • Consider bitflags crate for production Rust bitflag patterns
  • Exercises

  • Replace the score match with a bit-shift: 1u32 << (self as u32) for a zero-indexed enum. Verify the same scores result.
  • Add a Allergen::from_score(score: u32) -> Option<Allergen> that maps a single power-of-two back to a variant.
  • Implement allergies_count(score: u32) -> usize using .count_ones() on the score casted to u8.
  • Extend to 16 allergens by adding 8 more variants and changing the score type to u16.
  • In OCaml, implement a has_all : allergen list -> int -> bool predicate that returns true only when every allergen in the list is active in the score.
  • Open Source Repos