Forbij

 
24 juli 2009

Zeer recent (juni 2009) gaan er op internet stemmen op die m.i. de moeite waard zijn om aandacht aan te besteden. En het betreft niet zomaar een programmeerissue. Niet minder dan de for loop wordt naar het leven gestaan. En dat door de programmeurs zelf nog wel. Een element dat sinds jaar en dag deel uitmaakt van veel programma code is niet meer gewenst. Na jaren trouwe dienst wordt het ijskoud de deur gewezen. En daar blijft het niet bij, ook het if statement moet het ontgelden. [1] Wat is hier aan de hand? Hebben we hier te maken met een nieuw programmeertijdperk? Of moet het allemaal weer anders dan dat we gewend zijn? Of is het niet meer dan een storm in een glas water? Laten we eens gaan kijken.

De for loop voorbij

In hun blogs beschrijven Matthew Podwysocki [2] en Justin Etheredge [3] het [einde van het] gebruik van de for loop. Kort gezegd komt het er op neer dat de dagen van de klassieke for loop zijn geteld. Het grootste probleem met deze controlestructuur hebben de auteurs met het principe ervan. Het focused te veel op het ‘hoe’ en niet op het ‘wat’. Dus hoe iets gedaan wordt in plaats van wat er nu precies gedaan wordt. Dit is een sta-in-de-weg voor goed onderhoudbare code. Ik geef hun gedachten in mijn eigen woorden weer, aangevuld met mijn eigen visie.

Geen go to

Allereerst is het goed om eerst even terug te blikken in de geschiedenis. Kritische gedachten over controlestructuren zijn niet nieuw. In 1965 was het Edsger W. Dijkstra die het go to statement liefst zag verdwijnen uit de programmacode. In 1968 beschreef hij in zijn artikel “A Case against the GO TO Statement” (EWD215) waarom go to’s beter kon worden vervangen door gestructureerde controle structuren zoals de for- en while loop. Het go to statement is te primitief en nodigt uit om een flinke warboel van je programmacode te maken. [4] En inderdaad, met een go to statement is het voor een programmeur niet moeilijk om een programma te schrijven die, om deze te begrijpen, de moeilijkheidsgraad heeft van een Sudoku niveau 3. En dat doet de leesbaarheid van het programma geen goed. Hieronder is de structuur van het go to statement en van een for loop te zien.

fb1

x.1 Syntax go to statement in C#.


fb2

x.2 Syntax for loop in C#.

In principe is er niets mis met het go to statement. Echter, wanneer men gebruik maakt van meerdere, eventuele geneste go to statements, kan de flow van het programma voor een programmeur aardig bemoeilijkt worden. Er is dan geen draad meer aan vast te knopen hoe het programma zich zal gedragen. We kunnen het zien als een stap in de goede richting om de wat meer betekenisvolle controle structuren hiervoor in de plaats te gebruiken. Java biedt geen ondersteuning voor het go to statement maar in C# is deze nog wel te gebruiken.

In dit kader is het uiterst verbazend om te zien dat met de komst van PHP versie 6 het go to statement nieuw leven in wordt geblazen. In de vorige releases van PHP was deze immers niet aanwezig. In het verslag van de developersmeeting van 2005, gehouden in Parijs, is te lezen dat het go to statement nodig zou zijn omdat het in sommige gevallen de code een flink stuk zou moeten verminderen. [5] Blijkbaar hadden meerdere mensen hier oren naar want met de release van PHP 5.3 is het go to statement al beschikbaar gesteld. [6] We gaan dus terug naar start, met alle gevolgen van dien. De tijd zal leren hoe het uitpakt.

Geen for loop

Maar het kan dus ook de andere kant op. Nu dus ook geen for loop meer. Waarom, en wat wordt er als alternatief geboden?

Een frequente handeling voor een programmeur is het doorlopen van een collectie. Hiervoor wordt over het algemeen een for loop gebruikt met een tellertje om voor elk element in de collectie een operatie (al dan niet complex) uit te voeren.

fb3

x.3 Een for loop met counter in C#.

Omdat vaak ieder element nodig is kunnen we wat korter zijn door de meer zinvolle foreach controle structuur te gebruiken die simpel weg doet wat de naam zegt: elk element afgaan. Het scheelt niet alleen een regel code maar, wat veel belangrijker is, het is nu ook veel duidelijker wat er gebeurt. De code is nu zo beschrijvend, declaratief geworden dat we geen commentaar nodig hebben om te aan te geven wat we nu precies aan het doen zijn.


fb4

x.4 Een foreach loop in C#.

In Java is deze structuur ook aanwezig, de z.g.n. ‘enhanced for loop’ die er iets cryptischer uitziet en pas vanaf Java 5 beschikbaar is.


fb5

