Doe mij maar een statisch getypeerde taal

…en ik zal je vertellen waarom. Ik ben gisteren drieëneenhalf uur tijd kwijtgeraakt doordat de schrijver van de Python-API waar ik tegenaanpraat (een relatief onbekend bedrijfje met de naam Google) een kleine inconsequentie bleek te hebben ingebouwd, iets dat met een statisch getypeerde taal binnen een minuut duidelijk was geworden.

Er bestaan verschillende meningen over wat nou precies een ‘dynamische’ taal is, maar voor deze blog bedoel ik ermee dat variabelen, methode-parameters, en ook je member fields ongetypeerd zijn. Je kan er dus altijd willekeurige waarden instoppen, en in de loop van een enkele methode kan jouw myobject.myfield ook bijvoorbeeld eerst een string, dan een integer, en dan een Employee-object bevatten. Ongetwijfeld handig…???

Dat de formele parameters van methoden ook ongetypeerd zijn, betekent dus ook dat je van de documentatie van de API afhankelijk bent om te weten wat voor objecten je erin moet stoppen. Die is niet altijd even specifiek: toen een methode volgens de documentatie ergens een URL-parameter verwachtte, gebruikte ik braaf atom.url.parse_url(url_string) (er werd wel meer gebruik gemaakt van atom) – om vervolgens een exceptie om mijn oren te krijgen. Ik had namelijk gewoon url_string zelf moeten meegeven. Duidelijk!

Dus ik was in zekere zin al gewaarschuwd. Maar toch. Je verwacht het niet: als je een service-wrapper hebt met de methoden InsertEvent(event) en UpdateEvent(event), en de documentatie zegt van beide dat er een event in moet, dan is het natuurlijk vanzelfsprekend uiteraard logisch dat je voor de een moet zeggen

event.Title = atom.Title(text='bla')

en voor de andere

event.Title = atom.data.Title(text='bla')

omdat je als je dezelfde keuze bij beide aanhoudt, je altijd bij een van de twee een exceptie voor je kiezen krijgt. En oja, bij InsertEvent moet er stiekum een lijst events in om het te laten werken, maar dat vertellen we even niet.

Een halve werkdag naar de knoppen terwijl, zoals gezegd, met een fatsoenlijk getypeerde taal dit soort problemen binnen een minuut boven water komen. Grmbl.

Bij een ongetypeerde taal als Python heb je namelijk geen enkele controle over wat voor data er bij je methode binnenkomt. Je kan dus alleen maar hopen dat die data de juiste methoden bevat om jouw methode zijn werk te kunnen laten doen. En andersom kan je die data wel in een andere methode inschieten, maar je moet maar hopen dat die op zijn beurt geen onverwachte methoden op jouw data aanroept. En dat wordt lastig te analyseren als je op een gegeven moment 6, 7 lagen dieper de stack in bent gedoken.

Een typesysteem daarentegen garandeert je dat de binnenkomende data een bepaalde vorm heeft en je er dus bepaalde dingen mee kan doen. En andersom ook dat als je die data meegeeft aan een andere methode, hij ook de juiste structuur heeft om die andere methode goed zijn werk te kunnen laten doen. Bijvoorbeeld, door aan te geven dat je een String verwacht in plaats van een Url, voorkom je dat argeloze gebruikers zoals ik worden geconfronteerd met dat een Url geen length()-methode heeft. Of erger, dus.

Het typesysteem is daarmee een stuk documentatie geworden, en doordat het door de computer wordt afgedwongen weet je altijd zeker dat het (a) volledig en (b) up-to-date is. Ik ben er altijd een groot voorstander van dat de computer je zoveel mogelijk helpt bij het programmeren. Dit soort boekhouding is daar zeker een voorbeeld van.

Ik heb ooit de bewering gehoord dat eigenlijk alles wat met een typesysteem kan, ook met een goeie set (unit) tests kan. Maar dat geloof ik niet. Ten eerste is het perspectief anders: een unit test is erop gericht om te controleren of je software intern goed werkt, maar geeft geen signaal naar de buitenwereld van hoe het gebruikt moet worden. Je kan ermee wel controleren dat als je een String binnenkrijgt, de methode goed gaat, en als je een Url binnenkrijgt, het (evt. op een specifieke manier) fout gaat. Maar je kan niet afdwingen dat je altijd een String binnenkrijgt, omdat je je aanroepers niet onder controle hebt. Een typesysteem doet dat wel.

Ten tweede kan je je afvragen hoe handig het is om, in plaats van te weten dat je een Employee-object in handen hebt, bij elke methode-aanroep die je wilt doen te controleren of het ding wat je in handen hebt wel een getAfdeling()-methode heeft, en of het resultaat daarvan wel een getPostAdres()-methode heeft, enzovoort. En dan is het hopen dat je object niet zijn state verandert dankzij het testen van die aanroepen.

En daarbij moet je ook nog maar eens weten welke aanroepen er gedaan zullen worden; iets wat redelijk lastig is als je een externe API gebruikt. Bovendien breekt het de abstractie van het alleen moeten weten wat een methode doet i.p.v. hoe hij dat doet.

Natuurlijk, het is een redelijke blunder dat de ene methode verwacht dat de fields van zijn binnenkomende data bestaan uit objecten uit de ene module (namespace) terwijl de andere verwacht dat ze uit een andere module komen. Met een statisch getypeerde taal was dat misschien ook nog wel mogelijk geweest, maar dan had de schrijver zich toch wel in wat meer bochten moeten wringen om dat voor elkaar te krijgen.

Uiteraard is een statisch getypeerde taal ook niet alleszaligmakend. Het feit dat je fields ondanks statische typering toch onzindata (zoals null) mogen bevatten is daar al een voorbeeld van (daarover ooit nog een andere blogpost). En het is wat meer werk omdat je de types hier en daar moet aangeven. Maar in elk geval vang je er wel een bepaalde klasse problemen mee op.