#Klooienmetcomputers

Closures

Arnout van Kempen over rommelen in een digitale wereld.

We kennen functies in Rust, net als in C. Maar Rust kent ook een variant die erg lijkt op een functie, maar het niet is: de 'closure'. Een simpel en volstrekt onzinnig voorbeeld:

fn plus_a(x: i32, a:i32) -> i32 {
    x + a
}

fn main() {
    let a = 5;
    let mut b = 6;
    b = plus_a(b, a);
    println!(“b = {:?}”, b);
}

We definiëren een aparte functie, in de declaratie-regel geven we aan welke argumenten van welk type zullen worden gebruikt en welk type de returnwaarde zal hebben. Het codeblock zelf is simpel, en omdat Rust, anders dan C, het niet nodig vindt dat je een expliciet return-statement gebruikt, kan je hier simpelweg de berekening zonder ; zetten en klaar ben je. Borrowing is niet nodig, aangezien de functie geen waarden verandert, dus het is allemaal vrij simpel. Zo simpel, dat de functie-declaratie zelf groter is dan de functie-logica, het codeblock. 

In dit geval is een functie natuurlijk helemaal onnodig complex, want we roepen deze maar eenmaal aan. Je had simpelweg in main() de optelling kunnen doen. Maar voor het idee van closures moeten we het toch even zo doen.

Ditzelfde voorbeeld, op vrijwel dezelfde manier opgezet, maar nu met een closure, ziet er als volgt uit: 

fn main() {
    let a = 5;
    let plus_a = | x | x + a;
    let mut b = 6;
    b = plus_a(b);
    println!(“b = {:?}”, b);
}

Dit keer geen functiedeclaratie, maar een 'functie' toegewezen aan een variabele. Deze 'functie' is een closure. Een paar verschillen tussen een closure en een functie vallen direct op:

   De closure maakt gebruik van argumenten, opgenomen tussen ||. In dit geval slechts één, maar meer kan ook, gescheiden door komma's, en geen argument kan ook. Dan heb je alleen de twee ||.

   Hoewel je met een closure alle kanten op kan, is het minstens onhandig als de functie-logica erg complex is. Dan is een functie echt een beter idee.

   De functie werd gedeclareerd buiten main() en dus is alles wat binnen main in scope is, in de functie buiten scope. Je zal via argumenten alles aan de functie moeten meegeven dat binnen de functie nodig is. In dit geval dus x en a. En de compiler moet worden verteld welke types de argumenten en de uitkomst hebben. De closure is gedefinieerd binnen main() en wat in scope is binnen main() is in scope in de closure. De regels van ownership, borrowing, lifetime en mutability blijven wel gewoon gelden. Maar zoals in het voorbeeld te zien is, de closure gebruikt een argument x, maar ook een variabele uit zijn omgeving a. Met een functie gaat je dat niet lukken.

   De Rust-compiler is nogal strikt in het gebruik van types, maar daar staat tegenover dat als het type van een variabele evident is, je dat niet specifiek hoeft te benoemen. let a = 5; heeft geen type-aanduiding, maar doordat a meteen de waarde 5 krijgt, neemt de compiler aan dat het een i32 is. Met een functie kan dat niet, je moet aan de compiler exact vertellen wat je van plan bent met de types, zelfs als je generieke types wil gebruiken zoals we eerder zagen. De closure heeft geen type-aanduiding en de gebruikte argumenten ook niet. Meestal kan de compiler uit de context ook hier afleiden wat de bedoeling is. We gaan iets optellen bij een i32? dan zal dat iets ook wel een i32 zijn en de uitkomst ook.

Dit alles maakt dat closures soms gewoon simpeler zijn dan een functie en soms oplossingen mogelijk maken die in een functie lastig kunnen zijn, zoals het gebruik van variabelen “uit de omgeving”. In dat laatste zit wel een relevante beperking: de variabelen uit de omgeving moeten al bestaan op het moment dat de closure wordt gedeclareerd. Technisch zou dat niet absoluut noodzakelijk zijn, maar de Rust compiler kan het anders niet. Dus hoewel ieder mens de volgende code zou begrijpen, gooit Rust de kont tegen de krib:

fn main() {
    let plus_a = | x | x + a;
    let a = 5;
    let mut b = 6;
    b = plus_a(b);
    println!(“b = {:?}”, b);
}

Als je daar enige logica in wil zien, is het goed te bedenken dat closures in zichzelf niet lazy zijn, de evaluatie wacht niet tot het aanroepen van de closure, maar begint direct. De compiler kan er wel tegen dat x nog onbekend is, want dat is een argument van de closure zelf. Maar wat moet bij die x worden opgeteld, zolang a nog niet bekend is?

Over laziness, maar vooral over waar closures sterk tot hun recht komen de volgende keer, als we iterators bespreken.

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.