x.5 Een enhanced for loop in Java.

Maar, hoe enhanced het ook moge zijn, het is slechts het topje van de ijsberg. We kunnen deze lijn immers doortrekken en nog meer zinvolle constructies bedenken die dichter bij de wereld van de programmeur staan. Want met deze functie zijn we er niet direct. We hebben aan de ene kant een functie die meer beschrijvend is maar die aan de andere kant ook een hoop mogelijkheden uitsluit die we misschien wel graag zouden willen hebben. Hoe kunnen we bijvoorbeeld met stappen van twee door de collectie heen lopen? De foreach alleen is niet in staat om de for loop in zijn geheel te vervangen. Hij zou eigenlijk een hoop broertjes en zusjes moeten krijgen.

Maar hoe kunnen we dit voor elkaar krijgen. Zonder een for loop (of while) wordt het toch aardig lastig om een collectie te doorlopen. Laten we dit eens bekijken a.d.h.v. een voorbeeld. Stel we willen van een gegeven lijst:

  1. het aantal even nummers en;
  2. het gemiddelde van alle oneven nummers

boven water toveren. We zouden dit in de ‘oude’ situatie met twee for loops moeten doen die elk een resultaat voor hun rekening zouden nemen. Het zou ook in één for loop kunnen maar dan werken we ons zelf flink in de nesten omdat de overzichtelijkheid en aanpasbaarheid van de code flink geweld aangedaan wordt. Een andere optie is om eerst eens te kijken wat er nu precies moet gebeuren. We hadden het probleem al opgesplitst in twee deelproblemen:

  1. Geef het aantal even nummers uit de lijst;
  2. Geef het gemiddelde van alle oneven nummers uit de lijst.

Wanneer we deze deelproblemen verder uit elkaar rafelen, zoals we ze zouden implementeren, komt er een interessante herhaling boven water drijven:

  1. Geef het aantal even nummers uit de lijst;
    1. Filter de collectie op even nummers;
    2. Tel het aantal even nummers.
  2. Geef het gemiddelde van alle oneven nummers uit de lijst.
    1. Filter de collectie op oneven nummers;
    2. Tel het aantal oneven nummers bij elkaar op;
    3. Tel het aantal oneven nummers;
    4. Deel b door c.

Abstracter kunnen we het weergeven als:

  1. Filter een collectie a.d.h.v. criteria;
  2. Pas een operatie toe op het resultaat.

Met deze kennis kunnen we een interessante stap zetten. Als we nu eerst en generieke filterfunctie maken dan hoeven we die maar één keer te definiëren en kunnen we deze twee keer toepassen. Hieronder is een dergelijke functie te zien in C# die een (IEnumerable<T>)-lijst kan filteren.


fb6

x.6 Definitie van een ‘generic’ filter functie in C#. [7]

Gewapend met onze nieuwe functie kunnen we de strijd aangaan. Laten we eerst de ‘oude’ aanpak bekijken.


fb7

x.7 Twee for loops om een collectie te filteren en een operatie uit voeren op het resultaat in C#.

Het gedeelte waarin we onszelf herhalen in geel gearceerd. We halen twee keer hetzelfde truckje uit en dat is niet echt DRY. [8] Nu onze ‘nieuwe’ aanpak met de filterfunctie. Hiervoor gebruiken we een collectie die de IEnumerable interface implementeert zodat we onze filter functie kunnen gebruiken.


fb8

x.8 De generic filter functie met een IEnumerable collectie filtert en operaties uitvoert op het resultaat C#.

De code ziet er nu veel meer gestructureerd en beschrijvend uit. Je kunt zo lezen wat het doet. Dit komt omdat we het hoe hebben weggeabstraheerd door de for / foreach loop in de generieke functie te stoppen. En daardoor hebben we de meer zinvolle filterfunctie, het wat, ervoor teruggekregen. Belangrijk is ook dat beschrijvend niet gelijk is aan het zo kort mogelijk opschrijven van code. Het bovenstaande voorbeeld had ook in drie regels gekund maar met het gebruik van de tussenvariabelen even en odd is de code een stuk beter leesbaar. De grote flexibiliteit van onze ‘nieuwe’ manier wordt duidelijk als we gaan refactoren en we besluiten dat we in plaats van het gemiddelde van alle oneven nummers, het grootste getal willen weten van alle getallen tussen de 10 en de 36 in de collectie. Uiteraard willen we de wijziging altijd kunnen terugdraaien. In de oude situatie ziet dat er ongeveer zo uit:

fb9

x.7 Refactoring met twee for loops in C#.

Het wordt een lastig gepriegel en het is beter om hiervoor gewoon [weer] een nieuwe for loop te schrijven. Dit slagveld kunnen we in onze nieuwe manier van aanpak voorkomen. We hoeven enkel een nieuwe filter te definiëren en uit te voeren en hierop een berekening op toe te passen. Dit alles netjes gescheiden.


