#Klooienmetcomputers

Lang zullen ze leven

Arnout van Kempen over rommelen in een digitale wereld.

Rust, weten we inmiddels wel, doet er alles aan om 'veilige' code te produceren. De compiler controleert wat we doen voor we het doen. Waar C alles compileert en je vervolgens tijdens de uitvoering van je programma de pijn laat voelen met volstrekt onbegrijpelijke foutmeldingen, zal de Rust-compiler simpelweg weigeren je code te compileren als er een zichtbare fout in zit. Je krijgt er heldere foutmeldingen bij die meteen verklaren waarom het mis gaat.

Waar we het vandaag over moeten hebben is lifetimes, ofwel: Hoe lang leeft een variabele? Neem de volgende code uit het Rust book:

fn main() {
     let r;
     {
          let x = 5;
          r = &x;
     }
     println!(“r: {}”, r};
}

Niks bijzonders. De variabele r bevat een referentie naar de variabele x. Alleen, omdat x wordt gedeclareerd in het binnenste codeblock, is x uit zijn scope bij het afsluiten van dat codeblock. Maar omdat r buiten dat codeblock is gedeclareerd, blijft r in scope. En dat betekent een probleem, want r heeft als waarde een referentie naar x gekregen. Dus waar wijst r naar als x niet langer in scope is? C zou zeggen, r verwijst gewoon nog steeds naar hetzelfde geheugenadres waar x ooit stond. Wat daar gebeurt? Geen idee, dat zien we wel als het programma loopt. Voor Rust is dat onacceptabel. Dus de compiler geeft een foutmelding. Als r langer leeft dan de x waar r naar refereert, dan haakt de compiler af. Of nauwkeuriger, dan grijpt de borrow checker in. Er is een borrow van een variabele die niet meer leeft en dat mag niet. 

Dit is slechts een illustratie van het probleem van lifetimes. De enige oplossing hier is het herschrijven van de code, bijvoorbeeld door het weglaten van een codeblock. Het probleem van verschillende lifetimes kan zich echter ook voordoen in situaties waarbij de oplossing er uit bestaat de compiler simpelweg te wijzen op de benodigde lifetime. Neem de volgende functie: 

fn langste(s1: &str, s2: str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

Op het eerste gezicht weer niks bijzonders. Twee string-slices worden vergeleken en de langste van de twee wordt teruggegeven. Het probleem voor de compiler is echter dat deze bij de functie-declaratie niet kan weten of de twee slices die als argument dienen, lang genoeg leven om tot de return-slice te komen. Uit de logica van de functie volgt dat hier geen probleem ontstaat, maar de compiler analyseert de code van een functie niet om dat vast te stellen. Daarom krijg je een foutmelding bij compileren, met een oplossing. Zo werkt het wel:

fn langste<‘a>(s1: &’a str, s2: &’a str) -> &’a str { 

Wat hebben we nu toegevoegd? Een 'lifetime annotatie'. Die doet op zichzelf niets. Als je de functie langste() zou gebruiken met een codeblock zoals in de eerste code, krijg je nog steeds dezelfde foutmelding: je borrow (in dit geval is dat de string slice &str) overleeft, terwijl de eigenaar van de String uit scope raakt. Maar door het toevoegen van de lifetime annotatie help je de compiler wel te analyseren of code die de functie aanroept netjes met de lifetimes om gaat, zonder zelf de functie te hoeven analyseren. 

Iedere keer als je de compiler voor lifetime-raadsels stelt, kan je die oplossen met een lifetime annotatie. En als je niet ziet dat je raadsels schrijft, wijst de compiler je daar wel op. 

De notatie zelf is eenvoudig: met de geef je aan dat je een lifetime annotatie maakt. Die geef je vervolgens een naam, zoals hier a. Voor functies plaats je de lifetime annotatie tussen de <> die we al eerder zagen voor het gebruik van generieke types. En vervolgens plaats je dezelfde annotatie voor de type-aanduiding, maar achter de & van alle argumenten en return-waardes die dezelfde lifetime moeten hebben. In de functiecode zelf doe je hier verder niets mee, het is puur een aanwijzing voor de compiler welke lifetimes te verwachten bij deze functie. 

De apostrof is in principe genoeg, maar het is handig om de conventie te volgen om generieke types met een hoofdletter (T, U, V etc) te benoemen, lifetimes met een kleine letter (a, b, c), zodat duidelijk is wat wat is. Rust gebruikt overigens voor alles wat je een naam kan geven dergelijke conventies: 

1.Variabelen en parameters: snake_case (kleine letters, underscore als spatie). De naam geeft de betekenis weer, bijvoorbeeld speler_score, voorraad_pindakaas

2.Functies en methoden: snake_case. Opnieuw, met betekenis graag: voeg_toe, bereken_waarde_pindakaas

3.Structs, enums, overige typedefinities en type-aliassen: PascalCase (beginnen met hoofdletters, geen spaties). Bijvoorbeeld VoorraadWaarde, Pindakaas

4.Constanten: SCREAMING_SNAKE_CASE (alleen hoofdletters, underscore also spatie). Zoals PRIJS_KILO_PINDAKAAS, CAPACITEIT_SILO

5.Generieke typeparameters: PascalCase, maar bij voorkeur 1 letter, T, U, V, ValueType

6.Lifetimes: kleine letters, eventueel één woord, als a, b, c, static

7.Macros: snake_case met uitroepteken: print!, println!

8.Modules en crates: opnieuw snake_case.

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.

Arnout van Kempen di CCO CISA is Senior manager Risk & Compliance bij Baker Tilly. Hij schrijft op persoonlijke titel. Hij is lid van de Commissie Financiƫle verslaggeving & Accountancy van de AFM en lid van de signaleringsraad van de NBA. Daarnaast is hij diaken van het bisdom 's-Hertogenbosch.

Gerelateerd

reacties

Reageer op dit artikel

Spelregels debat

    Aanmelden nieuwsbrief

    Ontvang elke werkdag (maandag t/m vrijdag) de laatste nieuwsberichten, opinies en artikelen in uw mailbox.

    Bent u NBA-lid? Dan kunt u zich ook aanmelden via uw ledenprofiel op MijnNBA.nl.