Wednesday, June 9th, 2010
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:

1 $a_list = [1, 2, 3, 5, 8, 13, 21, 33, 54]
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):

5 $foo = $even_numbers.inject() {|acc,next| #{acc}#{next.to_s}, }
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.

7 kommentarer til “filtrer, projiser, aggreger”

  1. Ole Gunnar Borstad Says:

    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!

  2. Ameth Says:

    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.)

  3. Torbjørn Says:

    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 ;)

  4. Ameth Says:

    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

  5. Torbjørn Says:

    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..

  6. Ameth Says:

    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»…)

  7. Hvorfor map/filter/reduce? Says:

    [...] 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 [...]

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Siste kommentarer

Torbjørn
PS: Takk til Børge Hansen, som delte SCARF-modellen med meg!...
Børge Hansen
Denne likte jeg veldig godt. Du skriver godt og har gode betraktninger  Keep it up – flere trenger å tørre å lære mer om ledelse – du l...
Tormod
Er egentlig ikke overrasket. F# sin fortè er programmererens produktivitet/kvalitet og anledning til parallell kjøring. Men kjøremotoren har ...
Stian
Ville også prøvd med et større problem (x100 eller x1000 f.eks). Når man snakker så små brøkdeler av et sekund som her så kan tiden for en ell...
Torbjørn
Har ikke sjekket - tar en titt i morgen hvis tid :)...
Einar W. Høst
Mhp tco: hva sier ILSpy?...
Torbjørn
Har ikke sett noe på PSeq før, men kjenner til den typen funksjoner fra blant annet Clojure. Og problemet med slike funksjoner i sammenhenger som de...
Håvard
Veldig bra sammenligning! Har du sett på ytelsen av PSeq.* fra powerpakken? Tipper den vil gi performancehit på små mengder, men kan kanskje resul...
Torbjørn
Jeg kom på en demonstrasjon-variant til jeg burde inkludere, nemlig bruk av list comprehension (en type computation expression (også kalt monads)). ...
Einar W. Høst
Interessant, det blir en trade-off mellom eleganse og fart på en måte. Den funksjonelle løsningen med vanlig filter er ren og pen, mens den imperat...
Creative Commons-lisens
Innholdet på denne bloggen er tilgjengelig under Creative Commons Navngivelse-Ikkekommersiell-DelPåSammeVilkår 3.0 Norge lisens.

Programmeringsbloggen
Kjempekjekt.com

© 2006-2013 Torbjørn Marø

Jeg har vært en profesjonell programmerer siden 1999, og dette er min blogg. Målet med bloggen er å stimulere meg selv og alle andre til kontinuerlig eksperimentering og læring.

Jeg forsøker å være allsidig, og programmerer blant annet i C#, Ruby, Erlang og Clojure.

Jeg praktiserer TDD og andre smidige utviklingspraksiser. Jeg er opptatt av kvalitet og ren kode.

Dette og ganske mye mer kan du lese om på denne bloggen!