Automatisera Mera eller: Det Ultimata Vapnet Mot Boilerplate-kod

Permalänk
Medlem

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.

Permalänk
Medlem

Nån som läst allt?

Permalänk
Medlem
Skrivet av tufflax:

Nån som läst allt?

Oja! Har velat läsa mer om hur macros fungerar i Clojure ett tag så det var uppskattat. Däremot gillar jag inte de manuella radbrytningarna

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av Teknocide:

Oja! Har velat läsa mer om hur macros fungerar i Clojure ett tag så det var uppskattat. Däremot gillar jag inte de manuella radbrytningarna

Skrev det i vim och pastea, ska se om jag kan ta bort dem snabbt på nått sätt. För ovrigt, nått som var oklart?

EDIT: Så, borta.

EDIT 2: Jag la till följande stycke om condp:

Citat:

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.

Permalänk
Medlem

Jag läste oxå igenom det

Ett par kommentarer:
Exec/eval bör man undvika så långt det går. Väldigt ofta går det att göra saker utan exec/eval, och dom skapar en hel del potentiella problem.

Själv har jag ganska svårt för Lisp. Alla dessa parenteser till att börja med. Usch. Lite känns det som assembler, lågnivå fast från ett annat håll. Kan förstå att en del ser snyggheten i att man bara har ett abstrakt syntaxträd att jobba med.. Men jag föredrar lite fluff emellan (Ska passa på att säga att jag aldrig kodat något i lisp eftersom jag inte gillar att ens läsa det).

Men även om man nu ogillar Lisp så finns det massor av språk som kan lösa problem mer elegant än Java som är relativt sett väldigt "pratigt". T.o.m om man kommer från Microsofts Java-"klon" C# skulle man kunna skriva alla dina 3 exempel på ett kortfattat sätt.

Visa signatur

AK47s for everyone! - Angry mob
Since NaN /= NaN, I think, we should decipher 'NaN' as 'Not a NaN' - Miguel Mitrofanov
(Varför är människan så benägen att tro på Gud?) Antagligen har det lönat sig och evolutionen har drivit fram sådana hjärnor. - Anon

Permalänk
Medlem
Skrivet av tufflax:

Nån som läst allt?

yupph, ganska intressant tycker jag

Visa signatur

In order to understand recursion, one must first understand recursion

Permalänk
Medlem
Skrivet av vb:

Jag läste oxå igenom det

Ett par kommentarer:
Exec/eval bör man undvika så långt det går. Väldigt ofta går det att göra saker utan exec/eval, och dom skapar en hel del potentiella problem.

Sa aldrig att man skulle använda det. Men man skulle kunna, och det är halvvägs till makron, nästan.

Skrivet av vb:

Själv har jag ganska svårt för Lisp. Alla dessa parenteser till att börja med. Usch. Lite känns det som assembler, lågnivå fast från ett annat håll. Kan förstå att en del ser snyggheten i att man bara har ett abstrakt syntaxträd att jobba med.. Men jag föredrar lite fluff emellan (Ska passa på att säga att jag aldrig kodat något i lisp eftersom jag inte gillar att ens läsa det).

Det finns inget snyggare än Lisp. Förresten så har Clojure mycket färre parenteser än Common Lisp och Scheme, och inte heller fler än Java har, bara att de sitter på andra ställen. Jag vet dock inte vad du menar med lågnivå.

Varje gång jag ser ett nytt spårk med "vanlig" syntax så tänker jag: Varför? Har de inte fattat att Lispsyntax är överlägset?

Skrivet av vb:

Men även om man nu ogillar Lisp så finns det massor av språk som kan lösa problem mer elegant än Java som är relativt sett väldigt "pratigt". T.o.m om man kommer från Microsofts Java-"klon" C# skulle man kunna skriva alla dina 3 exempel på ett kortfattat sätt.

Jo, Java är ju ett av det språk där man får skriva mycket för att få nått gjort. Men det finns inte så många språk som är lika bra som Lisp på att få bort boilerplate. C# är väl inget bra exempel på ett språk med lite boilerplate, va?

Förresten efter ni läst det där borde ni se detta: http://www.infoq.com/presentations/Simple-Made-Easy Fler aha-upplevelser väntar!

Permalänk
Medlem

Läst allt!

Håller med om att en del kod skrivs alldeles för mycket. Inom PHP och webben tänker jag speciellt på