fb10

x.8 Refactoring met de ‘generic’ functie in C#.

Een ander bijkomstig voordeel is dat we onze filterfunctie op een plaats in de code hebben gedefinieerd. De kans op fouten is erg klein en mocht er een fout in zitten dan weten we waar we die kunnen vinden.

In Java is deze structuur niet mogelijk omdat de taal zich veel strikter gedraagt t.o.v. C#. De volgende implementatie zou er het dichts bij in de buurt moeten komen maar echt duidelijker en beschrijvender wordt de code er niet op.

fb11

x.9 Benadering van generieke filterfunctie in Java. [9]

fb12

x.10 Toepassen van generieke filterfunctie in Java.

In Java ziet de code er wat meer verbose uit dan in C#. Hoewel we dezelfde voordelen hebben m.b.t. refactoring is de code een stuk minder beschrijvend. Dit komt doordat we, met het gebruik van de anonieme klasse, het hoe niet helemaal weg hebben kunnen werken en daarmee een belangrijke laag van abstractie missen.

Voors …

Wanneer we beide talen, C# en Java, tegenover elkaar zetten kunnen we concluderen dat we in beide gevallen van deze manier van werken kunnen profiteren, al is de oplossing in C# beter leesbaar dan in Java. We kunnen stellen dat de filter functie is voor ons een krachtig hulpmiddel / tool geworden is. En hiermee kunnen we de lijn verder doortrekken. Willen we bijvoorbeeld de verschillende items in twee collecties weten dan is het beter om eerst een generieke compare functie te schrijven die twee collecties vergelijkt om vervolgens die te gaan gebruiken binnen een bepaalde context. Of, welke (unieke) elementen bevat een collectie eigenlijk? Schrijf dan eerst een generieke distinct funtie. Willen we op ieder element van een collectie een operatie uitvoeren? Schrijf een map functie. Enz. enz.. Ook orthogonaliteit is iets wat zeer hoog in het vaandel zou moeten staan. In voorbeeld x.9 hebben we de generic functie gedefinieerd op de interface IEnumberable die door veel collecties in de .NET bibliotheek wordt geïmplementeerd. Hiermee kunnen we dus met onze Filter functie verschillende collecties doorlopen. En zo kunnen we dus met een minimale set aan concepten een grote variëteit aan mogelijkheden realiseren.

… en tegens

Het volgende is het overwegen waard:

  1. Wanneer iedere programmeur voor zijn klasse zijn eigen functies (op zijn eigen manier) gaat definiëren is de kans groot dat de code binnen een project inconsistent wordt. En dat komt refactoring niet ten goede. Een [minder] mooi voorbeeld daarvan is de wispelturige keuze van het gebruik van parameters in PHP functies; [10]
  2. Ook zijn we op deze manier niet voor iedereen begrijpend bezig. De definitie van de static/abstract filter methode zal niet voor iedereen direct duidelijk zijn. Als het goed is hoeft deze methode niet aangepast te worden, alleen de calls ernaar zullen wijzigen maar toch. Aan de andere kant blinkt een anonieme klasse definitie van Java ook niet uit in helderheid.

DSL met LINQ en Quaere

In dit kader kunnen we natuurlijk niet om LINQ heen en daarom wil ik hier heel kort op in gaan. Met LINQ (.NET Language-Integrated Query) heeft Microsoft query faciliteiten voor algemene doeleinden aan de .NET Framework toegevoegd. LINQ is een uitblinker in het definiëren van wat de applicatie moet doen i.p.v. hoe het resultaat bereikt moet worden al is het wel wat meer SQL-achtig. De standaard query operators kunnen toegepast worden op iedere informatie bron die de IEnumerable<T> interface implementeert. Het voorbeeld in x.8 zouden we met LINQ nog korter kunnen schrijven.

fb13

x.11 Gebruik van LINQ om het aantal even nummers uit een collectie op te tellen in C#

Java kent in principe geen LINQ-achtige syntaxis maar met het opensource project Quarere wordt als LINQ voor Java aangeprezen. [11] Al wordt in dezelfde bron verwezen naar het feit dat met joSQL hetzelfde resultaat behaald kan worden. Hier en daar wordt geopperd dat deze functionaliteit met Closures in Java 7 wel beschikbaar zou moeten zijn maar dan praten we wel over 2010 of later. [12] Voorlopig dient Quarere dus handmatig aan een Java project te worden toegevoegd en is het niet zeker niet de facto standaard. Hoewel er dus initiatieven zijn, is er voor Java vooralsnog niets concreets voor handen.

F# (.NET)

