Iedere softwareontwikkelaar heeft te maken met foutmeldingen. Meestal zijn deze volstrekt onbegrijpelijk. Bastiaan Heeren promoveerde onlangs aan de Universiteit Utrecht op een reeks aanbevelingen voor verbetering. De onderwijsinstelling past die aanbevelingen reeds met succes toe in het programmeeronderwijs.
Zelfs wanneer een programma tijdens het schrijven uitvoerig wordt getest, is het vrijwel onmogelijk om alle fouten aan het licht te brengen. Daarom kan een aanvullende methode wordt toegepast: statische analyse. Deze controle vindt plaats zonder dat het programma zelf uitgevoerd hoeft te worden. Dat maakt een snelle terugkoppeling naar de programmeur mogelijk, zodat hij eventuele fouten direct kan herstellen. Een ander voordeel is dat het optreden van bepaalde fouten of foutsituaties categorisch valt uit te sluiten. Dat komt de kwaliteit van de programmatuur duidelijk ten goede.
De presentatie
Een moderne compiler kent nogal wat voorzieningen om in een vroegtijdig stadium fouten op te sporen. Aan de presentatie van foutmeldingen besteden compilerbouwers veel minder aandacht. Daardoor kunnen gemakkelijk meldingen op het scherm verschijnen als:
ERROR “C.hs”:6 – Type error in application
*** Expression : semLam <$ pKey "\" <*> pFoldr1 (semCons,semNil) pVarid <*>
pKey “->”
*** Term : semLam <$ pKey "\" <*> pFoldr1 (semCons,semNil) pVarid
*** Type : [Token] -> [((Type -> Int -> [([Char],(Type,Int,Int))] ->
Int -> Int -> [(Int,(Bool,Int))] -> (Doc,Type,a,b,[c] -> [Level],[S] -> [S])) ->
Type -> d -> [([Char],(Type,Int,Int))] -> Int -> Int -> e -> (Doc,Type,a,b,f ->
f,[S] -> [S]),[Token])
*** Does not match : [Token] -> [([Char] -> Type -> d ->
[([Char],(Type,Int,Int))] -> Int -> Int -> e -> (Doc,Type,a,b,f -> f,[S] ->
[S]),[Token])
Ook voor een ervaren softwareontwikkelaar is dat een nogal cryptische melding. Het toevoegen van duidelijke foutmeldingen aan een compiler is niet alleen veel werk, het is ook afhankelijk van de programmeerstijl en de deskundigheid van de programmeur. Zoals het voorbeeld aangeeft, is het dikwijls onduidelijk waarom een programma als incorrect wordt beschouwd, laat staan dat uit de melding snel en gemakkelijk informatie valt af te leiden over hoe de gemaakte fout te herstellen is.
Welke programma-analyses toepasbaar zijn, hangt vooral af van de gebruikte programmeertaal. Functionele talen als ML of Haskell beschrijven een programma als een groep van wiskundige functies. Kenmerkend voor dit soort talen is dat zij impliciet getypeerd zijn, waarbij een type de waarden die een expressie kan aannemen beschrijft. Denk aan ‘een getal’, ‘een lijst met getallen’, ‘een paar van twee karakters’ en dergelijke. Met andere woorden, de programmeur hoeft niet zelf voor iedere expressie het type op te schrijven, maar kan vertrouwen op het mechanisme dat automatisch de types afleidt. Dat noemen we ‘infereren’. Bij het infereren van de ontbrekende types kunnen inconsistenties worden ontdekt. Die worden vervolgens aan de programmeur gerapporteerd.
Meerdere oplossingsmethoden
Het werken met typeringsfoutmeldingen is niet eenvoudig. Ervaren programmeurs hebben er wellicht niet zo veel moeite mee, maar beginners wel. Zij hebben vaak het gevoel dat het ’type-inferentieproces’ hen tegenwerkt, doordat wel een fout wordt aangegeven, maar niet voldoende duidelijk wordt wat de fout is en hoe deze te herstellen valt. Het proefschrift van Heeren beschrijft een aantal mogelijkheden om bij het gebruik van Haskell hierin verbetering te brengen.
Een van de problemen met typeringsfoutmeldingen is dat het niet verplicht is alle types op te schrijven. Hierdoor kan een ‘mismatch’ ontstaan tussen de types die de programmeur verwacht en de types die de compiler afleidt. Bovendien ondersteunt Haskell hogere-orde functies (functies die als argument aan een functie worden meegegeven) en polymorfie (functies die gebruikt worden met verschillende types). Beide bemoeilijken het mechanisme van type-inferentie.
Traditionele inferentiealgoritmen gaan nogal mechanisch te werk. Heeren stelt een andere aanpak voor: bekijk het programma in zijn geheel, zoals een deskundige dat zou doen. Doe een globale analyse waarbij veel gemaakte fouten worden herkend. Presenteer die vervolgens op een inzichtelijke manier aan de programmeur. De analyse (type-inferentie) wordt daarbij geformuleerd als een constraintprobleem. Dit is een standaardtechniek voor (automatische) programma-analyse, al wordt hij niet altijd toegepast voor type-inferentie. Verder zijn heuristieken ontwikkeld die op een constraintverzameling werken en veel gemaakte fouten detecteren. In deze heuristieken zit als het ware de kennis van een deskundige. Een constraintverzameling beschrijft nauwkeurig de relaties tussen de types in een programma. Het levert verder een splitsing op van de specificatie van de analyse (constraints verzamelen) en de implementatie (constraints oplossen). Door deze scheiding kunnen meerdere oplossingsmethoden naast elkaar bestaan. Hiervoor is het Top Framework (typing our programs) ontwikkeld, waarin meerdere oplossingsmethoden zijn ondergebracht.
Raamwerk
Het Top Framework maakt het mogelijk een compiler op eenvoudige wijze uit te breiden met type-inferentie. Daarbij valt het inferentieproces af te stemmen op de wensen en de deskundigheid van de programmeur. Iedere in Top gebruikte constraint draagt zijn eigen informatie met zich mee. Deze informatie beschrijft onder meer waarom de constraint is gegenereerd en waar hij in de programmacode veroorzaakt is. Verder kan de informatie een heuristiek ofwel oplossingsmethode aansturen, of een foutmelding presenteren.
Interessant is verder dat het raamwerk nog een fase kent: het ordenen van de verzamelde constraints voordat deze worden opgelost. Hoewel de volgorde waarin de afhandeling van constraints plaatsvindt volgens Heeren niet relevant is voor de oplossing die voor een verzameling wordt gevonden, beïnvloedt het wel in sterke mate het moment waarop een inconsistentie wordt ontdekt en welke fout vervolgens wordt gerapporteerd. Dat gaat vooral op voor situaties waarbij constraints één voor één worden bekeken. Bij een globalere oplossingsstrategie geldt dat minder.
Top kent verder een scriptingtaal waarmee foutmeldingen te beïnvloeden zijn. Dat is volgens Heeren vooral handig bij gebruik van domeinspecifieke programmeertalen. Zonder dat aanpassing van de compiler nodig is, zijn foutmeldingen uit te drukken in terminologie die past bij het domein.
Overigens is aan de Universiteit Utrecht een eigen compiler ontwikkeld, Helium, die gebruikmaakt van de technologieën uit het Top Framework. Helium genereert die precieze en gebruikersvriendelijke foutmeldingen. De universiteit past deze compiler inmiddels toe in de introductiecursus ‘Functioneel programmeren’. Helium ondersteunt vrijwel de gehele Haskell 98-standaard. Daaruit blijkt, stelt Heeren ten slotte vast, dat het Top Framework echt toepasbaar is op een volwassen programmeertaal.