Enums, pattern matching en Options
Arnout van Kempen over rommelen in een digitale wereld.
Eerder zagen we structuur en enums in Rust, die erg lijken op dezelfde concepten in C. De aardige extra was dat we via impl functionaliteit, "methoden", kunnen toevoegen aan de data-structuur. Maar enums hebben nog een aantal andere extra mogelijkheden in Rust, die C niet kent.
We blijven weer een beetje bij Henk en zijn huisgenoten. De vorige keer deden we dat met structs, dit keer gaan we enums gebruiken, om de mogelijkheden van die data-structuur te laten zien.
We definiëren:
enum Bewoner {
Mens { naam : String, leeftijd : u32 },
Kat { naam : String },
Kanarie,
}
Dat betekent dat een variabele van het type Bewoner drie statussen kan aannemen: Mens, Kat en Kanarie. Maar per status slaan we verschillende aanvullende gegevens op, of helemaal geen.
Waar we bij een struct vrij eenvoudig bij de inhoud van de velden konden komen (variabele.veld), gaat dat bij een enum iets minder simpel. In Rust is de voor de hand liggende manier "pattern matching". Dit ziet er in beginsel zo uit:
let getal = 7;
match getal {
1 => println!(“Het getal is 1”),
2 => println!(“Het getal is 2”),
// etcetera
}
De compiler controleert of je wel alle opties voor in dit geval getal gebruikt. Als je een soort restcategorie wil maken, dan kan dat op twee manieren. Als je de waarde wil gebruiken, dan doe je dat met other, en zo niet, dan gebruik je _.
De hiervoor genoemde match zou je dus op de volgende manieren kunnen afmaken:
_ => println!(“Het is een ander getal”),
of
other => println!(“Het getal is {other}”),
In de code die op GitHub staat, voor ons programma over Henk en zijn huisgenoten, gebruik ik een methode voor de enum Bewoner, om via pattern matching de inhoud van een variabele van dit type te printen. Hierdoor kan later bewoner1.print(); worden gebruikt. Dit is vrij rechttoe rechtaan, maar we kunnen het zo ingewikkeld maken als we willen. Een nog steeds simpel voorbeeld daarvan is de functie om huisdieren mee te tellen:
fn huisdier(bewoner : Bewoner) -> u8 {
match bewoner {
Bewoner::Mens {
naam: _,
leeftijd: _,
} => {
println!(“In dit huis woont ook een mens!”);
0
}
Bewoner::Kat { naam: _ } => 1,
Bewoner::Kanarie => 1,
}
}
Let op het gebruik van _ om aan te geven dat we bepaalde velden niet gebruiken. Maar ze zijn er wel en moeten dus worden benoemd.
We zien hier een aantal zaken terugkomen. Per tak van de match kunnen we een waarde of een opdracht geven, maar met { en } kunnen we ook een compleet codeblock in een tak van de match kwijt. En omdat hier na de match niets meer komt, en het programma de match direct beëindigd nadat een overeenkomst is gevonden, kunnen we de return waarde van de functie simpelweg aan het einde van iedere tak zetten zonder ; er achter.
Op het einde van de code op GitHub heb ik nog iets opgenomen dat met Henk niets te maken heeft, maar alles met enums en pattern matching: de enum Option<T>. Dit is een enum die in Rust zo veel voorkomt, dat deze maar gewoon standaard gedefinieerd is in ieder programma. Een Option is in feite een soort verpakking voor een waarde van type T. Daarbij zijn er twee opties: Some() en None.
Je kan nu matchen op de optie, actie ondernemen met de waarde als het een Some() is en een oplossing geven als het None is. Dat lijkt wellicht omslachtig, maar als je je realiseert hoe dit in C zou werken, moet je de schoonheid van deze optie wel zien. Neem bijvoorbeeld het openen van een bestand. In C zou je zoiets doen als
fp = fopen(“bestand”, “r”);
Dat schiet lekker op, maar wat als dat bestand niet bestaat? Dan krijgt fp de waarde NULL, en als je daar geen controle op uitvoert zal C je code gewoon uitvoeren. Maar dan gebeuren er wel ongelukken.
Wat nu als je daar in Rust de Option<T> voor zou gebruiken? De compiler zou dan controleren of je, bij het ophalen van de pointer naar je bestand, zowel de optie some (het is goed gegaan) als none (het is niet goed gegaan) afloopt. Kortom, foutafhandeling in de uitvoering wordt al afgedwongen door de compiler. Nooit meer gedonder met NULL-pointers!
We zullen later zien dat Rust het nog iets mooier doet, maar conceptueel is dit wel wat er gebeurt.
We kunnen dit dus ook zelf gebruiken. Iedere keer dat je een variabele hebt die mogelijk leeg is, kan je Option<T> gebruiken om de waarde in te verpakken en vervolgens foutafhandeling te bouwen in je match.
Wie mee wil doen met #klooienmetcomputers kan dat doen via GitHub. Maak een account op github.com en zoek naar Abmvk/kmc. Het account Abmvk volgen kan ook. Lezers zijn vrij te gebruiken wat ze willen en om zelf zaken toe te voegen of aan te passen, vragen te stellen of commentaar te leveren.
Gerelateerd
Herhalingen
Arnout van Kempen over rommelen in een digitale wereld.
Voorwaarden in COBOL
Arnout van Kempen over rommelen in een digitale wereld.
Het Y2K-probleem
Arnout van Kempen over rommelen in een digitale wereld.
Over bits & bytes
Arnout van Kempen over rommelen in een digitale wereld.
En arrays dan?
Arnout van Kempen over rommelen in een digitale wereld.