F# is een multi-paradigma programmeertaal van Microsoft die voornamelijk het functionele paradigma implementeert. In F# worden dergelijke functies al ondersteund door de taal zelf en kunnen we zonder truckjes te gebruiken onze taak volbrengen op een duidelijk beschrijvende manier. Ook programmeurs die niet bekent zijn met het functionele paradigma moeten in staat zijn om onderstaande code te interpreteren en te begrijpen.

fb14

x.12 Gebruik van filter functie in F#

Scala (Java)

Wat F# voor .NET is zo zou men Scala kunnen beschouwen voor het Java platform. De overeenkomsten zijn treffend. Ten eerste lijken beide talen te zijn ontstaan na een implementatie van generics. Don Syme heeft eerst generics voor .NET geimplementeerd alvorens aan F# te beginnen en Martin Odersky ontwikkelde zijn succesvolle Generics voor Java voor hij aan Scala begon. Ten tweede kennen beide producten een naadloze integratie met het framework waar ze op draaien. Ten derde wordt van beide technieken vermeld dat ze de omvang van programmacode aanzienlijk kunnen reduceren en programmeren zelfs weer leuk kunnen maken – alsof dat het nog niet was! [13] Verschillen zijn er ook. Het belangrijkste is dat F# voornamelijk functioneel is het Scala voornamelijk objectgeoriënteerd. Helaas heb ik zelf niet de tijd om me in Scala te verdiepen en een voorbeeld te laten zien. Maar misschien kan een software-innovator ons iets laten zien?

Conclusie

Een stap verder in programmeren zou een stap over de for loop heen zijn naar meer betekenisvolle controlestructuren. Zeker wanneer we data collecties moeten doorlopen. Overige iteraties zouden recursief opgelost kunnen worden maar dat brengt weer zijn eigen specifieke problemen met zich mee. Wanneer we dit willen realiseren in Java zullen we voorlopig nog even wat geduld moeten hebben. Maar misschien dat Scala hier uitkomst kan bieden. In C# is het wel mogelijk al is de oplossing ook niet helemaal netjes en bestaan er diverse gevaren. Met het gebruik van een DSL, zoals LINQ, komen al een heel eind in de goede richting. In Java zou Quarere de oplossing moeten bieden maar het is de laatste tijd erg stil rondom dit onderwerp. Uiteindelijk is het beter om te kiezen voor een taal die deze structuren in zich heeft en voor de .NET omgeving is F# dan een prima keuze.


[1] Er is een heuse anti-if campagne opgestart met als slogan ‘less IF’s, more power’ waarbij programmeurs blijk kunnen geven van hun gelijkgezindheid door zich aan de lijst toe te voegen. Zie http://www.antiifcampaign.com.

[4] Zie de wiki: Edsger W. Dijkstra http://en.wikipedia.org/wiki/Edsger_W._Dijkstra.

[5] Zie Minutes PHP Developers Meeting onder paragraaf 4.2, http://www.php.net/~derick/meeting-notes.html.

[8] Zie wiki: Don’t repeat yourself http://en.wikipedia.org/wiki/Don’t_repeat_yourself.

[9] Met dank aan Erik Mulder.

[10] In PHP kan de logische volgorde van parameters verschillen binnen functies. Zie de volgende voorbeelden voor een verwarrende keuze van de parameters needle en haystack binnen twee functies:

string stristr ( string $haystack , mixed $needle [, bool $before_needle= false ] )

bool in_array ( mixed $needle , array $haystack [, bool $strict ] )

[11] Zie Quaere: LINQ Arrives for Java http://www.infoq.com/news/2007/09/quaere-linq.

[13] The Scala Programming Language http://www.scala-lang.org/node/25


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development, Java (EE), .Net, Overige talen en platformen


Reacties (3)

  • Voor die filter in Java zou je volgens mij ook gewoon een custom Iterator kunnen gebruiken, alleen wordt het dan wel redelijk verbose.

    Geplaatst op 31 juli 2009 om 10:39 Permalink

  • Jos Warmer schreef:

    Andries,

    Heb je wel eens naar OCL gekeken, ook een taal waarin je geen traditionele for loop nodig hebt. Overigens kent SmallTalk ook goede voorbeelden van collectie operaties die de for loop overbodig maken.

    Geplaatst op 28 juli 2009 om 10:40 Permalink

  • Jakob Ooms schreef:

    Hallo,

    Leuk artikel!
    Door middel van 2 extension methods op int kan je de C# code zelfs nog explicieter schrijven:

    var numberOfEven = collection.Count(x=>x.IsEven());
    var averageOdd = collection.Average(x=>x.IsOdd());

    PS: Jammer dat er geen extension properties zijn ;)

    Geplaatst op 25 juli 2009 om 9:57 Permalink