Data-analyse, The way of Rust deel 2
Arnout van Kempen over rommelen in een digitale wereld.
Een tweede module die we gebruiken is io, maar deze komt niet uit een crate van anderen, maar uit de standaardbibliotheek van Rust. Met use std::io::{self, BufRead, BufReader}; geven we aan de compiler aan dat de module std::io zelf in scope is, dat de trait BufRead en de struct BufReader gebruikt kunnen worden. Hiermee is het mogelijk om gebufferd te lezen uit, in dit geval, stdin. En stdin is dan weer de standaard input-stroom van het operating system, Windows, Linux of MacOS. Als je niets doet, is stdin gekoppeld aan het toetsenbord en dat is nu niet handig. Maar je kan via redirection een bestand aan stdin koppelen. Met < geef je aan dat een bestand als input zal dienen, bij het aanroepen van het programma.
Ook onderdeel van de io is het gebruik van een resultaat van het type Result voor de main-functie. Als alles goed gaat zal het programma een code Ok(()) aan het besturingssysteem teruggeven. En gaat het ergens fout, dan geeft het een error. Het betekent ook dat in het programma zelf via gebruik van een ? een panic() eenvoudig kan worden doorgegeven aan de hoofdfunctie, die vervolgens afsluit. Zo heb je nette foutafhandeling zonder veel moeite.
Maar het echt grote verschil tussen de code die we eerder in C schreven en vervolgens letterlijk naar Rust vertaalden en deze idiomatic Rust-code, zit toch vooral in dit stukje:
let r = BufReader::new(io::stdin());
let n = r.lines().try_fold(0, |partial, line| -> io::Result<_> {
let count = line?
.chars()
.filter(|&c| c.to_lowercase().next().unwrap() == search_char)
.count();
Ok(partial + count)
})?;
De variabele r wordt hier ingericht als buffer van waaruit gelezen kan worden. Bufferen is vaak efficiënter dan byte voor byte lezen, terwijl het minder geheugen inneemt dan ineens een compleet bestand lezen. En let op, stdin is geen bestand, maar de toetsenbordbuffer. Alleen maken we er via redirection alsnog een bestand van. Wat hier dus effectief gebeurt, is dat het programma een stukje geheugen reserveert en dat vult met wat er ook maar via stdin binnenkomt, tot dat geheugen vol is. Als iets uit deze buffer wordt gelezen, ontstaat er ruimte voor nieuwe data. Zo wordt het lezen van de data op byte of char niveau gedaan door het programma, terwijl het lezen vanaf harddisk in grotere blokken ineens kan gaan.
De variabele n wordt gevuld door een reeks methoden die achter elkaar gekoppeld worden. En dat is vrij typisch Rust. Al deze methoden werken zonder dat je hoeft aan te geven hoe ze werken, je geeft aan wat ze moeten doen. In C had je die optie niet, daar moet je het proces beschrijven; het algoritme, dat aangeeft hoe een resultaat bereikt moet worden, zonder dat je kan zien wat je aan het doen bent.
Met r.lines() wordt de inhoud van de buffer r genomen en regel voor regel teruggegeven, als iteratie. Vervolgens wordt try_fold gebruikt. Dit is een methode die een closure over een iteratie laat lopen, en de uitkomst van de closure per iteratie saldeert. Klinkt ingewikkeld, maar in feite staat er dat dezelfde functie, weergegeven als closure, wordt losgelaten op iedere regel input en dat de uitkomst van die closure wordt opgeteld.
Het echte telwerk gebeurt vervolgens in de closure:
|partial, line| -> io::Result<_> {
let count = line?
.chars()
.filter(|&c| c.to_lowercase().next().unwrap() == search_char)
.count()
Hier wordt de variabele count gebruikt per regel de gezochte letter te tellen. Eerst wordt met line? de inhoud van het Result van de bovenliggende iteratie te verkrijgen. Gaat dat fout, dan zorgt de ? er voor dat direct met een error wordt afgebroken. En als het goed gaat is het resultaat de inhoud van Ok(), en dat is dan dus een regel uit stdin. Met chars() wordt daar weer een iterator over de regel van gemaakt, die karakter voor karakter uitleest. Met filter() haal je daar dan de karakters uit die voldoen aan het criterium, in dit geval dat ze, in lowercase, gelijk moeten zijn aan het gezochte karakter. En tenslotte telt count() het aantal karakters dat uit het filter overblijft.
Door nu in de bovenliggende iteratie steeds het saldo op te tellen bij het totaal per regel, tel je uiteindelijk dus alle karakters die gelijk zijn aan het gezochte karakter. Dat alles wordt dus, zonder dat je zelf iets aan programma-logica of -flow schrijft, als waarde aan de variabele n afgeleverd.
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
Over bits & bytes
Arnout van Kempen over rommelen in een digitale wereld.
En arrays dan?
Arnout van Kempen over rommelen in een digitale wereld.
Typecasting in COBOL
Arnout van Kempen over rommelen in een digitale wereld.
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.