lørdag 20. august 2011 OOP Polyglot JavaScript
Hvor kommer objektorientert programmering fra? Hvilke problemer løser det? Kan man gjøre det samme i programmeringsspråk uten direkte støtte for objekter? I denne blogposten snakker jeg litt om røttene til OO, og demonstrerer – vha. JavaScript – hvordan man gjør OOP med bare funksjoner. Gjør deg klar for litt back to basics!
Og historien begynner med et spørsmål...
Jeg har stilt og forsøkt å svare på dette tidligere i blogposten Lispy C#. En "lexical closure" er en funksjon pluss en referanse til de variablene som brukes i funksjonen men som ikke er definert der – såkalte frie variabler. Selv om variablene faller ut av scope, vil de fortsatt være tilgjengelig i funkjonen, fordi closuren holder referanser til dem.
Konseptet ble utviklet på 60-tallet, og ble angivelig for første gang implementert i programmeringsspråket Scheme. Imperative språk har tradisjonelt ikke hatt støtte for closures ettersom disse språkene hverken støttet ikke-lokale variabler eller høyererangs funksjoner.
Neste spørsmål...
Et objekt er en datastruktur som består av datafelter og metoder. Vi snakker ofte om encapsulation, som i praksis betyr at metodene fungerer som mellommenn mellom datafeltene og dem som ønsker å benytte dem. Ingen får røre objektets tilstand direkte, kun via metodene.
Ideen om objekter startet allerede på 50 og 60-tallet. Det første programmeringsspråket designet for å organisere koden på denne måten ble utviklet i Norge, og het Simula 67. Men det var supergeniet Alan Key som kom opp med begrepet "objektorientert programmering", basert på sin forståelse av Lisp (hvor man brukte begrepet "objekt" om både tall, tekststrenger, symboler osv.).
På 70-tallet designet Alan Key språket Smalltalk, som kanskje har vært enda viktigere for utviklingen av dagens objektorientering enn det Simula var. Java, Objective-C, Ruby og mange andre språk hentet mye inspirasjon fra Smalltalks objektmodell.
Simula ..var en utvidelse med objektorienterte egenskaper til sterkt typet Allgol 60. Objektene eksisterte side om side med tradisjonelle datatyper som tall og strenger. | vs. | Smalltalk ..var basert på det dynamiske, løst typede Lisp, men erstattet funksjoner og s-expressions med metoder og objekter. I Smalltalk er all data representert av objekter. |
(Og så kom 80-tallet, og C++ tok over verden!)
Ser du på de to figurene mine er det slående hvor mye closures og objekter har til felles. Og det leder meg til det tredje spørsmålet...
Svaret er opplagt. Man trenger funksjoner som har mulighet til å danne closures. Siden closures skjuler tilstand (gjennom frie variabler) kan de brukes til å lage objekter.
En teknikk for å gjøre dette presenteres blant annet i boken Structure and Interpretation of Computer Programs (Informatikk-pensum på MIT), og kalles Message Passing Style. Det vi snakker om her nå er helt grunnleggende objektorientering, en teknikk som brukes til å implementere objektsystemer i programmeringsspråk – eller som lar deg lage objekter i språk hvor du ikke har dette out of the box.
I Smalltalk (og språk som er inspirert av Smalltalk) sier vi at det å kalle en metode på et objekt er det samme som å sende et objekt en melding. Er du kjent med Ruby, vet du for eksempel sikkert at de følgende to linjene gjør akkurat det samme:
1 an_array.length 2 an_array.send('length');
Så hvordan kan man implementere message passing uten å bruke "vanlige" objekter? Her følger et eksempel i JavaScript.
Jeg ønsker å kunne opprette objekter som skal representere bankkontoer. Kontoene skal ha en balanse, og skal kunne ta i mot meldinger om å sette penger inn på kontoen og å trekke penger ut av kontoen. Jeg trenger da en funksjon som fungerer som en konstruktør av kontoer – kaller jeg denne får jeg opprettet og returnert en ny konto. Her følger koden...
10 var createAccount = function(balance){ 11 function deposit(amount){ 12 return balance += amount; 13 }; 14 function withdraw(amount){ 15 return balance -= amount; 16 }; 17 return function(msg){ 18 switch(msg){ 19 case "deposit": 20 return deposit(arguments[1]); 21 case "withdraw": 22 return withdraw(arguments[1]); 23 default: 24 throw "Unknown message " + msg; 25 }; 26 }; 27 };
deposit og withdraw er to funksjoner som defineres inne i createAccount, og de vil ikke være tilgjengelige på utsiden. Fordi de benytter seg av variabelen balance danner hver av dem en closure.
Den tredje funksjonen – som returneres fra createAccount – kalles en dispatch-funksjon. Denne danner også en closure, fordi den refererer de to funksjonene deposit og withdraw. Jeg sa at createAccount skulle returnere en konto, og i message passing style vil det altså si en closure som inneholder en dispatch-funksjon.
For å opprette kontoer kan jeg nå skrive:
29 var account1 = createAccount(100); 30 var account2 = createAccount(100);
Og for å legge til eller ta ut penger sender jeg de respektive meldingene til "objektet" sammen med beløpet det gjelder:
32 account1("deposit", 10); // returns 110 33 account1("withdraw", 5); // returns 105 34 account2("withdraw", 5); // returns 95
Legg merke til at det ikke finnes noen annen måte å modifisere balance på enn å kalle dispatch-metoden. Encapsulation er oppnådd!
Det er egentlig ganske simple greier dette her, men likevel synes jeg det er fasinerende. Det hadde nå vært fristende å diskutere hva dette har med actor model og Erlang å gjøre. Jeg kunne også ha fortsatt med å snakke om forskjellen mellom Turing-modellen, språk basert på Von Neumann-arkitekturen, og Alonzo Church's Lambda Calculus - og hvordan det eneste man egentlig trenger for å lage et komplett programmeringsspråk er muligheten for å opprette og eksekvere lambdaer.., men det er nok på tide å gi seg mens leken er god.
Så det du skal huske fra denne blogposten er: Closures er funksjoner pluss referanser til frie variabler. Du kan lage objekter i språk som ikke i utgangspunktet støtter dem, såsant du har førsterangs funksjoner som kan danne closures. I denne sammenhengen er en dispatch-funksjon en clojure som tar imot en melding og eksekverer kode for å aksessere tilstand som ikke kan nås på andre måter. Dispatch-funksjonen er i prinsippet et objekt. Dette kalles Message Passing Style.