<?php $user_name_or_something = mysql_real_escape_string($_POST['user_name_or_something']); $user_name_or_something = mysql_real_escape_string($_POST['user_name_or_something']); $user_name_or_something = mysql_real_escape_string($_POST['user_name_or_something']); mysql_query('insert into x values (`'.$user_name_or_something.'`....), (`user_name_or_something`, ....)') ?> <input type="" name="user_name_or_something"> <input type="" name="user_name_or_something"> <input type="" name="user_name_or_something">

Varje fälts namn skrivs 4 gånger, väldigt jobbigt. Här kan man ju dock skapa ett ramverk som utgår ifrån att allt har samma namn som kolumnen i databasen och sedan automatiserar. Ruby on Rails gör detta. Tror att FuelPHP gör ungefär samma sak.

Annars intressant indruktion till Clojure. Har prövat på Scheme för länge sen och det hatade jag
Men kanske borde testa Clojure, har lärt mig en heldel sen jag testade Scheme.

Som svar på kodexemplen:

för try, catch grejerna kan man ju skapa en pre-proccessor(?) som less och coffescript!
Kanske redan finns, känns som något som skulle kunna bli väldigt stort.

Sedan kände jag för att koda lite till månads grjen

Få ut månadens namn:

function month($i) { return date("F", mktime(0, 0, 0, $i)); //F = Returnera månaden, mktime = skapa en tid, från sekund, timme, minut 0 och månad $i }

Skapa array med alla månader:

for($i = 0; $i < 13; $i++, $month[$i] = date("F", strtotime('+'. $i .' month', mktime(0, 0, 0, 0)))) {} //Brainfuck kod är rolig att skriva :) men svår å förstå (:

Visa signatur

Programmerare -> PHP | HTML | CSS | JS | Java.

Permalänk
Medlem

Jag stötte på ett riktigt problem när jag programmerade här om dagen där makron verkligen kom till användning. Jag har skrivit en sorts regex-sökning, fast som funkar på maps, d.v.s. key-value-saker. Säg att man har en lista av maps, t.ex.

[{key val} {a b} {1 2, 3 4} {url "sweclockers.se"} ...]

och man vill leta efter mönster i den. Då kan man skriva ett regex som ser ut ungefär t.ex.

"{key val} .* [{a b} {c d}] {3 4}?"

som skulle matcha på det ovan 3 första mapsen ovan. I alla fall: Nu finns det några olika sätt att söka igenom listan.

* Ska man hitta kortaste eller längsta matchningen?
* Ska man leta bara från början, eller ska man börja om på position 2 om man inte hittar något från position 1? Och ska man börja om på nytt om man hittar något som matchar?
* Vad vill man ha som output? Hela matchningen, eller en delmatching (ni vet, man kan ofta referera till den första parentesen med \1 o.s.v.)?

Jag hade även en annan dimension som är lite svårare att förklara. Jag ville utöka cascalog, som är ett sorts frågespråk för Hadoop, med min regex-funktionalitet. P.g.a. vissa begränsningar i cascalog så kan man inte göra funktioner med ett variablet antal argument, och sånt. Det jag ville göra var 16 olika funktioner, d.v.s. en för varje av alla möjliga kombinationer av de 4 "dimensionerna". T.ex. så skulle submatches-list-long se ut:

(defmapop submatches-list-long [re input submatch] (let [matches (map :saved (regex-seq re input {:long? true}))] (mapv #(get-submatch input submatch %) matches)))

medan matches-list (d.v.s. hela matchningar och korta i stället för långa) se ut:

(defmapop matches-list [re input] (let [matches (map :saved (regex-seq re input {:long? false}))] (mapv #(get-submatch input 0 %) matches)))

De båda funktionerna är rätt lika. Så jag gjorde ett makro med vilket man kan definera sånna funktioner.

(defmacro match-def [def-op name sub? long? restart?] (with-gensyms [re input sub] (let [args [re input] args (if sub? (conj args sub) args)] `(~def-op ~name ~args (let [kwargs# ~{:long? long? :restart? restart?} matches# (map :saved (regex-seq ~re ~input kwargs#)) result# (mapv #(get-submatch ~input ~(if sub? sub 0) %) matches#)] (if (= '~def-op '~'defmapop) [result#] (mapv vector result#)))))))

Med detta makro så kan man definiera "submatches-list-long" med:

(match-def defmapop submatches-list-long true true false)

För att se vad koden blir när man skriver så, kan man kolla vad makrot expanderar till, genom att skriva

(macroepand-1 '(match-def defmapop submatches-list-long true true false))

Då ser man att det blir:

(defmapop submatches-list-long [G__3069 G__3070 G__3071] (clojure.core/let [kwargs__2954__auto__ {:long? true, :restart? false} matches__2955__auto__ (clojure.core/map :saved (regex-engine/regex-seq G__3069 G__3070 kwargs__2954__auto__)) result__2956__auto__ (clojure.core/mapv (fn* [p1__2953__2957__auto__] (regex-engine/get-submatch G__3070 G__3071 p1__2953__2957__auto__)) matches__2955__auto__)] (if (clojure.core/= 'defmapop 'defmapop) [result__2956__auto__] (clojure.core/mapv clojure.core/vector result__2956__auto__))))

En liten förbättring i alla fall. Fast man måste ju ändå skriva 16 sådana rader, även om de är kortare än hela funktioner. Så jag gjorde ett nytt makro:

(defmacro def-match-fns [] (let [tf [true false]] `(do ~@(for [op '[defmapop defmapcatop] sub? tf long? tf restart? tf] (list 'match-def op (symbol (str (if sub? "sub" "") "matches" (if (= op 'defmapop) "-list" "-split") (if (or long? restart?) "-" "") (if long? "long" "") (if restart? "restart"))) sub? long? restart?)))))

Efter det kan man bara skriva (def-match-fns) så får man alla 16 funktioner definierade. Koden som detta makro genererar är:

(do (match-def defmapop submatches-list-longrestart true true true) (match-def defmapop submatches-list-long true true false) (match-def defmapop submatches-list-restart true false true) (match-def defmapop submatches-list true false false) (match-def defmapop matches-list-longrestart false true true) (match-def defmapop matches-list-long false true false) (match-def defmapop matches-list-restart false false true) (match-def defmapop matches-list false false false) (match-def defmapcatop submatches-split-longrestart true true true) (match-def defmapcatop submatches-split-long true true false) (match-def defmapcatop submatches-split-restart true false true) (match-def defmapcatop submatches-split true false false) (match-def defmapcatop matches-split-longrestart false true true) (match-def defmapcatop matches-split-long false true false) (match-def defmapcatop matches-split-restart false false true) (match-def defmapcatop matches-split false false false))

Så med 20 rader makron får man 16 funktioner, och man slipper såklart upprepning vilket gör det koden mycket enklare att ändra på senare.

Permalänk
Medlem
Skrivet av tufflax:

Jo, Java är ju ett av det språk där man får skriva mycket för att få nått gjort. Men det finns inte så många språk som är lika bra som Lisp på att få bort boilerplate. C# är väl inget bra exempel på ett språk med lite boilerplate, va?

Om du inte vet så självklart tror du att C# är likt Java. Ja C# var likt java vid version 1.0. C# är numrera version 5.0 och mycket har hänt. Faktum är att version 3 var den version som det sprang iväg som ett betydligt trevligare språk än Java flera ggr om och inte vidare lik alls längre förutom på grund syntaxen.

T.ex. några av dina exempel om du nu skulle vilja slippa skriva all denna kod. Dock inget jag skulle Rekommendera att skriva på detta sätt MEN DET GÅR. Att få bort onödig kod betyder inte att din kod blir värken bättre eller lättare att läsa eller debugga o.s.v. Snarare tvärt om. Bättre att ha en editor som har bra snippet funktionalitet för att underlätta problemet.

Och bara för att du inte ska tro att jag snackar skit om allt detta så kommer lite exempel på vad du kan göra för några ovan har inte tid att ta allt.

Exempel #1 i C# är helt onödig, och koden du behöver skriva istället är ett enda ord mer eller mindre så kommer skippa visa en exakt metod för den. I C# fall behöver du enbart använda Using, denna ser till att disposa så fort man kommer utan för på ett eller annat sätt. ( http://msdn.microsoft.com/en-us/library/yh598w02(v=vs.80).asp... )

Här är dock t.ex. hur du kan göra med Try Catch med bland annat Extension methods.

class Program { static void Main(string[] args) { new Action(Method).TryCatchInvoke(); } public static void Method() { Console.WriteLine("Hello World"); } } public static class RandomExtensions { public static void TryCatchInvoke(this Action action) { try { action.Invoke(); } catch (Exception e) { //Handle exception. } } }

Dold text

Exempel #2 att Loopa igenom något ett antal ggr går också att lösa på många sätt, oftast när man loopa dock så arbetar man ju med någon form av listor. Men vi säger att i ditt fall vill man ha de 10 första elementen i en lista

C# har du en lista eller array och vill ha första 10 elementen så räcker
arrayObjektet.Take(10), denna tar upp till 10 eller färre om listan innehåller mindre, med den sedan kan du göra lite allt möjligt. T.ex.

Ett dåligt Exempel men ändå, ger ändå en liten bild på styrkan med LINQ, Lambda & Extension Methods.

var list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }; list.Take(10).ToList().ForEach(i => Console.WriteLine(i)); Console.WriteLine("The sum {0}", list.Take(10).Sum()); Console.WriteLine("The Average of numbers over 10: {0}", list.Where(x => x > 10).Average());

Exempel #3 kommer jag inte ens kommentera, bara dåligt att ens skriva koden så till att börja med även Java kan bättre!

Visa signatur

Speldator: i7-8700k, 32GB DDR4, RTX2080
Server 1: SB 2500k, MZI -P67GD55, 32GB DDR3, Corsair MX 240GB SSD
Surface Pro 2017, Konsoler: Typ alla, Oculus Rift

Permalänk
Medlem
Skrivet av MugiMugi:

Om du inte vet så självklart tror du att C# är likt Java. Ja C# var likt java vid version 1.0. C# är numrera version 5.0 och mycket har hänt. Faktum är att version 3 var den version som det sprang iväg som ett betydligt trevligare språk än Java flera ggr om och inte vidare lik alls längre förutom på grund syntaxen.

T.ex. några av dina exempel om du nu skulle vilja slippa skriva all denna kod. Dock inget jag skulle Rekommendera att skriva på detta sätt MEN DET GÅR. Att få bort onödig kod betyder inte att din kod blir värken bättre eller lättare att läsa eller debugga o.s.v. Snarare tvärt om. Bättre att ha en editor som har bra snippet funktionalitet för att underlätta problemet.

Och bara för att du inte ska tro att jag snackar skit om allt detta så kommer lite exempel på vad du kan göra för några ovan har inte tid att ta allt.

Exempel #1 i C# är helt onödig, och koden du behöver skriva istället är ett enda ord mer eller mindre så kommer skippa visa en exakt metod för den. I C# fall behöver du enbart använda Using, denna ser till att disposa så fort man kommer utan för på ett eller annat sätt. ( http://msdn.microsoft.com/en-us/library/yh598w02(v=vs.80).asp... )

Här är dock t.ex. hur du kan göra med Try Catch med bland annat Extension methods.

class Program { static void Main(string[] args) { new Action(Method).TryCatchInvoke(); } public static void Method() { Console.WriteLine("Hello World"); } } public static class RandomExtensions { public static void TryCatchInvoke(this Action action) { try { action.Invoke(); } catch (Exception e) { //Handle exception. } } }

Dold text

Exempel #2 att Loopa igenom något ett antal ggr går också att lösa på många sätt, oftast när man loopa dock så arbetar man ju med någon form av listor. Men vi säger att i ditt fall vill man ha de 10 första elementen i en lista

C# har du en lista eller array och vill ha första 10 elementen så räcker
arrayObjektet.Take(10), denna tar upp till 10 eller färre om listan innehåller mindre, med den sedan kan du göra lite allt möjligt. T.ex.

Ett dåligt Exempel men ändå, ger ändå en liten bild på styrkan med LINQ, Lambda & Extension Methods.

var list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }; list.Take(10).ToList().ForEach(i => Console.WriteLine(i)); Console.WriteLine("The sum {0}", list.Take(10).Sum()); Console.WriteLine("The Average of numbers over 10: {0}", list.Where(x => x > 10).Average());

Exempel #3 kommer jag inte ens kommentera, bara dåligt att ens skriva koden så till att börja med även Java kan bättre!

Ok! Det var länge sedan jag använde C#. Men Python, Ruby, Haskell osv har väl ändå mindre boilerplate? Men i alla fall: Inget av de språken har makron på samma sätt som Lisp har, med vilka man kan få bort i princip all boilerplate om man vill.