Ik vind dat functionele talen een bewondering zijn voor zijn eigen geïdealiseerde zelf. Hun streven naar wiskundige (lambda-calculus) zuiverheid vergeet dat systemen in de echte wereld draaien en niet in theorie. Ja, voor academici en oprukkende compilers, ze zijn geweldig waar geïdealiseerde theorie het streven is. Hun idealisme verwaarloost echter de beperkingen en realiteiten van computers voor commerciële ontwikkeling. Waarom worden we beschaamd omdat we bijwerkingen en muterende objecten willen toestaan? Vooral wanneer functioneel programmeren de objectoriëntatie nog verder beperkt.

 

Beperkingen

Om mijn punt van beperkingen binnen computers te illustreren, laten we het typische functionele programmeervoorbeeld gebruiken voor het berekenen van een bepaalde waarde voor de Fibonacci-reeks.

Voor imperatieve (objectgeoriënteerde) programmering ziet de code er als volgt uit:

Voor Functional Programming, zo:

De functionele code is nu veel beknopter. De imperatieve code duurt een beetje te lezen en heeft meer variabelen om te overwegen. Plus, de iteratieve code doet dat muteren van waarde dat functioneel programmeren zwaar tegen is.

Functioneel programmeren lijkt de winnaar te zijn. Het is gemakkelijker te lezen en met minder code, veel minder waarschijnlijk om codeerfouten op te lopen. Jazeker, laten we al onze imperatieve code verwijderen en dingen functioneel gaan doen! OK, dit is een kleine testcase om een ​​dergelijke beslissing op te baseren. Het internet staat echter vol met voorbeelden van dit gemak van functionele programmering (geduwd door raamwerken, zoals Spring Reactive).

Ik moet echter vragen of de functionele code echt beter is dan de imperatieve code? Hebben ze allebei het juiste antwoord de hele tijd?

Laten we wat testen:

term Imperative Functional
0 0 0
1 1 1
2 1 1
3 2 2

Ja, alles ziet er tot nu toe goed uit.

Laten we nu enkele randvoorwaarden testen. Beide hebben geen negatieve voorwaarden. Dus laten we wat grotere termen proberen:

term Imperative Functional
10,000 3364… (full numbers removed for brevity) 3364…
15,000 2531… StackOverFlowError
1,000,000 (wait a while then) 1953… StackOverFlowError

 

Oh, die mooie leesbare functionele code kan geen termen groter dan ongeveer 10.000 berekenen (voor een standaard Java8 JVM). Ja, sommige functionele talen hebben zaken als tail call optimalisaties om dit probleem te voorkomen. Dit benadrukt echter problemen met geïdealiseerde attributen van functioneel programmeren, zoals onveranderlijkheid.

De functionele code heeft eigenlijk twee problemen die de imperatieve code vermijdt.

 

Opgave 1: Recursie kan snel de discussiestapel vullen

Het eerste probleem is dat elke functieaanroep op de threadstack wordt geladen. Naarmate meer en meer functies recursief worden opgeroepen, wordt de thread stack groter. Omdat thread-stacks beperkt moeten blijven, heeft de thread-stack op een gegeven moment onvoldoende geheugen. Op dit punt krijg je een StackOverFlowError (niet het antwoord dat je zou verwachten in een geïdealiseerd beeld van de wereld).

De functionele code is alles recursief. Hoe hoger de term, hoe meer geheugen er nodig is op de threadstack om de diepte van recursieve functieaanroepen te verwerken.

De imperatieve code vermijdt dit probleem door gewoon afor loop te draaien. OK, de for-lusteller is gemuteerd en de variabelen van de methode blijven veranderen, maar de threadstack blijft tijdens de uitvoering constant in grootte.

Laten we de draadstapelgrootte tot een zeer grote waarde verhogen. Nou, je loopt het volgende probleem in met recursieve functionele code.

 

Probleem twee: Recursie geeft geen referenties vrij

Op een gegeven moment zullen beide uitvoeringen onvoldoende geheugen hebben. De heap-ruimte voor de BigInteger-objecten wordt overschreden en er wordt een OutOfMemoryError gegenereerd.

