filtrer, projiser, aggreger
Skriv en kommentar
Det siste året har jeg knyttet intim bekjentskap til bruk av såkalte higher order functions; først da jeg virkelig tok i bruk LINQ og lambda i C# 3.0, deretter da jeg lærte meg Ruby, og til sist nå når jeg lærer meg å bruke språk som Erlang og F# i den funksjonelle paradigmen. Jeg har lært om Lambda calculus, og hva en closure er for noe – ting jeg nå mener alle utviklere burde ha et forhold til.
For å gi deg, kjære leser, et bedre innblikk i noe av dette, vil jeg snakke litt om noen av disse funksjonene av “høyere orden”. Higher order function betyr i bunn og grunn bare en funksjon som tar som innput en annen funskjon som spesifiserer en del av hva den skal utføre.
Filtrer
Filter er en av de mest vanlige, høyere orden funskjonene som brukes på lister eller andre datastrukturer. Gitt en liste og et predikat så brukes filter-funksjonen til å returnere de elementene i listen som tilfredstiller predikatet. Jeg sier “filter” fordi det er det funksjonen heter i de fleste programmeringsspråkene. I C# heter derimot filter-funksjonen for IEnumerable<T> Where, og den kan for eksempel brukes slik:
var aList = new []{1, 2, 3, 5, 8, 13, 21, 33, 54};
var evenNumbers = aList.Where(n => n % 2 == 0);
// array contains: 2, 8, 54
Filter-funksjonen for Enumerations i Ruby heter select (evt. find_all). Her er samme eksempel i Ruby:
2 $even_numbers = $a_list.select {|n| n % 2 == 0}
3 # array contains: 2, 8, 54
Projiser
Den neste av de mest brukte, høyere orden funksjonene er Map. Denne funksjonen tar en liste eller lignende, samt en funksjon for å omforme (“mappe”) elementene i listen til en ny type elementer. Dette kaller vi også en projisering (projection). C#-varianten av map heter Select (og er altså ikke det samme som select i Ruby). Ønsker jeg for eksempel å konvertere listen med tall om til en liste med strenger kan jeg gjøre det slik:
var evenNumbersAsString = evenNumbers.Select(n => n.ToString());
I de fleste andre språk heter funksjonen map, men i Ruby har man også et alias som heter collect.
Aggreger
Filter og map er fine funksjoner som er anvendelige og lette å forstå og å huske på. En som ikke er fullt så mye brukt, blant utviklere innenfor den imperative paradigmen i alle fall, er Fold. For å forvirre oss fremstår også denne funksjonen med ulike navn i de ulike språkene – som for eksempel Reduce, Accumulate, Compress og Inject. Og i .NET heter den Aggregate:
Console.Write(evenNumbersAsString.Aggregate(
"", (accumulator, next) => accumulator + next + " "));
// prints "2 8 54 "
Ser du hvordan den virker? I motsetning til de andre funksjonene som returnerer lister, så returnerer Aggregate én verdi. En verdi som er bygget opp ved å kalle det spesifiserte lambda-uttrykket for hvert element i listen. Accumulator er både output og input til lamdaen. For det første elementet vil accumulator ha verdien som ble spesifisert som den første parameteren til Aggregate, altså en tom streng. Når funksjonen kalles igjen for det andre elementet vil accumulator ha verdien som ble returnert fra den første. Og så fortsetter det til listen er tom, og accumulator returneres.
Her er nesten samme eksempel i Ruby, hvor fold heter inject (eller reduce om du ønsker å bruke det):
6 puts $foo[0..-3] # outputs “2, 8, 54″
Har du aldri brukt en slik funksjon ser dette sikkert ganske gresk ut. Den er på kanten til å være uleselig, og hvis du jobber i et team som ikke er så godt kjent med fold/aggregate så bør du kanskje være forsiktig med å bruke den. Men jeg lover deg at om du venner deg til denne funksjonen så vil du i mange tilfeller kunne produsere mere kompakt og elegant kode.
Oppsummering i F#
Her er et oppsummerende eksempel i F#. Jeg oppretter en liste, og bruker pipelining til å sende listen gjennom filter, map (projiser) og fold (aggreger), for til slutt å skrive ut den resulterende strengen.
open System
let aList = [1; 2; 3; 5; 8; 13; 21; 33; 54]
aList |> List.filter (fun n -> n % 2 = 0)
|> List.map (fun n -> n.ToString())
|> List.fold (fun acc n -> acc + n + " ") String.Empty
|> Console.WriteLine
Kategorier: F#, Polyglot, Ruby.
RSS feed for kommentarene.
Tilbaketråkk.
Programmeringsbloggen
June 9th, 2010 at 2:43 pm
Ryddig framstilling , gode eksempler, enkle forklaringer. Bra innhold som alltid på denne bloggen!
Det er mange som ikke vet om mulighetene med høyere ordens funksjoner og f.eks LINQ. Temaet kan virke veldig tungt hvis det framstilles for akademisk, så det er godt å se en lettspiselig forklaring på temaet!
June 10th, 2010 at 1:02 am
Currying og nå Higher Order Functions… Jeg synes du går i riktig retning ^_^
Og som vanlig:
putStrLn . foldl1 ((++).(++” “)) . map show . filter even $ aList
(«foldl1 ((++).(++” “))» kalles også «unwords», bortsett fra at førstnevnte ikke tåler en tom liste.)
June 10th, 2010 at 6:02 am
Takk for hyggelige kommentarer! Og Armeth,jeg må innrømme at jeg klassifiserer dine Haskell-eksempler blant noe av den mest uleselige koden jeg har sett. Ser ut som et forsøk på å være kryptisk. Selv Lisp, med alle parantesene, ser bedre ut etter å ha fått en fem minutters introduksjon ;)
June 10th, 2010 at 8:03 am
Hmm? Det er jo akkurat samme kode som din, bare baklengs. Hvis du tenker på «((++).(++” “))» så er en «pointfree» versjon av «(\acc n -> acc ++ ” ” ++ n)» og “unwords” er som sagt et mer leselig alternativ, men da ser du ikke at det er en fold involvert.
Er dette bedre?
(|>) = flip ($)
aList |> filter even |> map show |> unwords |> putStrLn
June 10th, 2010 at 8:19 am
Jepp, mye bedre! ;)
Selvfølgelig kan det tenkes at jeg er utsatt for å tolke leseligheten som dårlig fordi jeg ikke er vandt til syntaksen. Men jeg har tatt meg selv i å lure på om man ikke ofrer litt vel mye lesbarhet når man skriver så konsis kode som jeg ofte ser spesielt fra funksjonsbaserte språk. “Pointfree” er ett eksempel på noe jeg vil være forsiktig med.
Jeg antar at pipelining ikke er så vanlig å bruke i Haskell, og kanskje ikke i andre FP-språk heller, men for en med OO-bakgrunn blir det lettere å forstå. F#-koden min i oppsummeringen ville i praksis vårt helt lik i C# om jeg brukte Linq og method chaining.
I objektorientert programmering har vi blitt mer og mer opptatt av lesbar kode, og bryter f.eks. gjerne opp i mange små metoder kun for å oppnå beskrivende navn / selvdokumenterende kode. Kunsten er å balansere mengde kode med hvor mye hjernekapasitet som må til for å forstå den! Utviklere bruker mye mer tid på å lese kode enn å skrive den..
June 10th, 2010 at 10:27 am
Joda, pipelining brukes mye, i den grad det kan kalles det. Hele do-notasjonen er syntaktisk sukker for pipelining av monader ((do {x >= \x -> putStrLn x) == (getLine >>= putStrLn)), men for funksjoner er det som oftest baklengs som i det første eksempelet mitt. (|>) er som du ser bare en annen funksjon med dens argumenter byttet om (flip f a b = f b a) (og (|>) er også definert i «pointfree»…)
November 6th, 2012 at 2:48 pm
[...] jeg programmerer. Jeg har snakket om dette mange ganger. Første gang var i artikkelen jeg kalte filtrer, projiser, aggreger. Og sist var i artikkelen Den Lille Erlanger. Og nå gjør jeg det jammen meg [...]