Sie befin­den sich hier:Start­sei­te/Blog/Ein­fach, sicher, pro­duk­tiv: Funk­tio­na­le Programmierung

Einfach, sicher, produktiv: Funktionale Programmierung

Lese­zeit: 30 Minu­ten
Einfach, sicher, produktiv: Funktionale Programmierung

So les­bar könn­te und soll­te siche­rer Code aussehen:

In die Zwi­schen­ab­la­ge kopieren

Wenn Ihr Code so klar und aus­drucks­stark ist, haben Sie die­sen Bei­trag womög­lich nicht mehr nötig. Alle ande­ren soll­ten sich von sei­ner Län­ge nicht abschre­cken las­sen, denn: Ich möch­te Ihnen anhand kon­kre­ter Code­bei­spie­le die Grund­prin­zi­pi­en und immensen Poten­zia­le der soge­nann­ten funk­tio­na­len Pro­gram­mie­rung (FP) ver­mit­teln. Am Ende wer­den Sie nicht nur glau­ben, dass die oben gezeig­te Code­stre­cke funk­tio­niert, son­dern auch schon unge­fähr ver­ste­hen, wie und warum.

Bit­te neh­men Sie sich die Zeit zur Lek­tü­re also in Ihrem eige­nen Inter­es­se! Der Ein­satz von FP kann Ihre Ent­wick­lung auf ein völ­lig neu­es Niveau von Qua­li­tät und Pro­duk­ti­vi­tät heben. Das soll­te Ent­wick­ler genau­so inter­es­sie­ren wie Manager.

Ich wer­de am Anfang zunächst kurz auf die Moti­va­ti­on und den theo­re­ti­schen Hin­ter­grund von FP ein­ge­hen. Das soll dem Ver­ständ­nis die­nen, ist aber für die wei­ter unten fol­gen­den Erläu­te­run­gen anhand kon­kre­ter Code­bei­spie­le nicht unbe­dingt nötig. Sie kön­nen auch direkt zu „Unse­re ers­ten Funk­tio­nen“ sprin­gen und ggf. bei Fra­gen auf die Ein­lei­tung zurückkommen.

Scheitern scheint vorprogrammiert zu sein

Pro­gram­mie­rer ste­hen in einem eher schlech­ten Ruf. Fast jeder kennt Sym­pto­me wie diese:

  • Hoher Anteil von Debug­ging und/oder Defekten
  • Regres­si­on der Soft­ware (d.h. Defek­te durch Wartung)
  • Ent­wick­ler ver­ste­hen nach kur­zer Zeit den eige­nen Code nicht mehr
  • Ent­wick­ler schrei­ben lie­ber neu­en Code, statt exis­tie­ren­den zu verwenden
  • Abneh­men­de Wart­bar­keit bis hin zum Wartungskollaps
  • „Iso­lier­te Pro­gramm­si­tua­tio­nen“ (Euphe­mis­mus für uner­klär­li­che Fehler)
  • Pro­duk­ti­vi­tät und Qua­li­tät sub­op­ti­mal, Zie­le wer­den gerissen

Das alles weist auf hand­werk­li­che Schwä­chen hin. Wer (ob als Mana­ger oder Ent­wick­ler) die­se Sym­pto­me ernst nimmt, muss dar­an inter­es­siert sein, die Ent­wick­lung ein­fa­cher, siche­rer und pro­duk­ti­ver zu machen. Man kann und soll­te mit sinn­vol­len Pro­zes­sen (Agi­le, TDD, BDD, u.ä) gute Rah­men­be­din­gun­gen schaf­fen. Aber für das Ver­mei­den hand­werk­li­cher Feh­ler gibt es m.E. kei­nen grö­ße­ren Schritt als die Ein­füh­rung des Para­dig­ma „Funk­tio­na­le Pro­gram­mie­rung“.

Vie­le hal­ten funk­tio­na­le Pro­gram­mie­rung irr­tüm­lich für kom­pli­zier­ter als „nor­ma­le“ Pro­gram­mie­rung. Eigent­lich ist eher das Gegen­teil rich­tig: Guter FP-Code besticht durch sei­ne Ein­fach­heit und Klar­heit. Davon kön­nen Sie sich in die­sem Bei­trag über­zeu­gen. Rich­tig ver­stan­den, kann FP den Soft­ware­ent­wick­ler von unnö­ti­gem men­ta­len Bal­last befrei­en und damit erheb­lich die Pro­duk­ti­vi­tät und Qua­li­tät erhö­hen.

Auch vor der Lek­tü­re des Fol­gen­den soll­ten Sie intui­tiv begrei­fen kön­nen, was die ein­gangs gezeig­te Code­stre­cke errech­net. Und das ist der Punkt: Jeder, des­sen Code deut­lich kom­pli­zier­ter und/oder unsta­bi­ler ist, soll­te sich fra­gen: Muss das im Jahr 2017 noch sein? Und: War­um ist das über­haupt so? Mit die­ser Fra­ge möch­te ich beginnen.

Problemverursacher: „Wie“ statt „Was“

Die ein­gangs genann­ten (und wei­te­re) Pro­ble­me sind typi­sche Sym­pto­me der soge­nann­ten „impe­ra­ti­ven Pro­gram­mie­rung“ (zu die­ser Fami­lie gehört übri­gens auch das in der Indus­tie am meis­ten ver­brei­te­te Para­dig­ma der objekt-ori­en­tier­ten Pro­gram­mie­rung!). Die­se Art des Pro­gram­mie­rens ist seit Jahr­zehn­ten so all­ge­gen­wär­tig, dass sie zu sel­ten in Fra­ge gestellt wird, obwohl es bes­se­re Alter­na­ti­ven gäbe (neben FP gibt es z.B. auch logi­sche Pro­gram­mie­rung, die für man­che Auf­ga­ben­stel­lun­gen eine inter­es­san­te Alter­na­ti­ve ist).

Die meis­ten Men­schen (Pro­fis wie Ama­teu­re) haben von Pro­gram­mie­rung fol­gen­de Vor­stel­lung: Man beschreibt Programmabläufe(Algorithmen), die schritt­wei­se Daten ein­le­sen, verarbeiten/verändern und aus­ge­ben. Genau die­ses schritt­wei­se Vor­schrei­ben (Pro­gramm: lat. das Vor-Geschrie­be­ne) nennt man impe­ra­tiv (lat. „befeh­lend“).

Im Klei­nen funk­tio­niert das auch noch eini­ger­ma­ßen gut, gera­de für Anfän­ger, weil es an Koch­re­zep­te o.ä. erin­nert. Aber das Pro­blem ist: Mit zuneh­men­der Pro­gramm­grö­ße wird das gan­ze unüber­sicht­lich, es kommt immer stär­ker zu den gefürch­te­ten Neben­ef­fek­ten. Selbst die ein­fachs­ten Din­ge wie die Berech­nung eines Auf­trags­werts aus der Sum­me sei­ner Posi­tio­nen erfor­dern im Ver­hält­nis zum fach­li­chen Pro­blem zu viel Auf­merk­sam­keit. Hoch­ge­rech­net auf kom­ple­xe Anwen­dun­gen führt das zu einer men­ta­len Über­las­tung der Pro­gram­mie­rer. Die Fol­ge: Kleins­te Kon­zen­tra­ti­ons­schwä­chen oder Stö­run­gen füh­ren zur Ein­füh­rung von Feh­lern, deren Fol­gen oft weit über die aktu­ell bear­bei­te­te Pro­gramm­stel­le hinausgehen.

Die­se ohne­hin schon pro­ble­ma­ti­sche Situa­ti­on ist in den letz­ten Jahr­zehn­ten noch ver­schärft wor­den: Soft­ware­sy­te­me wer­den nicht nur immer kom­ple­xer, son­dern arbei­ten zuneh­mend neben­läu­fig und ver­teilt (man den­ke nur an Mul­ti-Core-Pro­zes­so­ren und die asyn­chro­ne, ver­teil­te Natur des Inter­nets). Durch die Grund­schwä­che der impe­ra­ti­ven Pro­gram­mie­rung, stän­dig Daten­zu­stän­de zu ver­än­dern, kommt es dabei zu den gefürch­te­ten „race con­di­ti­ons“, bei denen Algo­rith­men sich gegen­sei­tig in die Que­re kom­men und inkon­sis­ten­te Zustän­de erzeu­gen. Mit stei­gen­der Last tau­chen plötz­lich ver­meint­lich uner­klär­li­che Feh­ler auf – auch bek­abnnt als „Hei­sen-Bugs“ (in Anspie­lung auf Wer­ner Hei­sen­berg, Erfin­der der „Unschär­fe­re­la­ti­on“ der Quan­ten­theo­rie und einer der theo­re­ti­schen Väter der Atom­bom­be). Die Pro­gram­me fan­gen an, sich non­de­ter­mi­nis­tisch (nicht vor­her­sag­bar) zu ver­hal­ten – und das ist ja gera­de nicht das Ziel impe­ra­ti­ver Programmierung.