De imperatieve code wijzigt de variabelen van de methode om te verwijzen naar de nieuwe Biggetget-objecten die zijn gemaakt. Hierdoor kunnen de oude, niet langer gerefereerde BigInteger-objecten als rommel worden verzameld.

De functionele code blijft echter de variabelenreferenties laden op de threadstack terwijl deze recursief wordt uitgevoerd. Dit betekent dat de functionele code verwijst naar elke BigInteger die het creëert totdat het antwoord is bereikt. De functionele code staat afvalverzameling van oudere BigInteger-objecten niet toe en vult vervolgens het geheugen veel sneller op.

Nogmaals, de imperatieve code zal in staat zijn om de juiste resultaten te produceren voor veel grotere termen.

 

Mutatie is noodzakelijk

Oké, dus wie vraagt ​​er ooit om de miljoenste term van de Fibonacci-reeks?

Meestal genereren we webpagina’s, REST-payloads en andere veel kleinere berekeningen.

Laat de hardware verder gaan met het beperking probleem. Computers worden geavanceerder en hebben aanzienlijk meer geheugen beschikbaar. We kunnen het probleem gewoon naar hardware pushen, zoals we altijd doen wanneer we moeten opschalen. Met de vooruitgang in het computergeheugen en de verbeterde CPU-prestaties zijn sommige dingen echter nog steeds eindig.

De pixels op het scherm waarop u dit artikel leest, zijn eindig. Functioneel programmeren zou voorschrijven dat ik een nieuwe set onveranderlijke pixels krijg voor elke update van het scherm. Stel je dat voor – onveranderlijke schermen. Elke keer dat ik iets nieuws wil laten zien, heb ik een nieuwe monitor nodig. Bij 60Hz zal mijn bureau behoorlijk vol raken … sorry (sorry ju..st cle..arin..g mijn de..sk tot cont..inue writ..ing th..is). Jazeker, die onveranderlijke idealisering werkt goed in onbegrensde theoretische wiskunde, maar niet in de begrensde computer.

Oh, sorry. We hebben geen onveranderlijke schermen; we hebben onveranderlijke framebuffers. Framebuffers zijn buffers in RAM die bitmaps bevatten voor videostuurprogramma’s om te gebruiken bij weergave op het scherm. CPU’s schrijven naar hen wat moet worden weergegeven en videostuurprogramma’s moeten van deze worden gelezen om naar het scherm te worden weergegeven.

Nou ja, eigenlijk, nee. We hebben geen onveranderlijke framebuffers. De kosten van afval verzamelen van al deze gecreëerde framebuffers zou buitensporig zijn. Meestal maken we een (mogelijk twee) framebuffers en muteren ze.

Functioneel programmeren vereist mutatie als het iets wil weergeven op het scherm.

Welnu, het is server-side codering dat functioneel programmeren uitblinkt.

Dit bufferprobleem is ook aanwezig op de netwerkkaarten. Er is slechts zoveel bufferruimte beschikbaar, en het vereist gemuteerd zijn om op de draad te komen voor dat REST-antwoord.

In wezen vereist functioneel programmeren mutatie om meer te doen dan om geheugen in een computer te creëren. Als functioneel programmeren van elke waarde wil zijn door het weer te geven aan het scherm of interactie met andere systemen, vereist het een mutatie. OK, de codering door de ontwikkelaar ziet dit niet. De mutatie is er echter binnen computers die de zuiverheid van functionele concepten beperken.

Eigenlijk begint hier de relatie tussen imperatief en functioneel programmeren duidelijk te worden.

 

Geheugen beperkingen

Om de relatie tussen imperatief en functioneel programmeren te zien, moeten we kijken naar geheugenstructuren die worden gebruikt in computers en programmeertalen. De volgende tabel gaat van minst beperkend naar meest beperkend. Het laat ook zien hoe elk niveau het vorige niveau verder beperkt.

