Wat is Duck Typing en hoe kun je het toepassen in F#?
Laten we vertrekken vanaf bekend terrein in de OO wereld. Stel we hebben twee classes, van type “Rechthoek” en type “Cirkel”, en beide hebben de member-functie “TekenOpScherm()”. Om ze klaar te maken voor generieke toepassing, laat je ze beiden de interface “IVorm” implementeren, met functie “TekenOpScherm()”.
Vervolgens bouwen we een class “Tekening”, die een lijst bijhoudt van type “IVorm”. Deze class implementeert de functie “TekenAlles()” en die loopt door de lijst om voor ieder object-instantie, de functie “TekenOpScherm()” aan te roepen.
Tot dusver het bekende OO patroon.
Het wordt iets lastiger als je zelf geen source-eigenaar bent van de classes die je wilt gebruiken. Stel je wilt de generieke functie “sum” schrijven, die door een lijst van een onbepaald type heenloopt, de inhoud optelt en het resultaat teruggeeft.
De eerste barrière voor “sum” wordt gevormd door het feit dat in F#, de types int en float niet vrij uitwisselbaar zijn. Maar om het nog een stukje ingewikkelder te maken, wat als we nu eens de “+” operator van “TimeSpan” willen gebruiken, zodat we hiermee de duur van periodes bij elkaar op kunnen tellen. Hoe gaan we dan één generieke functie “sum” schrijven, die al deze types aankan?
Het patroon van hierboven met “IVorm” kunnen we niet volgen, want we hebben geen invloed op de overerving van int, float en TimeSpan. Als we die invloed wél hadden gehad, dan hadden we een interface gemaakt met de naam “IHeeftPlusOperator”. Dat is een lelijke naam, want deze naam heeft geen enkele relatie met iets in de echte wereld; wat “IVorm” wel heeft. Je ziet eigenlijk al aan die lelijke naam, dat het een work-around is, vanwege een technisch obstakel.
Duck Typing biedt voor dit soort gevallen een oplossing. Deze naam komt van het Amerikaanse gezegde “If it walks like a duck, quacks like a duck, it is a duck”.
Op basis van de waargenomen eigenschappen vellen we een oordeel. Wat willen we nu met onze functie “sum”? We willen een type ontvangen die de “+” operator ondersteunt. Als dit het geval is, dan is dat type geschikt voor onze functie. Alle overige eigenschappen zijn verder niet interessant.
F# biedt de mogelijkheid om generieke types en functies te implementeren, waarbij je eisen stelt aan het generieke type. In C# bestaat dit ook, maar in C# zijn de mogelijkheden beperkt tot eisen op class-niveau.
In F# kun je eisen dat een type een bepaalde (static) methode implementeert, of een bepaalde property heeft, maar je kunt ook dezelfde class-niveau eisen stellen zoals die in C# bestaan.
Duck Typing wordt vooral mogelijk gemaakt via de property en method constraints. Door “inline” functies te gebruiken, worden je Duck Types ook nog eens tijdens het compileren gecontroleerd.
Wat wil je nog meer? Zelf heb ik nu wel trek in een peking eend...
Overigens, List.sum bestaat al. Maar het is wel een mooi voorbeeld om in een artikel als deze te gebruiken.
Waar Duck Typing wel heel goed voor gebruikt kan worden is bijvoorbeeld om in FSharp een XML document te parsen. De types XElement en XAttribute hebben beide de property Name, maar hebben verder niets gemeen. De volgende functie haalt de stringnaam op, uit een instantie van XElement of XAttribute:
(^a : (member Name : XName ) o)
Overigens, Duck Typing heeft een merkwaardige, ietwat onaangename syntax. Gelukkig levert het wel veel op.