Back to Church: Transformieren statt Reformieren

Wenn es um die Grund­la­gen moder­ner Pro­gram­mie­rung geht, fällt meist der Name Alan Turing. Weni­ger bekannt ist sein Zeit­ge­nos­se Alon­zo Church. Die­ser ent­wi­ckel­te 1935 das soge­nann­te Lambda(λ)-Kalkül und leg­te damit nicht nur den Grund­stein für Com­pi­ler­bau, son­dern auch für das, was wir heu­te funk­tio­na­le Pro­gram­mie­rung nennen.

Schon wenn man die theo­re­ti­sche Model­le die­ser bei­den ver­gleicht, ahnt man, wel­ches eher zu Pro­ble­men führt: Turing arbei­te­te u.a. mit der nach ihm benann­ten Maschi­ne, einem Mecha­nis­mus, der theo­re­tisch unend­li­che Bän­der vor und zurück­spu­len und die auf ihm befind­li­chen Infor­ma­tio­nen belie­big immer wie­der ändern kann. Selbst die ein­fachs­ten Lösun­gen wer­den auf die­ser hypo­the­ti­schen Infra­struk­tur schnell extrem unüber­sicht­lich, der kleins­te Feh­ler ver­ur­sacht das Schei­tern des Pro­gramms. Zwar ist die Turing-Maschi­ne nur ein theo­re­ti­sches Modell zum Beweis des soge­nann­ten Ent­schei­dungs­pro­blems, aber ihre Funk­ti­ons­wei­se ist eben auch ein Mene­te­kel für das Schei­tern der impe­ra­ti­ven Pro­gram­mie­rung an Kom­ple­xi­tät.

Church hin­ge­gen hat sich über­legt, was die ein­fachs­te und uni­ver­sells­te Wei­se wäre, Funk­tio­na­li­tät so dar­zu­stel­len, dass man sie einer Maschi­ne bei­brin­gen kann. Statt auf mecha­ni­sche Maschi­nen a la Turing hat er sich an den Grund­bau­stei­nen der Mathe­ma­tik ori­en­tiert: Funk­tio­nen. Deren Grund­prin­zip ist es, erhal­te­ne Para­me­ter in Rück­ga­be­wer­te zu trans­for­mie­ren, ohne jemals die Para­me­ter selbst zu ändern. Die­se Ein­schrän­kung hat Church zu einem Modell geführt, das so mäch­tig wie Turings ist, aber viel ein­fa­cher und ungleich siche­rer (im Sin­ne von robus­ter und korrekter).

Ein wenig Theorie, aber keine Angst vor griechischen Buchstaben!

Auch wenn Mathe­ma­tik nicht Ihr Ding ist, kön­nen Sie sorg­los wei­ter­le­sen: Denn das Lamb­da-Kal­kül von Church besticht durch sei­ne Ein­fach­heit. Es kann nur Funk­tio­nen beschrei­ben, die einen ein­zi­gen Para­me­ter erhal­ten und einen Aus­druck zu des­sen Umwand­lung besitzen!

Schau­en wir, wie die Qua­drat­funk­ti­on bei Church aussieht:

In die Zwi­schen­ab­la­ge kopieren

Ist doch ein­fach, oder? λ defi­niert eine Funk­ti­on, x ist ihr Para­me­ter, der Punkt trennt Para­me­ter und Trans­for­ma­ti­ons­aus­druck. Es ist ein­fach nur eine ande­re Nota­ti­on für Funktionen.

Die soft­ware­tech­ni­sche Umset­zung ist auch ein­fach: Neh­men wir an, die o.g. Qua­drat­funk­ti­on hät­te den sym­bo­li­schen Namen sqr erhal­ten. Wenn im Pro­gramm­code z.B. sqr(x) auf­taucht, dann weiß der Com­pi­ler, dass er das durch x*x erset­zen kann. Das glei­che gilt natür­lich, wenn statt x kon­stan­te Wer­te ver­wen­det wer­den (sqr(3) könn­te im Com­pi­lat sofort als 9 ste­hen). Nur damit wir das auch erwähnt haben: Die­sen Vor­gang nennt man im Lamb­da-Kal­kül Beta(β)-Reduktion. Den Begriff wer­den Sie aber viel­leicht nie mehr benut­zen. Es sei denn, Sie sind Com­pi­ler­ent­wick­ler – oder Angeber.

Nun wis­sen wir aber, dass die wenigs­ten Funk­tio­nen nur einem Para­me­ter haben. Und hier zeigt sich wie­der ein­mal, dass Beschrän­kung frei machen kann. Denn auch Church stand natür­lich vor die­sem Pro­blem – und er hat es geni­al ein­fach gelöst. Schau­en Sie sich die Defi­ni­ti­on einer Addi­ti­ons­funk­ti­on in Lamb­d­ano­ta­ti­on an:

In die Zwi­schen­ab­la­ge kopieren

Aha, zwei Para­me­ter, zwei Lamb­das. Aber was bedeu­tet das? Das ers­te Lamb­da erzeugt ein zwei­tes Lamb­da, in des­sen Trans­for­ma­ti­ons­aus­druck der Wert des Para­me­ter des ers­ten auf­taucht (man spricht hier auch vom Bin­den eines Para­me­ters; der Rück­ga­be­wert ist eine soge­nann­te Clo­sure, weil dar­in der kon­kre­te Wert von x beim Auf­ruf von f(x) ein­ge­schlos­sen wird)!

Die­ser geni­al ein­fa­che Trick führt impli­zit zu einer wei­te­ren ganz grund­le­gen­den Eigen­schaf­ten funk­tio­na­ler Pro­gram­mie­rung: Funk­tio­nen selbst sind Typen, beschrie­ben über ihre Signa­tur. Das führt zu vie­len extrem mäch­ti­gen Mög­lich­kei­ten, die es so nur in der FP gibt: Funk­tio­nen höhe­rer Ord­nung, Par­ti­al Appli­ca­ti­on, Cur­ry­ing, Piping etc. Kei­ne Sor­ge, das ist alles ganz ein­fach, und Sie wer­den es bald kennen.

Zum Abschluss des Theo­rie­blocks und als Über­gang in die Pra­xis möch­te ich Ihnen zei­gen, wie (von der syn­tak­ti­schen Ver­ein­fa­chung in Pro­gram­mier­spra­chen abge­se­hen) die Signa­tur (also der Typ) einer FP-Funk­ti­on eigent­lich typi­scher­wei­se aus­sieht, hier am Bei­spiel der o.g. add-Funk­ti­on, die mit dem Ganz­zahl­typ int arbei­ten soll:

In die Zwi­schen­ab­la­ge kopieren

Eine FP-Funk­ti­on mit ver­meint­lich zwei Para­me­tern ist also in Wirk­lich­keit eine mit einem Para­me­ter, deren Rück­ga­be­wert eine wei­te­re Funk­ti­on mit eben­falls einem Para­me­ter ist!

Damit sei vor­erst Schluss mit der Theo­rie. Was das Lamb­da-Kal­kül in der Pra­xis für Mög­lich­kei­ten bie­tet und dass es sich mit der rich­ti­gen Pro­gram­mier­spra­che über­haupt nicht fremd anfüh­len muss, möch­te ich Ihnen im Fol­gen­den demonstrieren.

Ein Schluck vom Zaubertrank gefällig? Elixir

Als Spra­che für unse­re Code­bei­spie­le habe ich Eli­xir ver­wen­det. Zum einen ist sie ele­gant und auch ohne gro­ße Erläu­te­run­gen leicht ver­ständ­lich, zum ande­ren ist sie als Tech­no­lo­gie für Digi­ta­li­sie­rung und IoT eine ziem­lich gute Wahl.