Memory Level Restriction on previous level Description
Hardware The physical RAM, CPU caches, swap space, etc.
Process Space Addressable finite amount of memory for process The operating system provides memory address abstractions that allow a process to reference its available memory.
Buffers Process memory is requested from the operating system as a buffer. This allows direct bit manipulation of memory. The addressable space is much larger than the available RAM/swap space/etc. of the computer. Hence, the operating system allocates the memory only on demand/request. Typical programming at this level is for HTTP network communication, screen displays, and other low level functionality.
Structs A Struct can be thought of as a buffer that organizes the bits into higher-level fields (e.g. integers, bytes, longs, floats, chars, address references, etc.) Structs enable imperative programming to pass by reference.
Classes Makes the Struct fields private by default, with methods controlling accessing/mutating the fields Classes are the foundation of object-oriented programming.
Immutable Objects Disallows mutating the fields. Immutable objects are the foundation of pure functional programming.

 

Vanuit het oogpunt van geheugen kan functioneel programmeren dus als beperktere programmering worden beschouwd dan imperatief / objectgeoriënteerd programmeren (en buffer, besturingssysteemprogrammering daarvoor).

Deze beperking van wat de ontwikkelaar kan doen staat meer voorspelling door de compiler toe om de code te optimaliseren. Omdat objecten niet kunnen worden gemuteerd, worden functies voorspelbaarder. Voor dezelfde invoerobjecten kan de compiler cachen (memoization) – het resultaat om dure herberekeningen te voorkomen voor een prestatieverbetering. De volgorde van berekening van de functies kan worden gewijzigd (currying) om de uitvoering beter te optimaliseren en mogelijk uitvoeringsfuncties te voorkomen (luie evaluatie) – plus een groot aantal andere op compilers gerichte optimalisaties. Ik zou verwachten dat de meeste ontwikkelaars deze onderwerpen om 2 uur ‘s morgens willen vermijden om die deadline te halen of om die productie-bug opgelost te krijgen.

Eigenlijk kunnen we misschien bedenken dat computing uiteindelijk datastructuren biedt ter ondersteuning van de lambda-calculus-theorie die in de jaren dertig van de vorige eeuw is bewezen (ja, voordat computers zelfs echt bestonden). Wiskunde genereert altijd kopieën. Het is erg transformatief door een dataset te nemen en een nieuwe dataset te produceren. Door muterende objecten niet toe te staan, volgt computertechnologie de wiskunde door altijd nieuwe gegevenssets te moeten maken (onveranderlijke objecten).

Echter, alleen vanwege je functionele taalmodellen, betekent de theorie niet dat het dit kan doen met uitsluiting van imperatief programmeren. Er zijn tijden dat ontwikkelaars de flexibiliteit nodig hebben om te coderen in lagen met een laag geheugenrestrictie. Sommige problemen, om alleen performance-redenen, vereisen zelfs dat je codeert op het bufferniveau (zie TechEmpower-benchmarks voor enkele van de snelste webservers). Ja, de zuiverheid is nuttig voor een schonere, meer abstracte applicatiecode, maar het zou niet uw enige spreekwoordelijke hulpmiddel in de taalband moeten zijn.

En voor degenen die denken dat functioneel programmeren het mogelijk maakt om functies samen te stellen, heeft Java, met lambda’s, aangetoond dat eersteklasfuncties kunnen worden gemodelleerd als enkele-methode-objecten. Ja, je kunt hier veel suiker syntaxis en compiler-ondersteuning aan toevoegen. Het werkelijke verschil tussen objectoriëntatie en functioneel programmeren is echter het geheugen.

 

Samenvatting

Ja, op de juiste manier gebruikt, functionele programmering kan problemen eenvoudiger oplossen. Het heeft zeker geholpen academische inspanningen en verbeterde compilers. Maar het is geen debat van beter of slechter. Functioneel programmeren kan gewoon worden beschouwd als een modelleringsbeperking waardoor de compiler / frameworks meer aannames kunnen doen over uw code.

In feite maakt het de compilers / frameworks eigenlijk gemakkelijker om te schrijven ten koste van het beperken van ontwikkelaars.

Maar waarom zouden compilers en functionele frameworks (zoals Spring Reactive) deze beperkingen krijgen van wat u als ontwikkelaar kunt doen?