Automatisera Mera eller: Det Ultimata Vapnet Mot Boilerplate-kod
I Java finns mycket boilerplate-kod, d.v.s. kod som måste finnas på många ställen i samma skepnad, och som inte är så intressant. Exempel:
// Exempel 1
try {
// gör nånting
}
finally {
nånting.close();
}
eller
// Exempel 2
for (int i = 0; i < tal; i++) {
// gör nånting
}
eller
// Exempel 3
switch (month) {
case 1: monthString = "January";
break;
case 2: monthString = "February";
break;
case 3: monthString = "March";
break;
case 4: monthString = "April";
break;
case 5: monthString = "May";
break;
case 6: monthString = "June";
break;
case 7: monthString = "July";
break;
case 8: monthString = "August";
break;
case 9: monthString = "September";
break;
case 10: monthString = "October";
break;
case 11: monthString = "November";
break;
case 12: monthString = "December";
break;
default: monthString = "Invalid month";
break;
Titta på all upprepning! "case", "monthString" och "break;" står ju 12 gånger var. Okej, just denna kod behöver man inte skriva så där. Jag tog den bara från ett exempel jag såg på webben. Men det är ändå vanligt med if-else-if-else-osv-satser och switch-satser med mycket repetition i Java.
Det man skulle vilja göra är att skriva någon slags kod som hanterar sånt där. Exempelvis så vore det ju lätt, givet alla månader, att skriva kod som genererar en sträng med switch-satsen i. Man borde kunna generera koden också, inte bara stängen.
Om vi tar Exempel 1 så skulle man ju vilja skriva någon typ av metod som ser ut nånting i stil med:
public void finallyClose(code c) {
try {
execute(c);
}
finally {
closeStreamsIn(c);
}
}
Ovnastående går förstås inte, men vore det inte skönt om det hade funkat? Då hade man bara kunna skriva finallyClose(new BufferedReader(...).readLine()); i stället för hela try-satsen.
Det finns många språk, t.ex. Python som försöker skära ner på boilerplate-kod. Python lyckas ganska bra. Om man bara ska läsa en fil och skriva ut innehållet i Python behöver man t.ex. bara skriva
f = file("filnamn")
for line in f:
print line
Det är rätt mycket kortare än mostsvarande kod i Java skulle vara, där man behöver "public class Blabla" och "public static void main(String[] args) {...}", o.s.v.
Python har också funktionen exec. Med den kan man exekvera kod i en sträng, t.ex. exec("print 'hej'"). Om Java hade haft en exec-metod hade vi kunnat generera switch-satsen ovan som en sträng och sedan kört den. Att generera strängar är dock lite jobbigt. Det är inte lika lätt som att skriva vanlig kod som loopar igenom listor och sånt.
Det språk som har kommit längst i detta avseende är Lisp. Det finns många dialekter, som det heter, av Lisp, t.ex. Common Lisp, Scheme och Clojure. Låt oss ta en titt på Clojure:
(defn abs [n]
((if (< n 0) - +) n))
Det första man borde veta om Lisp är att allt är skrivet i prefixnotation, d.v.s. operatorn kommer alltid först. (+ 1 2) är 3. (< n 0) kollar om n är mindre än 0. Om man skiver t.ex. (+ 1 2) så anropar man funktionen +, precis som att om man skriver println() i Java så anropar man funktionen println.
(if (< n 0) - +) returnerar funktionen - om n är mindre än 0, annars returneras funktionen +. Sedan används returvärdet som operator i nästa lista, d.v.s. man kommer evaluera (- n) eller (+ n) beroende på vad n var.
Koden ovan är en funktion för att beräkna absolutbelopp. Ni kanske tycker att det ser lite konstigt ut; massa parenteser. Men det fina med det är att parenteserna inte bara är vanlig syntax, utan det är faktiskt listor, d.v.s. liknande ArrayList i Java, eller en [] i Python. Namnen som används, t.ex. if, +, -, abs, etc. är symboler. Symboler skiljer sig från namn i andra språk, t.ex. Java, genom att de kan agera värden, som man kan spara i t.ex. variabler och listor. Koden ovan är allstå data, kan man säga. Det är en lista som har symbolen defn som första element, symbolen abs som andra element, vektorn [n] som tredje element, osv. Vad är det för mening med detta? Jo, man kan lätt hantera kod med kod. Kod hanterar vanligtvis data, men i Lisp så är koden data.
Man utnyttjar detta bäst med makron. Man kan skriva makron som fungerar som vi ville att finallyClose() skulle fungera ovan.
Vanligtvis när man anropar en funktion så evalueras ju argumenten innan man kör funktionen man anropar på dem. Om man skulle skriva add(a, b + c) i Java, så hade ju add fått som indata innehållet av a, och resultatet av b + c. Så är det i Clojure med för vanliga funktioner, men inte för makron. I Clojure så hade föregående sett ut så här: (add a (+ b c)). Makron får i stället koden som indata. D.v.s om add var ett makro, a var 3 och (+ b c) var 6, så hade add fått koden a och koden (+ b c) som indata, och inte 3 och 6. Makron kan sen manipulera koden hur som helst, och också generera massa ny kod.
Vi kan ta ett exempel. Vi sa ju att i Clojure så skriver man funktionen först i en lista, och sedan argumenten, t.ex. (+ 1 2). Men man skulle kunna skriva ett makro som gör att man kan skriva koden baklänges. Inte så användbart kanske, men förhoppningsvis illustrativt.
(defmacro reverse-clojure [kod]
(reverse kod))
Eftersom kod som (+ 1 2) är en lista så kan vi använda funktionen reverse, som fungerar på listor. Man skulle kunna använda reverse-clojure på följande sätt.
(reverse-clojure (1 2 +))
Svaret hade såklart blivit 3.
Det finns ett makro som heter condp i Clojure som är rätt likt en switch-sats i Java. Eller egentligen är det inte så likt, men det kan användas i samma syfte. Det kan se ut såhär:
(condp = month
1 "January"
2 "February"
3 "March"
"default-case")
Jag orkade inte skriva dit alla månader, men förhoppningsvis går det att förstå ändå. condp tar en funktion (det första argumentet, här är det =), ett värde (month) och sedan godtyckligt många par av värden. Om (i detta fall) (= month 1) är sant, så blir svaret "January", osv.
Makros manipulerar bara syntax (listor, symboler, osv). condp är som sagt ett makro, så det får som indata all den kod vi har skrivit som argument till condp. Sedan expanderas, som det heter, condp-anropet, d.v.s. condp genererar kod, som byts ut mot (condp ...)-listan.
I just detta exempel hade condp-anropet expanderat till:
(let [pred = expr month]
(if (pred 1 expr) "January"
(if (pred 2 expr) "February"
(if (pred 3 expr) "March"
"default-case"))))
Ovan ser vi förstås en lång lista av if-else-if-else-osv. Det hade varit mycket jobbigare att skriva.
Jag skerv ovan att det första argumentet till condp skulle vara en funktion. Men eftersom makron bara manipulerar syntax, så bryr sig inte condp om att det första argumentet är en funktion. Att man har skickat rätt saker till condp märker man först när koden som condp expanderas till körs.
Varje gång man ser ett mönster som upprepar sig, d.v.s. boilerplate-kod, så kan man bara skriva ett makro som genererar den tråkiga koden åt en. Det är inte bara switch-satser och sånt som har boilerplate-kod i sig, utan det finns överallt. Tänk er följande: Man ser massa boilerplate-kod i Clojure och skriver makron för att ta hand om det. Därefter kan man programmera på en högre nivå, utan att behöva upprepa sig så mycket. Men efter ett tag så kommer man säkert se mönster som upprepar sig även i denna högre nivå. Då kan man skriva nästa lager av makron som i sin tur expanderar till kod på den första nivån.
Man programmerar ju för att automatisera saker. Med Lisp kan man automatisera programmeringen. Lispmakron är det ultimata vapnet mot boilerplate-kod.