Functies van diverse types
Arnout van Kempen over rommelen in een digitale wereld.
Een functie in Rust heeft of geen argumenten, of argumenten van een vaststaand type. En een functie heeft een returnwaarde van een bepaald type, of geen returnwaarde. Maar wat nu als we een functie willen maken die op verschillende types moet kunnen werken? Neem bijvoorbeeld een functie om het aantal elementen in een vector te tellen, of een functie om het grootste element in een vector te vinden. Afhankelijk van het type van de elementen van de vector zou je hiervoor aparte functies moeten schrijven, die inhoudelijk exact hetzelfde zijn. Maar omdat ze een verschillend type bewerken, krijgen ze een verschillende aanroep. Voor het vinden van het grootste element zou dat er als volgt kunnen uitzien:
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
De logica van deze functie is redelijk eenvoudig. Je gaat de functie in met een referentie naar een list van integers van 32 bits, de returnwaarde komt uit die list en zal dus ook een i32 zijn. Vervolgens begin je bij element 0, dat als de grootste wordt aangeduid. Met een for loop doorloop je nu alle elementen van list en als je een groter element tegenkomt, wordt dat de grootste. Op het einde geef je het grootste element weer terug.
Maar als je ditzelfde met een andere list wilt doen, ongeacht het type, dan moet je met dezelfde code steeds een nieuwe functie maken, voor het nieuwe type. En dat schiet niet op. Rust kent daarvoor het generieke type. Je mag dit iedere naam geven die je wil, maar Rust-praktijk is één letter, een hoofdletter en meestal kies je een T (van "type") als die nog vrij is. Hier een voorbeeld voor een functie die het aantal elementen in een vector telt, ongeacht het type van de elementen. De uitkomst heeft wel een vast type, het zal immers een positief, geheel getal zijn. Om grote vectoren aan te kunnen kiezen we bijvoorbeeld voor u16. De functie wordt nu
fn aantal<T>(list: &[T]) -> u16 {
let mut teller = 0;
for _ in list {
teller += 1;
}
teller
}
Wat gebeurt hier nu precies? In de eerste regel geven we met <T> aan dat in de argumenten en/of de returnwaarde van deze functie met generieke types zal worden gewerkt. Pas als we de functie aanroepen, zal duidelijk worden welk type dat gaat zijn. Bij de argumenten zien we dat waar normaal gezien een type had moeten staan, nu de T staat, in ons geval een referentie naar een lijst van type T, dus &[T]. De rest ziet er normaal uit. In het codeblock van de functie zit nog een aardigheidje van Rust. Als je volgens de syntax van Rust ergens een variabele moet gebruiken, maar die variabele heb je verder nergens voor nodig, dan kan je een _ invullen. We hebben nu dus een functie die het aantal elementen in een vector van ieder denkbaar type kan tellen.
Passen we datzelfde toe op de eerder genoemde functie largest, dan gaat het mis. Waarom? Omdat we alle types toelaten, maar niet alle types kunnen vergeleken worden naar grootte. Immers, alleen ordinale types kunnen naar grootte worden gesorteerd. Wat nu als ons type een enum is met de waarden papier, steen, schaar? Wat is groter, papier of steen? De compiler accepteert de generieke functie dus niet. Gelukkig is daar wel een oplossing voor, door de mogelijke types te beperken tot ordinale types. We bereiken dit door de functie als volgt te declareren:
fn largest<T: std::cmp::PartialOrd>(list : &[T]) -> &T
Nu is T beperkt tot ordinale types. De rest werkt weer zoals verwacht: een argument bestaande uit een referentie naar een lijst en als returnwaarde een referentie naar een waarde met hetzelfde type als de elementen van de lijst.
Om te demonstreren dat dit werkt, staat op GitHub een code met daarin beide hiervoor uitgewerkte functies, met een generiek type als argument. In main worden beide functies nu toegepast op een vector van integers en een vector van characters. En dat blijkt netjes te werken.
Let nog even op een paar details: de list zelf wordt beide keren gemaakt met de macro vec! en is niet mutable. Dat is ook niet nodig, want we veranderen niets. Ownership wordt niet overgedragen bij aanroep van de beide functies, we maken gebruik van borrowing, dus alleen de referentie wordt doorgegeven. Zouden we dat niet doen, dan zouden we geen twee functies op dezelfde list los kunnen laten, immers na overdracht van ownership naar de eerste functie, zou de variabele waarmee we begonnen ownership kwijt zijn en geen tweede functie meer kunnen benutten.
Ten slotte nog aardig om te zien, is dat we de variabele result en lengte twee maal lijken te gebruiken. Dat is echter schijn. Beide variabelen worden vervangen door een nieuwe variabele met dezelfde naam. Als we dezelfde variabele twee keer hadden willen gebruiken, dan hadden we deze mutable moeten maken en de tweede keer zonder let moeten gebruiken.
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
Gewone variabelen
Arnout van Kempen over rommelen in een digitale wereld.
Bestanden in soorten en maten
Arnout van Kempen over rommelen in een digitale wereld.
Alles draait om data
Arnout van Kempen over rommelen in een digitale wereld.
Omgeving: input-output
Arnout van Kempen over rommelen in een digitale wereld.
Omgeving: de configuratie
Arnout van Kempen over rommelen in een digitale wereld.