Mange skal bli flere (eller noe sånt)


søndag 13. juni 2010 Funksjonell programmering C#

I artikkelen Filtrer, Projiser, Aggreger presenterte jeg tre av de mest grunnleggende høyereordens-funksjonene man bruker til å behandle lister og andre datastrukturer. Grafisk kan de tre funksjonene illustreres slik:

filter_map_fold

Filter returnerer et subsett av den orginale strukturen. Map omformer strukturen, og returnerer en ny struktur som har et 1-til-1 forhold til de orginale dataene. Fold returnerer én verdi som er generert ved å analysere hele datastrukturen.

I denne blogposten vil jeg se nærmere på en fjærde funksjon, som kan illustreres på denne måten:

select_many

Denne funksjonen omformer/projiserer den orginale datastrukturen, på samme måte som map, men uten at det er et 1-til-1 forhold mellom input og output. Dette kan brukes til å "hente ut" data fra en mer kompleks datastruktur, men uten at det slås sammen slik som i fold/aggregate.

La oss anta at vi har en liste med SMS-meldinger. Hver melding har en meldingstekst, og vi er interessert i å lage en liste med alle ordene som inngår i meldingene. Dette tilfellet er ikke så veldig komplekst, men nok til å illustrere poenget. I C# kunne vi for eksempel ha opprettet en tom liste, og loopet over alle meldingene for så å trekke ut ordene fra teksten, slik som dette:

// ALTERNATIV 1 : foreach
var words1 = new List<string>();
foreach (var message in messages)
    words1.AddRange(message.Text.Split());

Takket være AddRange-metoden slipper vi en eksplisit indre loop for å legge til alle ordene.

Bruker du CodeRush i Visual Studio vil den foreslå at du omformer den vanlige foreach-loopen til en Linq ForEach action, slik som i alternativ 2:

// ALTERNATIV 2 : ForEach action
var words2 = new List<string>();
messages.ForEach(message 
    => words2.AddRange(message.Text.Split()));

.. men i bunn og grunn er dette selvsagt akkurat det samme.

Kan vi bruke en høyereordens-funskjon i stedet? Ja, det er klart, vi kan for eksempel bruke Aggregate. Som du så lengre oppe skal aggregate returnere én verdi, men en liste er jo også én verdi, så det burde gå greit:

// ALTERNATIV 3 : Aggregate action
var words3 = messages.Aggregate(new List<string>(), 
              (list, message) => 
              {
                  list.AddRange(message.Text.Split());
                  return list;
              });

I stedet for å opprette listen som en egen statement (som i alternativ 1 og 2) oppretter vi den her som første argument til aggregate. Den listen brukes så videre når en og en melding blir behandlet, og lambda-uttrykket må returnere listen for hver melding. Ja, dette fungerte, men dette ble ikke særlig elegant…

Nå er det derimot på tide å introdusere denne artikkelens hovedperson, funksjonen som i .NET har fått navnet SelectMany. Den gjør akkurat det vi er ute etter; vi spesifiserer en funksjon som gitt en melding returnerer en ny liste. SelectMany vil så slå sammen alle listene som produseres:

// ALTERNATIV 4 : SelectMany
IEnumerable<string> words4 = messages.SelectMany(message 
    => message.Text.Split());

Dette var klart mye bedre enn enn å bruke aggregate, og mer konsist og deklerativt enn å bruke foreach.

Med litt av det syntaktisk sukker som ligger i C# 3.0 og Linq kan dette også skrives slik:

// ALTERNATIV 5 : From x 2
var words5 = from message in messages
             from words in message.Text.Split()
             select words;

Slik jeg har forstått det genererer denne bruken av 2 from den samme koden som alternativ 4.

PS: F# har akkurat den samme funksjonen, men der heter den collect. Dette må ikke blandes med Ruby, hvor collect forvirrende nok er et alias for map. Jeg kjenner ikke til hvilke andre spåk som har funksjonen, og følgelig ikke hva de heter (men jeg er sikker på at Ameth vil fortelle meg alt om hvilke muligheter man har i Haskell).

Knagger: , , , ,


comments powered by Disqus