Wenn Sie ger­ne hands-on ler­nen: Aus­führ­li­che Infos und gute Start-Tuto­ri­als fin­den Sie unter https://elixir-lang.org/ (die fol­gen­den Code­bei­spie­le kön­nen Sie aber auch ohne wei­te­res beim Lesen nach­voll­zie­hen, die Spra­che ist ein­fach genug)

elixir 1024x679 1

Unsere ersten Funktionen

Dann schrei­ben wir doch ein­fach mal Funk­tio­nen, wie wir sie oben bespro­chen haben. In Eli­xir macht man das in soge­nann­ten Modulen:

In die Zwi­schen­ab­la­ge kopieren

Viel ele­gan­ter kann eine Spra­che nicht mehr sein, oder? Auf den ers­ten Blick mögen Sie Daten­ty­pen ver­mis­sen. Eigent­lich bin ich auch ein Ver­fech­ter eines star­ken Typ­sys­tems, aber Eli­xir hat mich mit sei­ner Typ­lo­sig­keit bis­her über­zeugt, zumal ein mes­sa­ge-basier­tes Aktor­sys­tem wegen der losen Kopp­lung in der Regel ohne­hin nicht mit Typen arbei­ten sollte.

Wenn man Funk­tio­nen eines Moduls ver­wen­den möch­te, kann man das ent­we­der expli­zit durch Vor­an­stel­len des Modul­na­mens machen (SimpleSample.add) oder indem man das Modul in den aktu­el­len Scope impor­tiert. Um die Code­bei­spie­le über­sicht­lich zu hal­ten, wäh­len wir für unser Modul die­sen zwei­ten Weg.

Wie alle moder­nen Spra­chen besitzt Eli­xir eine inter­ak­ti­ve Shell (REPL). Dort kön­nen wir unse­re Funk­tio­nen erpro­ben (der Ein­fach­heit hal­ber las­se ich im Fol­gen­den das vor­an­ge­stell­te Prompt iex(…)> jeweils weg und gebe die Aus­ga­be als Kom­men­tar an:

In die Zwi­schen­ab­la­ge kopieren

Wie kann ich mir Wer­te „mer­ken“, um sie in wei­te­ren Schrit­ten zu ver­wen­den? So ein­fach, wie man sich das wünscht:

In die Zwi­schen­ab­la­ge kopieren

result ist in die­sem Bei­spiel übri­gens kei­ne Varia­ble, wie Sie sie aus ande­ren Pro­gram­mier­spra­chen ken­nen, son­dern nur ein Ali­as für einen unver­än­der­li­chen Wert. Aus Con­ve­ni­ence-Grün­den kann man sol­che Ali­as­se in Eli­xir sogar mehr­fach ver­wen­den, wodurch es wie eine Varia­ble aus­sieht. Aber der Com­pi­ler schützt uns vor Unge­mach und weist hin­ter den Kulis­sen jeweils neue ein­deu­ti­ge Namen zu. Ein Bei­spiel dafür, wie Eli­xir uns den Umstieg in die FP ein­fach macht.

Durch diese Röhre muss er kommen: Die Pipe

Aber in FP-Code ist es ohne­hin eher unüb­lich, mit zuge­wie­se­nen Zwi­schen­er­geb­nis­sen zu arbei­ten, vor allem wenn man nur am End­ergeb­nis inter­es­siert ist. In Java u.ä. Spra­chen hat man als Not­lö­sung die­ses Pro­blems das imp­le­me­tie­rungs­in­ten­si­ve Pat­tern Flu­ent API erfun­den. In der FP gehört die­se Pro­ble­ma­tik der Wei­ter­ver­ar­bei­tung zum Grund­kon­zept „Kom­po­nier­bar­keit“ (dar­un­ter ver­steht man das Zusam­men­ste­cken von Funk­tio­nen zu grö­ße­ren Funk­tio­nen – funk­tio­na­les Lego, sozusagen).

Sie wer­den dafür schnell ein Aus­drucks­mit­tel ver­in­ner­li­chen und lie­ben, das Sie viel­leicht aus Unix ken­nen: Piping

In die Zwi­schen­ab­la­ge kopieren

Das fühlt sich schon fast wie ein Taschen­rech­ner an, oder? (Und es gleicht fast impe­ra­ti­vem Pro­gram­mie­ren durch das Dekla­rie­ren einer Trans­for­ma­ti­ons-Abfol­ge, aber auf Basis von Funk­tio­nen und ohne „Varia­blen“.) Genau die­ses intui­ti­ve und mäch­ti­ge Anein­an­der­rei­hen von Funk­tio­nen ist ein all­ge­gen­wär­ti­ges Kon­zept von FP, das übri­gens ohne zusätz­li­chen Ent­wick­lungs­auf­wand auf allen Abs­trak­ti­ons­ebe­nen gleich funk­tio­niert. Es ist Grund­la­ge des mäch­ti­gen Archi­tek­tur­mus­ters „Pipes & Fil­ters“, wie auch der Code zu Beginn des Bei­trags zeigt. Der Eli­xir-Web­ser­ver Phoe­nix besitzt auf Basis die­ses Mus­ters eine beein­dru­ckend sta­bil und ein­fach erwei­ter­ba­re Archi­tek­tur mit soge­nann­ten Plugs.

Falls jemand Pipes nicht kennt, hier die ein­fa­che Arbeits­wei­se: Der Pipe-Ope­ra­tor (in Unix ist das | , in Eli­xir |>) nimmt den Rück­ga­be­wert der vori­gen Funk­ti­on und speist ihn als ers­ten Para­me­ter in die fol­gen­de Funk­ti­on. Das ist auch der Grund, war­um im Bei­spiel die Funk­ti­ons­auf­ru­fe jeweils einen Para­me­ter weni­ger benö­ti­gen, ohne dass Sie irgend­was anders defi­nie­ren müs­sen. An die­ser Stel­le sei an das Lamb­da-Kal­kül erin­nert, das mit sei­nem ein­pa­ra­me­ter­i­gen Auf­bau hier­für die Grund­la­ge ist.

Level up: Funktionen höherer Ordnung

Bereits der Ver­zicht auf ver­än­der­li­che Zustän­de bie­tet inhä­ren­te Sta­bi­li­täts­vor­tei­le, wäre aber mit eini­ger Dis­zi­plin auch in impe­ra­ti­ver Pro­gram­mie­rung zu errei­chen (in Java kann man bei kon­se­quen­ter Ver­wenundg von final durch­aus thread-siche­ren Code schrei­ben, das macht bloß kaum jemand). Die eigent­li­che Stär­ke von FP sind aber eine gro­ße Aus­drucks­mäch­tig­keit und ein Grad an Wie­der­ver­wen­dung, den sie in OOP selbst beim bes­ten Design nicht errei­chen wer­den. Und das wird vor allem durch ein bei FP unab­ding­ba­res Kon­zept ermög­licht: Funk­tio­nen höhe­rer Ord­nung. Das sind ganz ein­fach Funk­tio­nen, deren Para­me­ter und/oder Rück­ga­be­wert selbst Funk­tio­nen sind.

Ein­gangs habe ich Ihnen erläu­tert, dass die Ein­fach­heit des Lamb­da-Kal­küls zu einer wich­ti­gen Eigen­schaft von FP-Spra­chen führ­te: dass Funk­tio­nen selbst Typen sind. Das ent­spricht auch mathe­ma­ti­schem Usus und Bedarf. Den­ken Sie z. B. an die Mengenlehre.

Wor­um geht es bei Funk­tio­nen höhe­rer Ord­nung? Nicht nur in der Mathe­ma­tik, son­dern auch in Anwen­dun­gen des all­täg­li­chen Gebrauchs hat man regel­mä­ßig den Bedarf, Daten­men­gen umzu­wan­deln, zu fil­tern, zu sor­tie­ren, zu kumu­lie­ren u.ä. In der impe­ra­ti­ven Pro­gram­mie­rung macht man so etwas eher umständ­lich mit Schlei­fen, Varia­blen etc. Durch die­sen Auf­wand sind über die Jahr­zehn­te ver­mut­lich Mil­lio­nen Per­so­nen­jah­re ver­geu­det wor­den. Das Bewusst­sein für die unsin­nig hohe Red­un­danz und Feh­ler­an­fäl­lig­keit die­ses Ansat­zes hat z. B. zu OO-Pat­terns wie „Tem­p­la­te Method“, „Stra­tegy“ u.ä. geführt. Aber die­se Mus­ter sind Behelfs­lö­sun­gen und kurie­ren eigent­lich nur Sym­pto­me. Die Dia­gno­se bleibt: Impe­ra­ti­ve Pro­gram­mie­rung hält sich zu sehr mit dem „Wie“ auf. Funk­tio­na­le Pro­gram­mie­rung küm­mert sich um das „Was“, ist also eher deklarativ.

Ich erläu­te­re das am klas­si­schen Bei­spiel der Men­gen­leh­re: Der Abbil­dung einer Men­ge auf eine ande­re. Wer im Mathe­ma­tik­un­ter­richt nicht geschla­fen hat, weiß, dass man dazu eine Abbil­dungs­funk­ti­on benö­tigt, die jeweils ein Ele­ment der Quell­men­ge in ein Ele­ment der Ziel­men­ge trans­for­miert.

Schau­en wir uns im Code an, wie wir für die Men­ge der natür­li­chen Zah­len 1 bis 100 die Men­ge ihrer Qua­drat­zah­len ermit­teln kön­nen. Jetzt fan­gen wir all­mäh­lich an, rich­tig funk­tio­nal zu programmieren:

In die Zwi­schen­ab­la­ge kopieren

Wenn Sie bis­her nur impe­ra­tiv pro­gram­miert haben, wer­den Sie ver­mut­lich nicht mit einer so ein­fa­chen Lösung gerech­net haben. Sie haben dabei zwei neue Eli­xir-Daten­ty­pen und ihre ers­te Funk­ti­on höhe­rer Ord­nung kennengelernt:

  • [1, 4, …, 10000] ist eine Dar­stel­lung eines sehr grund­le­gen­den FP-Daten­typs: Lis­ten (wie man mit Lis­ten arbei­tet, schau­en wir uns wei­ter unten bei Rekur­si­on an)
  • 1..100 stellt eine Ran­ge dar, die für Enu­me­ra­ti­on und Strea­ming (wir kom­men in spä­te­ren Tei­len der Serie zum Strea­ming; dann kön­nen wir die Qua­drat­zah­len aller natür­li­chen Zah­len ermit­teln) ver­wen­det wer­den kann. Sie ent­spricht im Wesent­li­chen einer Lis­te [1,2,.. 99, 100]
  • map ist eine von vie­len Funk­ti­on aus dem Modul Enum. Sie bil­det eine auf­zähl­ba­re Menge(daher Enum für Enu­me­ra­ti­on) auf eine ande­re ab. Um das machen zu kön­nen, benö­tigt sie als 2. Para­me­ter (der ers­te ist die Aus­gangs­men­ge und kommt über die Pipe rein) die Abbil­dungs­funk­ti­on für die Ele­men­te. Das macht sie zu einer Funk­ti­on höhe­rer Ordnung

Das Beson­de­re hier ist also der Para­me­ter der map-Funk­ti­on. Hier soll­ten Sie unse­re selbst geschrie­be­ne Qua­drat-Funk­ti­on sqr wie­der­erken­nen, die für die eigent­li­che Trans­for­ma­ti­on der Ele­men­te sorgt. Die etwas komi­sche Schreib­wei­se &map/1 wird hier benö­tigt, um eine ein­deu­ti­ge Refe­renz auf die Funk­ti­on mit einem Para­me­ter zu erzeu­gen. Wir wer­den gleich ele­gan­te­re Nota­ti­ons­me­tho­den kennenlernen.

Was mache ich, wenn ich die Zah­len mit drei mul­ti­pli­zie­ren möch­te (war­um auch immer)? Am liebs­ten wür­de ich unse­re Mul­ti­pli­ka­ti­ons­funk­ti­on times/2 wie­der­ver­wen­den. Aber die benö­tigt im Gegen­satz zu sqr zwei Para­me­ter. Nun, ganz ein­fach, denn den zwei­ten Para­me­ter ken­nen wir ja bereits: 3. So kön­nen wir ihn mitgeben:

In die Zwi­schen­ab­la­ge kopieren

Ist das cool, oder was? >&1 ist der Platz­hal­ter des erhal­te­nen Para­me­ters (map stellt nur einen Para­me­ter, das zu map­pen­de Ele­ment, zur Ver­fü­gung, aber wir wer­den auch kom­ple­xe­re Funk­tio­nen höhe­rer Ord­nung ken­nen­ler­nen). Wir hät­ten im obe­ren Bei­spiel statt &sqr/1 also auch &sqr(&1) schrei­ben kön­nen. Ver­wen­den Sie ein­fach die Schreib­wei­se, die Ihnen und Ihren Kol­le­gen les­ba­rer erscheint.

Spie­len wir noch ein wenig mit der Pipe und Funk­tio­nen höhe­rer Ord­nung rum. Sagen wir, wir wol­len nur die unge­ra­den Viel­fa­chen von drei haben, müs­sen also irgend­wie fil­tern. Kön­nen Sie raten, wie die Funk­ti­on heißt und wie wir sie ein­bau­en? Richtig.

In die Zwi­schen­ab­la­ge kopieren

Es funk­tio­niert. Übri­gens hät­te man in die­sem Fall die filter-Funk­ti­on auch vor die map-Funk­ti­on stel­len kön­nen, da nur die Mul­ti­pli­ka­ti­on unge­ra­der Zah­len selbst unge­ra­de ist. Für die meis­ten Anwen­dungs­fäl­le spielt die seqen­ti­el­le Anord­nung der Pipe natür­lich schon eine Rol­le, die sich aus den Anfor­de­run­gen und dem suk­zes­si­ven Trans­for­ma­ti­ons­be­darf ergibt.

Sie haben dabei eine wei­te­re Mög­lich­keit ken­nen­ge­lernt, näm­lich eine anony­me Funk­ti­on inline zu defi­nie­ren: fn x -> … end. &rem/2 (für rema­in­der) ist eine Ker­nel-Funk­ti­on zur Divi­si­ons­rest-Ermitt­lung (in ande­ren Spra­chen auch als mod bekannt).

Das ist doch alles recht ein­fach, vor allem im Ver­gleich zu impe­ra­ti­vem Code. Aber abge­se­hen davon, dass wir die Pipe jetzt in der übli­che­ren und über­sicht­li­che­ren Schreib­wei­se unter­ein­an­der ver­wen­den, drückt unser Code schon wie­der etwas zu viel „Wie“ statt „Was“ aus und fängt syn­tak­tisch an wie Java­Script aus­zu­se­hen (noch nicht wirk­lich, aber weh­ret den Anfängen …).

Auf der Abs­trak­ti­ons­ebe­ne die­ser Code­stre­cke möch­te ich aber eigent­lich nur dekla­rie­ren, dass ich unge­ra­de Zah­len brau­che, mich und den Code­le­ser nicht damit beschäf­ti­gen, wie ich fest­stel­le, ob eine Zahl unge­ra­de ist. Zumal eine sol­che Funk­ti­on womög­lich ander­wei­tig ver­wend­bar ist. Blei­ben Sie mög­lichst immer auf einem Abs­trak­ti­ons­ni­veau. Das kön­nen wir also mit mehr Wie­der­ver­wen­dungs­po­ten­zi­al und Klar­heit schrei­ben. Unser ers­tes funk­tio­na­les Refac­to­ring:

In die Zwi­schen­ab­la­ge kopieren

Bit­te sehen Sie für den Moment vom fach­li­chen Unsinn des Bei­spiels ab. Sie wer­den sich sicher bes­se­re Anwen­dungs­mög­lich­kei­ten vor­stel­len kön­nen, soll­ten aber schon jetzt einen Ein­druck davon erhal­ten haben, wie aus­drucks­stark der Code sein könn­te, den Sie bereits mit den bis­her erlern­ten Sprach­mit­teln für Ihre Anwen­dung schrei­ben könnten.

Dabei haben wir eines der mäch­tigs­ten und unver­zicht­ba­ren Sprach­mit­tel der funk­tio­na­len Pro­gram­mie­rung noch gar nicht ken­nen­ge­lernt: Rekursion.

Play it again, Sam! Die Macht der Rekursion und grenzenloser Datentypen

Man­cher von Ihnen hat sicher schon rekur­si­ve (d.h. sich selbst auf­ru­fen­de) Funk­tio­nen ent­wi­ckelt oder gese­hen. Vie­le haben Angst vor Rekur­si­on und/oder schlech­te Erfah­run­gen damit gemacht: Stack­über­läu­fe, Daten­typ-Über­läu­fe, End­los­schlei­fen sind typi­sche Effek­te bei Rekur­si­on in impe­ra­ti­ver Pro­gram­mie­rung.

In FP ist Rekur­si­on kein Schreck­ge­spenst, son­dern erleich­tert das Leben erheb­lich. Zum einen lernt man bei FP schnell, wie ein Mathe­ma­ti­ker zu den­ken, zum ande­ren sind FP-Spra­chen auf rekur­si­ve Funk­tio­nen opti­miert, weil unfass­bar vie­le Pro­ble­me sehr ele­gant rekur­siv lös­bar sind. Das sehen wir jetzt.

Wie defi­niert ein Mathe­ma­ti­ker Funk­tio­nen (und oft auch Men­gen, z. B. die Pea­no-Zah­len)? Er denkt nicht in Schlei­fen, Sum­men­va­ria­blen, Aus­stiegs­be­din­gun­gen o.ä. wie ein impe­ra­ti­ver Pro­gram­mie­rer, son­dern er unter­schei­det Basis­fäl­le und rekur­si­ve Defi­ni­tio­nen. Über­le­gen Sie kurz, wie Sie bis­her die Fakul­täts-Funk­ti­on imple­men­tiert hät­ten (womög­lich mit einer Varia­ble, die in einer Schlei­fe hoch­mul­ti­pli­ziert wird, also impe­ra­tiv). Und jetzt schau­en Sie, wie Sie das in Eli­xir machen kön­nen bzw. sollten:

In die Zwi­schen­ab­la­ge kopieren

Sie kodie­ren auf ein­fachs­te Wei­se, wie ein Mathe­ma­ti­ker die Funk­ti­on defi­niert: x! = {1 für x = 0; x*(x-1)! für alle natürlichen Zahlen}. Wie das Unter­schei­den die­ser bei­den Fäl­le tech­nisch funk­tio­niert, schau­en wir uns gegen Ende an. Stich­wor­te sind Pat­tern Matching und Value Extra­c­tion. Im Moment wür­de uns das zu sehr aufhalten.

Die­ser dekla­ra­ti­ve Pro­gram­mier­stil ist viel­leicht unge­wohnt, aber extrem ein­fach und sicher, sobald man sich einen ent­spre­chen­den Denk­stil ange­eig­net hat. Wir wer­den spä­ter bei der Lis­ten­re­kur­si­on noch sehen, dass das nicht nur für offen­sicht­lich rekur­si­ve mathe­ma­ti­sche Funk­tio­nen hilf­reich ist.

Durch ihrer Opti­mie­rung auf Rekur­si­on hat die FP außer­dem einen wei­te­ren unschlag­ba­ren Vor­teil gegen­über den übli­chen Ver­däch­ti­gen: Exak­te­re Daten­ty­pen. Mit „alle natür­li­chen Zah­len“ meint der Mathe­ma­ti­ker nicht nur die, für die das Ergeb­nis zufäl­lig in 64 Bit passt. Haben Sie schon mal in Java o.ä. Spra­chen ver­sucht, die Fakul­tät von 1000 aus­zu­rech­nen? „Geht nicht!“ gilt nicht. Was ich scho­ckie­rend fin­de: Die meis­ten Code­bei­spie­le, die man zuhauf im Inter­net und in Lehr­bü­chern fin­det, ver­wen­den ganz selbst­ver­ständ­lich den Daten­typ long und gehen oft noch nicht ein­mal auf die Rah­men­be­din­gung ein, dass damit nur bis zu 20! gerech­net wer­den kann – geschwei­ge denn, wie man die­se sprach­ent­wurfs-beding­te Gren­ze über­win­den könn­te, denn es soll ja vor­kom­men, dass fach­li­che Anfor­de­run­gen auch jen­seits sol­cher will­kür­li­cher Gren­zen rei­chen. Lei­der wird auf die­se Wei­se ange­hen­den Pro­gram­mie­rern das Fach ver­mit­telt. Da wun­dert einen nichts mehr!

Schau­en wir ein­fach, was Eli­xir von begrenz­ten Daten­ty­pen hält (ähn­li­ches gilt für Has­kell, Sca­la u.a. FP-Sprachen):

In die Zwi­schen­ab­la­ge kopieren

Sie kön­nen sich vor­stel­len, dass das ganz schön vie­le Zif­fern sind. Um ein Haar hät­te ich sie gezählt, da fiel mir ein, dass ein FP-Ent­wick­ler so etwas nicht nötig hat. Das hier soll­ten Sie mitt­ler­wei­le pro­blem­los verstehen:

In die Zwi­schen­ab­la­ge kopieren

Wit­zi­ger­wei­se ent­spricht die Anzahl der Stel­len der Tele­fon­num­mer mei­nes Eltern­hau­ses (ich kom­me gebür­tig aus einem klei­nen Ort); viel­leicht ist es ja mei­ne per­sön­li­che 42 …

Hier haben Sie auch noch mal ein Best Prac­ti­ce: Statt die ggf. uner­klär­li­che Pipe to_string |> String.length direkt zu ver­wen­den, wei­sen wir sie als adhoc-Funk­ti­on einem Ali­as zu, der aus­drückt, was sie macht: Sie ermit­telt die Anzahl der Stel­len einer Zahl. Die Regeln von „Clean Code“ las­sen sich auch auf FP anwenden.

In Anleh­nung an die an „Casa­blan­ca“ ange­lehn­te Kapi­tel­über­schrift soll­ten Sie jetzt all­mäh­lich den­ken „Ich glau­be, das ist der Beginn einer wun­der­ba­ren Freundschaft“.

Elegant? Definitiv! Exakt? Verblüffend! Aber auch schnell?

Aber ele­gan­te, bei Ent­wick­lern belieb­te Pro­gram­mier­spra­chen haben oft eine teu­re Kehr­sei­te, man den­ke an Ruby. Falls Sie also jetzt im typi­schen „Java ist aber schnel­ler und C noch mehr“-Reflex den­ken „Ja, aber die Per­for­mance …!“: Bei fac(1000) zuckt die REPL noch nicht mal, das Ergeb­nis (immer­hin das Pro­dukt von 1000 Mul­ti­pli­ka­tio­nen mit einer Län­ge von 2568 Zif­fern) steht da in Sekun­den­bruch­tei­len (und die längs­te Zeit benö­tigt ver­mut­lich die Aus­ga­be der rie­si­gen Zahl auf dem Dis­play). Ähn­lich ist es bei fac(10000) (35660 Stel­len). Erst bei fac(30000)(121288 Stel­len exakt berech­net!) fängt die Berech­nung an, in den spür­ba­ren Bereich meh­re­rer Sekun­den zu gehen. Der Pro­zes­sor ist auch nur ein Mensch. Aber wir haben ja auch noch nicht über die Ver­tei­lung von Arbeits­last auf meh­re­re Pro­zes­sor­ker­ne gespro­chen. Das The­ma Par­al­le­li­sie­rung heben wir uns lie­ber für weni­ger tri­via­le Pro­ble­me in fol­gen­den Bei­trä­gen auf (z. B. rekur­si­ve Ermitt­lung opti­ma­ler Züge bei Brett­spie­len, für deren kom­bi­na­to­ri­schen Explo­si­on FP und Neben­läu­fig­keit a la Akto­ren per­fek­te Lösungs­mit­tel sind). Das wäre hier zwar auch schon mög­lich, aber bei einem so lächer­lich klei­nen Berech­nungs­pro­blem nun wirk­lich mit Kano­nen auf Spat­zen geschos­sen. Wann berech­net man schon mal sol­che Fakul­tä­ten in die­sen Grö­ßen­ord­nun­gen? Der Punkt ist: Man soll­te es ein­fach kön­nen, wenn man es braucht.

Natür­lich funk­tio­nie­ren auch die nor­ma­len Rechen­funk­tio­nen in die­sen gigan­ti­schen Größenordnungen:

In die Zwi­schen­ab­la­ge kopieren

Zur Erin­ne­rung: 77338 ist nicht das Ergeb­nis von 20000! – 10000!, son­dern die Anzahl der Stel­len des im ers­ten Schritt der Pipe kor­rekt berech­ne­ten Ergebnisses!

Ich fin­de es ziem­lich beein­dru­ckend, so etwas im Sekun­den­be­reich aus­rech­nen zu kön­nen, wäh­rend die ver­meint­lich auf die Pro­zes­sor­ar­chi­tek­tu­ren unse­rer PCs hoch­op­ti­mier­ten Com­pi­ler vie­ler Pro­gram­mier­spra­chen jen­seits von 2^63 die Hufe hoch­rei­ßen (hat übri­gens gera­de mal 19 Stel­len!) – und wirk­lich schnell ist Rekur­si­on in Java auch nicht, wes­we­gen vie­le ja lie­ber auf Schlei­fen zurückgreifen.

Ich weiß nicht, wie die Com­pi­ler­bau­er von Eli­xir, Erlang, Has­kell, Sca­la und Co. die­se Gren­zen­lo­sig­keit mit die­ser Per­for­manz schaf­fen, aber das schö­ne ist ja gera­de: Als FP-Pro­gram­mie­rer soll­te und muss ich mich nicht mit so mon­dä­nen Pro­ble­men wie Pro­zes­sor-Wort­brei­te oder Stack­tie­fe auf­hal­ten las­sen, son­dern kann mich auf die zu lösen­den Pro­ble­me kon­zen­trie­ren. Selbst wenn nur die­ser Vor­teil für FP sprä­che, wäre er eigent­lich schon zwingend.

Aber immer Kopieren statt Ändern ist doch langsam?

Vie­le argu­men­tie­ren auch, dass es doch ein Per­for­mance-Fres­ser sein muss, dass in FP stän­dig neue Daten­struk­tu­ren ent­ste­hen, weil bestehen­de ja nicht geän­dert wer­den dür­fen. Aber das stimmt nicht. Zum einen soll­te sich jeder, der so klein­tei­lig opti­mie­ren will, die berühm­ten Wor­te von Donald Knuth in Erin­ne­rung rufen: „Pre­ma­tu­re opti­miza­ti­on is the root of all evil“. Das zeigt sich auch dras­tisch, wenn man bedenkt, das bei Mul­ti­th­re­a­ding durch die Syn­chro­ni­sie­rung gemein­sam benutz­ten Spei­chers ein Viel­fa­ches der durch ver­än­der­li­che Zustän­de ver­meint­lich gespar­ten Zeit ver­geu­det wird.

Außer­dem sind vie­le Daten­struk­tu­ren bei FP über­haupt erst auf­grund der Nur-Lese-Eigen­schaft opti­mier­bar: Wenn einer LinkedList ein neu­es Ele­ment vor­an­ge­stellt wird, besteht die neue Lis­te nur aus die­sem Ele­ment und dem Link auf die alte Lis­te (die sich ja nie ändern kann). Und schließ­lich kann FP fast aus­schließ­lich mit dem (bekannt­lich schnel­len) Stack arbei­ten und hat kaum ver­gleichs­wei­se teu­re Heap-Auf­ru­fe. Natür­lich gibt es auch in FP die Mög­lich­keit, ver­än­der­li­che Zustän­de fest­zu­hal­ten. In Eli­xir geschieht das u.a. über Aktoren/Prozesse, die wie­der­um hoch­op­ti­miert bzgl. der Nut­zung ihres abso­lut geschütz­ten loka­len Spei­chers sind.

Rekursive Listenverarbeitung

Viel­leicht wird man­cher ein­wen­den, dass die Welt der rein zah­len­men­gen-basier­ten Mathe­ma­tik natur­ge­mäß ein Heim­spiel für funk­tio­na­le Pro­gram­mie­rung ist, aller­dings für typi­sche tech­ni­sche oder Busi­ness-Anwen­dun­gen nur eine rela­tiv klei­ne Rol­le spielt. D’accord!

Eine typi­sche­re Pro­ble­ma­tik der meis­ten Anwen­dungs­do­mä­nen ist das Ver­ar­bei­ten von Daten­men­gen (Sor­tie­ren, Fil­tern, Sum­mie­ren, Opti­mie­ren etc.). Natür­lich geben die exis­tie­ren­den Funk­tio­nen höhe­rer Ordung (z.B. des Enum-Moduls) dafür schon unglaub­lich viel her. Mitt­ler­wei­le soll­ten Sie eine gro­be Vor­stel­lung davon haben, was die­se anfangs schon gezeig­te Code­stre­cke macht und wie Sie die auf­ge­ru­fe­nen fach­li­chen Funk­tio­nen imple­men­tie­ren könnten:

In die Zwi­schen­ab­la­ge kopieren

Aber ich möch­te Ihnen gegen Ende die­ses lan­gen Bei­trags noch bei­brin­gen, wie Sie rekur­siv mit Lis­ten arbei­ten und dabei selbst belie­bi­ge Funk­tio­nen höhe­rer Ord­nung schrei­ben kön­nen, die Sie dann für ver­schie­de­ne fach­li­che Zwe­cke wie­der­ver­wen­den können.

Stel­len Sie sich vor, Sie sol­len den höchs­ten Jah­res­um­satz einer gege­be­nen Men­ge von Kun­den ermit­teln und sehen noch wei­te­re ähn­li­che Auf­ga­ben auf sich zu kom­men. Sie wol­len daher von vorn­her­ein eine Funk­ti­on höhe­rer Ord­nung schrei­ben, die nicht auf Kun­den beschränkt ist, son­dern belie­bi­ge maxi­ma­le Wer­te aus belie­bi­gen Men­gen mit belie­bi­gen Attri­bu­ten ermit­telt. Über­le­gen Sie kurz, wie Sie das ange­hen wür­den (z. B. mit OOP-Mit­teln, ger­ne aber auch mit den schon bekann­ten FP-Mitteln).

Ein Tip, den ich auch bei OOP schon immer gege­ben habe und den man mit „Test First!“-Ansätzen wie TDD und BDD her­vor­ra­gend umset­zen kann: Über­le­gen Sie immer, wie Sie selbst etwas ger­ne benut­zen wür­den, wenn es das schon als API gäbe. Das führt Sie in der Regel zu gut ver­wend­ba­ren Schnitt­stel­len, in unse­rem Fall also Funk­tio­nen. Wie wür­den Sie das Pro­blem des maxi­ma­len Jah­res­um­sat­zes also in idio­syn­kra­ti­scher Eli­xir-Syn­tax lösen wol­len? Ich schla­ge fol­gen­des vor:

In die Zwi­schen­ab­la­ge kopieren

Spre­chen­der und dabei wie­der­ver­wend­ba­rer kann Code kaum sein. Jeder, der das liest, wüss­te sofort, dass hier der maxi­ma­le Wert unter den Jah­res­um­sät­zen der Kun­den gesucht wird (kun­den steht hier stell­ver­tre­tend für eine Men­ge von Kun­den, die selbst schon als Funk­ti­ons­pa­ra­me­ter oder aus dem vor­de­ren Teil der Pipe kom­men könnte).

Man könn­te sich auch schon etwas unter jah­res­um­satz vor­stel­len, wenn man an die map-Bei­spie­le denkt: Das ist näm­lich eine Funk­ti­on, die den Jah­res­um­satz für jeweils einen Kun­den ermit­telt. Wie sie das macht, ist an die­ser Stel­le nicht wich­tig. Denk­bar wäre, dass sie ein­fach ein Attri­but aus einer Daten­struk­tur zurück­gibt, 12 Monats­um­sät­ze auf­ad­diert, eine Daten­bank-Abfra­ge macht etc. (Auch eine Daten­bank ist in FP eine Samm­lung von Funk­tio­nen. Den­ken Sie hier bit­te nicht an die Opti­mie­rung von DB-Abfra­gen, es gibt gera­de in dekla­ra­ti­ver FP-Pro­gram­mie­rung aus­rei­chend Mög­lich­kei­ten, der­lei Pro­ble­me zu lösen; das wür­de die­sen Bei­trag aber end­gül­tig sprengen.)

Es spielt an die­ser Stel­le also schlicht­weg kei­ne Rol­le, wie die Funk­ti­on das Pro­blem löst, den Wert zu ermit­teln, weil wir sie gera­de nicht imple­men­tie­ren, son­dern nur ver­wen­den. Ent­schei­dend ist nur Ihre Signa­tur, in die­sem Fall (Kunde) -> Jahresumsatz. Da wir max als gene­ri­sche Funk­ti­on höhe­rer Ord­nung anle­gen, kön­nen wir sogar ver­all­ge­mei­nern zu (Datenelement) -> VergleichbarerWert. Hier kommt uns erneut ent­ge­gen, dass Eli­xir nicht getypt ist. Das bedeu­tet: Ohne gro­ße Regu­la­ri­en kann jeder eine belie­bi­ge Lis­te in unse­re Funk­ti­on max geben, solan­ge er uns eben­falls eine Funk­ti­on gibt, die für jedes Ele­ment die­ser Lis­te ver­gleich­ba­re Wer­te ermit­telt. So wird in FP Wie­der­ver­wen­dung erzeugt. Viel­leicht anfangs unge­wohnt, aber man gewöhnt sich recht schnell dar­an. Man kann das zwar auch for­ma­ler machen (in Eli­xir über soge­nann­te Pro­to­kol­le, ver­gleich­bar den Inter­faces in Java), aber für unse­ren und vie­le ähn­li­che Zwe­cke tut es das unge­typ­te Ver­fah­ren wunderbar.

Was statt wie ist also sehr hilf­reich. Aber schau­en wir uns jetzt aber an, wie wir die­se Funk­ti­on max imple­men­tie­ren müss­ten, wenn es sie noch nicht gäbe. Aber auch das machen wir wie­der über­wie­gend dekla­ra­tiv. Noch­mal zur Erin­ne­rung: Die Funk­ti­on erhält eine Men­ge uns unbe­kann­ter Daten­ele­men­te (ich sage bewusst nicht Objek­te, weil wir in FP eher von Daten­struk­tu­ren reden, nicht von Klas­sen) und eine Funk­ti­on, die für jeweils eines die­ser Ele­men­te einen Wert ermit­teln kann. Auf­ga­be der max-Funk­ti­on ist es, den größ­ten die­ser Wer­te zu ermit­teln (und zurückzugeben).

Könn­ten Sie es schon rekur­siv lösen? Sie müs­sen wie­der wie ein Mathe­ma­ti­ker den­ken (tei­le und herr­sche) und kön­nen drei Fäl­le defi­nie­ren, deren letz­ter mit Lis­ten­re­kur­si­on arbei­tet. Ver­su­chen Sie doch mal die fol­gen­den drei Funk­ti­ons­de­fi­ni­onen zu ver­ste­hen, bevor ich Ihnen die feh­len­den Syn­tak­ex­le­men­te erläutere:

In die Zwi­schen­ab­la­ge kopieren

Nach Jah­ren müh­se­lig red­un­dan­ter Pro­gram­mie­rung mit Schlei­fen und tem­po­rä­ren Varia­blen mag es Sie wun­dern bis frus­trie­ren, aber das ist wirk­lich alles, was Sie defi­nie­ren müs­sen. Und zwar ein für alle Mal. Sie kön­nen sich auf die­se max-Funk­ti­on ver­las­sen (die Sie natür­lich im ech­ten Leben mit Unit-Tests gesi­chert haben) und sich ab jetzt auf fach­li­che Logi­ken und sinn­vol­le Imple­men­tie­run­gen von valFn beschränken.

Wie funk­tio­niert max(elemente, wertfunktion), mathe­ma­tisch gedacht? Im Prin­zip wie die Fakul­tät wei­ter oben. Der maxi­ma­le Wert zu einer Men­ge von Ele­men­ten ist defi­niert als:

  • Unde­fi­niert, wenn die Men­ge leer ist (wir hät­ten die­se obers­te Defi­ni­ti­on auch weg­las­sen kön­nen, aber dann hät­te es ggf. Lauf­zeit-Excep­ti­ons gege­ben, jetzt muss sich der Auf­ru­fer damit aus­ein­an­der­set­zen, dass es ggf. auch kei­nen Wert gibt, wenn es kein Ele­ment gibt, alter­na­tiv hät­te man einen optio­na­len Para­me­ter für den Default­wert ein­füh­ren können)
  • Der Wert zum ein­zi­gen Ele­ment, wenn es nur ein Ele­ment gibt (Basis­fall)
  • Der grö­ße­re Wert aus dem Wert des ers­ten Ele­ments und dem maxi­ma­len Wert aller übri­gen Ele­men­te (Rekur­si­ons­fall)

Ver­glei­chen Sie die­se drei Fäl­le mit den drei imple­men­tier­ten Metho­den. Sie soll­ten zumin­dest erah­nen kön­nen, dass sie genau ana­log sind. Zum Ver­ständ­nis des Codes feh­len Ihnen noch zwei zusam­men­ge­hö­ri­ge Kon­zep­te, die ich Ihnen zum Abschluss die­ses Bei­trags noch antun muss.

Pattern Matching und Wertextraktion

Wenn Sie sich die drei Funk­ti­ons­de­fi­ni­tio­nen des letz­ten Bei­spiels anse­hen, könn­ten Sie sich fol­gen­des fra­gen: Wenn Eli­xir unge­typt ist und Funk­tio­nen somit nur nach Funk­ti­ons­na­me und Anzahl der Para­me­ter unter­schei­den kann, wie kann es denn sein, dass es drei Funk­tio­nen namens max/2 gibt? Wie ent­schei­det das Sys­tem, wel­che Metho­de die rich­ti­ge ist?

Hier kommt ein gän­gi­ges Mus­ter der FP zum Ein­satz: Pat­tern Matching. Aus einer Rei­he for­mal gleich­wer­ti­ger Alter­na­ti­ven (max/2, fakultaet/1) wird die jeweils ers­te benutzt, deren Para­me­ter­mus­ter den beim kon­kre­ten Auf­ruf über­ge­be­nen Para­me­tern ent­spricht. Die­ses Kon­zept ist unglaub­lich mäch­tig, da es weit über Typi­sie­rung und dem aus ande­ren Spra­chen bekann­ten Over­loa­ding nach Typen hin­aus­geht. Man kann damit belie­big vie­le spe­zia­li­sier­te Funk­tio­nen imple­men­tie­ren, wo man anders­wo gro­ße switch-Blö­cke im Funk­ti­ons­bo­dy bräuch­te. Das zuvor erwähn­te Phoe­nix macht sich das z. B. bei der Rou­ter-Imple­men­tie­rung zunutze.

In die­sem Fall nut­zen wir fol­gen­de Pat­tern, die für die Lis­ten­ver­ar­bei­tung typisch sind:

  • [] stellt eine lee­re Lis­te dar
  • [head|tail] stellt eine nicht lee­re Lis­te dar. Dabei ist head das ers­te Ele­ment und tail eine Lis­te der übri­gen Ele­men­te. Das ist ein sehr per­for­man­tes Pat­tern für die rekur­si­ve Ver­ar­bei­tung der in FP-Spra­chen fast aus­schließ­lich benutz­ten LinkedLists
  • [head|[]] ist ein Bei­spiel für eine Kom­bi­na­ti­on die­ser bei­den Pat­tern. Es besagt, dass es zwar ein Kop­f­ele­ment gibt, aber der tail eine lee­re Lis­te. Wir benut­zen das, um unse­ren Basis­fall einer ein­ele­men­ti­gen Lis­te zu defi­nie­ren. Des­halb muss die­ser auch vor dem Rekur­si­ons­fall ste­hen, weil des­sen Mus­ter auch auf die ein­ele­men­ti­ge Lis­te pas­sen wür­de. (Wir könn­ten auf die­se Wei­se übri­gens auch Pat­ter für Lis­ten mit z. B. drei Ele­men­ten und/oder bestimm­ten Wer­ten der Ele­men­te defi­nie­ren, wenn das fach­lich sinn­voll ist; hier brau­chen wir es nicht)

Pas­send zu Pat­tern Matching gibt es Value Extra­c­tion. Wenn Sie sich die Funk­tio­nen 2 und 3 anschau­en, wer­den Sie fest­stel­len, dass im Funk­ti­ons­rumpf der ers­te Para­me­ter nicht als gan­zes ange­spro­chen wird (ja, er hat nicht mal einen Namen wie elements, weil wir die­sen nicht brau­chen). Statt­des­sen wird mit den per Pat­tern zer­schnit­te­nen Teil­wer­ten gear­bei­tet, die wir ja auch in unse­rer Defi­ni­ti­on brau­chen. Die­ses Fea­ture von FP-Spra­chen ist extrem hilf­reich und ver­ein­facht das Den­ken in und das Umset­zen von sau­be­ren mathe­ma­ti­schen Defi­ni­tio­nen – nicht nur, aber vor allem bei Rekursion.

Wenn Sie die Imple­men­tie­rung der drit­ten Metho­de, also des Rekur­si­ons­falls genau­er betrach­ten, wer­den sie die bei­den Mus­ter auch dort wie­der­fin­den. Zunächst aller­dings ler­nen Sie ein neu­es Sprach­kon­strukt ken­nen, das Tupel ({wert1, … wertn}). Die­se Daten­struk­tur ist sehr hilf­reich, wenn meh­re­re Wer­te gemein­sam trans­por­tiert wer­den müs­sen, z. B. als Rück­ga­be­wert einer Funk­ti­on. Hier machen wir es uns wie folgt zunut­ze: Wir müs­sen zwei Wer­te berech­nen: Näm­lich den Wert des head-Ele­ments (hier first genannt) und den maxi­ma­len aller Wer­te aus dem tail (hier remaining genannt).

Da vor allem das letz­te­re eine ggf. teu­re Funk­ti­on ist, wir die Ergeb­nis­se für das fol­gen­de Pat­tern-Matching aber zwei­mal brau­chen, berech­nen wir sie am Anfang, ver­pa­cken sie in ein Tupel und matchen die­ses dann gegen die zwei defi­nier­ten Fäl­le. Die­ses Matching machen wir wie­der­um mit case … do … end, Eli­xirs Vari­an­te zu Sca­las match-Funk­ti­on. Eine Art switch, aber viel mäch­ti­ger. Im ers­ten Fall kön­nen Sie auch sehen, dass man die Matches mit soge­nann­ten Guards ver­fei­nern kann. Die Match-Klau­sel wür­de näm­lich für bei­de Fäl­le pas­sen. Den ers­ten wol­len wir aber nur anwen­den, wenn das ers­te Ele­ment einen grö­ße­ren Wert hat als der Rest.

Nur noch eine Erläu­te­rung, da sie sich viel­leicht über die Unders­cores (_) wun­dern. Mit die­sen kann man aus­drü­cken, dass das Ele­ment an die­ser Stel­le für das Mus­ter kei­ne Rol­le spielt und/oder man es im Code nicht ver­wen­den möch­te. Da der Wert des ers­ten Ele­ments die ers­te Bedin­gung nicht erfüllt hat, brau­chen wir ihn im zwei­ten Fall nicht und kön­nen unge­prüft den ande­ren Wert zurückgeben.

So einfach wie möglich, aber nicht einfacher!

Viel kom­pli­zier­ter als die oben gezeig­te Imple­men­tie­rung von max soll­ten sau­be­re FP-Funk­tio­nen typi­scher­wei­se nicht wer­den – im Gegen­teil: Man soll­te sich ange­wöh­nen, bei zu lan­gen Metho­den zu fra­gen, ob sie nicht meh­re­re Din­ge ver­mi­schen und man den Code noch ein­fa­cher machen kann.

Und das machen wir auch hier: Im letz­ten Bei­spiel könn­te man argu­men­tie­ren, dass das Ver­mi­schen der Wert­ermitt­lung und der Ermitt­lung des Maxi­mal­werts in einer Funk­ti­on max/2 eigent­lich ein wenig red­un­dant ist, weil es ja schon eine Funk­ti­on Enum.map/2 zum Umwan­deln von Wer­te­men­gen gibt. In der Tat hät­te man auch schrei­ben können:

In die Zwi­schen­ab­la­ge kopieren

Sie kön­nen das inzwi­schen lesen: Zunächst wer­den die Kun­den in ihre Jah­res­um­sät­ze trans­for­miert (ge„map“t). Danach reicht eine Funk­ti­on max/1, die den größ­ten Wert aus einer Lis­te von Wer­ten bestimmt. Natür­lich wäre max/1 etwas einfacher:

In die Zwi­schen­ab­la­ge kopieren

Bei­des sind vali­de Alter­na­ti­ven. Am Ende zählt, wel­cher Code les­ba­rer ist. Und ich per­sön­lich fin­de, dass max(jahresumsatz) an der Auf­ruf­stel­le eigent­lich kla­rer den Sinn (das „Was“) aus­drückt: Wir wol­len den größ­ten Jah­res­um­satz haben. Die max/2‑Variante kann für den Auf­ru­fer also les­ba­rer sein. Trotz­dem müs­sen Sie die max/2‑Variante nicht so kom­pli­ziert imple­men­tie­ren wie oben. Ver­la­gern Sie ein­fach das „Wie“ mit map in eine zwei­te Funk­ti­on und schrei­ben Sie unter Ver­wen­dung der ohne­hin sinn­vol­len max/1‑Funktion ein­fach noch das hier:

In die Zwi­schen­ab­la­ge kopieren

So haben Sie den Vor­teil bei­der Ansät­ze. Am Ende geht es um den Code mit der bes­ten Les-/Wart­bar­keit. Sie soll­ten jetzt auch erken­nen, war­um ich anfangs die stei­le The­se auf­ge­stellt habe, dass FP viel bes­ser für Wie­der­ver­wen­dung geeig­net ist als OOP.

Grund­sätz­lich gilt: In FP gibt es vie­le Wege zum Ziel. Und das Ziel ist Code der best­mög­li­chen Qua­li­tät. Dabei ist die ein­fachs­te und ele­gan­tes­te (d.h. mathe­ma­tisch sau­bers­te) Imple­men­tie­rung meis­tens die beste.

Einfach, sicher, produktiv: Was will man mehr?

Wir sind am Ende unse­rer Ein­füh­rung ange­langt. Ich hof­fe, Sie kön­nen nach­voll­zie­hen, war­um ich FP im Titel mit die­sen Attri­bu­ten ver­se­hen habe. Eine gute FP-Funk­ti­on ist

  • ein­fach, indem ihre Arbeits­wei­se auf den ers­ten Blick nach­voll­zieh­bar ist
  • sicher, indem sie nichts ande­res als eine defi­nier­te Trans­for­ma­ti­on macht (dadurch übri­gens auch her­vor­ra­gend test­bar)
  • gut (wieder)verwendbar und ein­fach mit ande­ren Funk­tio­nen kom­po­nier­bar, z. B. in Pipes

FP erhöht die Qua­li­tät und Pro­duk­ti­vi­tät nach­hal­tig, indem Code ent­steht, der ein­heit­li­che Abs­trak­tio­nen besitzt und auf jeder Abs­trak­ti­ons­ebe­ne gut nach­voll­zieh­bar ist, ohne den Erstel­ler oder den War­tungs­ent­wick­ler men­tal zu über­for­dern.

Ich hof­fe, die Lek­tü­re hat sich für Sie gelohnt. Viel­leicht konn­te ich Sie etwas für FP und sei­ne Vor­tei­le inter­es­sie­ren. Auf jeden Fall soll­ten Sie jetzt etwas kon­kre­te­re Vor­stel­lun­gen von die­sem geheim­nis­um­wit­ter­ten Para­dig­ma haben. Viel­leicht mögen Sie selbst etwas rum­ex­pe­ri­men­tie­ren. Natür­lich ist das noch nicht alles, aber das wesent­li­che Rüst­zeug haben Sie jetzt. Und Eli­xir ist nicht nur die per­fek­te Spra­che zum Ein­stieg, son­dern auch die rich­ti­ge Wahl in Hin­blick auf Betriebsqualität.

Sie haben schon jetzt einen guten Ein­druck bekom­men, wie ein­fach man spre­chen­den Code wie den ein­gangs erwähn­ten ent­wi­ckeln kann. In den nächs­ten Bei­trä­gen die­ser Serie wer­de ich Ihnen zei­gen, wie man Funk­tio­nen als Grund­bau­stei­ne ver­blüf­fend ein­fach zu immer grö­ße­rer Seman­tik kom­po­nie­ren kann. Dabei las­sen sich auf ein­mal im Hand­um­dre­hen Pro­ble­me lösen, deren Kom­ple­xi­tät einem in impe­ra­ti­ver Pro­gram­mie­rung erheb­li­ches Kopf­zer­bre­chen gemacht hät­te, wenn man sich über­haupt her­an­ge­traut hätte.

Ger­ne wei­se ich auch schon dar­auf­hin, dass wir dem­nächst ein hands-on-Trai­ning für FP mit Eli­xir anbie­ten wer­den. Wir bie­ten das auch ger­ne als Inhouse-Trai­ning an oder beglei­ten Ihre Teams mit Coa­ching auf dem Weg in die ein­fa­che, siche­re und pro­duk­ti­ve Welt der funk­tio­na­len Programmierung.

Kommentare und Feedback gerne via Social Media:

Blei­ben Sie auf dem Lau­fen­den – mit dem monat­li­chen sepp.med Newsletter: