Sie befin­den sich hier:Start­sei­te/Blog/Typen, die die Welt retten

Typen, die die Welt retten

Lese­zeit: 11 Minu­ten
Typen, die die Welt retten

Nein, mit dem Titel sind nicht die Mit­ar­bei­ter von sepp.med gemeint, auch wenn wir eini­ges tun kön­nen, um mit Qua­li­tät Ihren Erfolg zu sichern. In die­sem Bei­trag geht es buch­stäb­lich um Typen, näm­lich um die Typ­sys­te­me von Pro­gram­mier­spra­chen, von denen vie­le durch man­geln­de Aus­drucks­kraft und fata­le Lücken den größ­ten Teil der Pro­ble­me ver­ur­sa­chen, die wir mit Soft­ware ver­bin­den. Den­ken sie allei­ne an Null-Poin­ter und Excep­ti­ons. Ich möch­te Sie heu­te mit eini­gen Kon­zep­ten ver­traut machen, wie sie nicht nur, aber vor allem in der Funk­tio­na­len Pro­gram­mie­rung ver­wen­det werden.

Ein Fehler für die Ewigkeit?

Kaum jemand kennt Tony Hoare, aber fast jeder Soft­ware­ex­per­te kennt das, was er selbst ein­mal als sei­nen „bil­li­on dol­lar mista­ke“ bezeich­net hat: Beim Ent­wurf des Typ­sys­tems von Algol W hat er 1965 das Kon­zept der Null-Refe­renz ein­ge­führt („sim­ply becau­se it was so easy to imple­ment“), das erlaub­te, dass eine ver­meint­li­che Zei­chen­ket­te auch mal kei­ne Zei­chen­ket­te war. Der Gesamt­scha­den dürf­te ver­mut­lich über den von ihm geschätz­ten Mil­li­ar­den-Bereich hin­aus gehen, denn seit nun­mehr einem hal­ben Jahr­hun­dert schla­gen wir uns mit den Fol­gen die­ser fata­len Ent­wurfs­ent­schei­dung her­um, die sich durch fast alle impe­ra­ti­ven Spra­chen zieht: Java ist neben der Geschwät­zig­keit sei­nes Codes vor allem für sei­ne NPEs (Null­Poin­ter­Ex­cep­ti­ons) berüch­tigt. C++ ist da aber auch kei­nen Deut bes­ser (viel­leicht sind C‑Programmierer ein­fach nur gebrann­te Poin­ter-Arith­me­tik-Kin­der). Und wer in Java­Script noch nicht auf ein (noch dazu oft in der Brow­ser-Kon­so­le ver­steck­tes) „Unde­fi­ned is not a func­tion“ gesto­ßen ist, hat ver­mut­lich nur äußerst tri­via­le Din­ge damit programmiert.

Passen Sie immer auf? Oder freuen Sie sich, wenn Ihr System mitdenkt?

Eds­ger Dijks­tra hat eine schö­ne Defi­ni­ti­on von Kom­pe­tenz for­mu­liert: „The com­pe­tent pro­gramm­er is ful­ly awa­re of the strict­ly limi­t­ed size of his own skull“. Ja, man kann es nicht oft genug sagen: Demut ist das A und O in unse­rer Pro­fes­si­on. Selbst­über­schät­zung (und schlech­tes Werk­zeug) sind – ver­bun­den mit eher zuneh­men­dem Ter­min­druck – der sichers­te Weg zu schlech­ter Qua­li­tät. Die­se Erkennt­nis war Aus­gangs­punkt der agi­len Bewe­gung. Und ist der Grund des Stre­bens nach immer bes­se­ren Ent­wick­lungs­tech­no­lo­gien. Der Wunsch nach moder­nen Spra­chen und Werk­zeu­gen hat nichts mit Cool­ness zu tun, wie ger­ne von den Ver­fech­tern eta­blier­ter Tech­no­lo­gien abge­tan wird, son­dern mit der Ein­sicht in die eige­ne Beschränkt­heit. (Davon abge­se­hen, ist es schon irgend­wie coo­ler, sich pro­fes­sio­nell zu ver­hal­ten als ande­re für den eige­nen Unwil­len zur Ver­än­de­rung zah­len zu lassen)

Natür­lich kann man Feh­ler wie Null­Poin­ter­Ex­cep­ti­ons ver­mei­den, genau so wie man auch in Java thread­si­che­ren Code schrei­ben kann. Aber das erfor­dert Bewusst­sein und Kon­zen­tra­ti­on, die dann an ande­ren Stel­len feh­len – und es geht erheb­lich zu Las­ten der Pro­duk­ti­vi­tät. Je kom­ple­xer Soft­ware wird, umso mehr muss man unnö­ti­ge men­ta­le Belas­tung ver­mei­den. Eine Mög­lich­keit, mit unaus­rott­ba­ren Feh­lern umzu­ge­hen, habe ich Ihnen vor kur­zem am Bei­spiel Elixir/Erlang/OTP erläu­tert: Die „Let it crash!“-Philosophie über­wach­ter, mes­sa­ge-basier­ter Akto­ren­sys­te­me. Wer mas­siv ver­teil­te dyna­mi­sche Sys­te­me mit diver­sen Soft­ware­stän­den und Vari­anz ent­wi­ckelt, kann sich eben nicht auf über­grei­fen­de Absi­che­rung durch einen Com­pi­ler ver­las­sen. Und alle Ver­su­che, über­grei­fen­de Sche­ma­ta zu eta­blie­ren, sind weit­ge­hend an der Kom­ple­xi­tät geschei­tert, weil sie nicht dyna­misch genug waren. Am Ende sind wir nach XML, WDSL, SOA und Co. bei JSON gelandet.

Aber wer ein zusam­men­hän­gen­des Stück Soft­ware (vor allem ein gro­ßes) ent­wi­ckelt, ist gut bera­ten, das nach Mög­lich­keit zur Ent­wurfs­zeit so sicher wie mög­lich zu machen, zumal wenn kein super­sta­bi­les Aktor­sys­tem sei­ne Lauf­zeit­feh­ler kom­pen­siert. Und der bes­te Weg das zu tun, ist ein was­ser­dich­tes, aus­drucks­star­kes Typ­sys­tem mit einem Com­pi­ler, der mit mög­lichst hilf­rei­chen Feh­ler­mel­dun­gen auf Ent­wurfs­feh­ler auf­merk­sam machen. Ein sol­ches möch­te ich Ihnen heu­te vor­stel­len. Ich mache das am Bei­spiel von Elm. (Ja, ich weiß, wie­der eine „Mode“-Sprache. Habe ich vor kur­zem auch noch gedacht, bis ich sie erlern­te. Ich wer­de gele­gent­lich in einem sepa­ra­ten Bei­trag erläu­tern, war­um ich Elm für das der­zeit bes­te Ent­wick­lungs­sys­tem für Brow­ser-Frontends hal­te. Hier und heu­te geht es nur um sei­ne sehr schlan­ke, aber unglaub­lich siche­re Syn­tax – und guter Code ist bekannt­lich auf allen Tiers wichtig.)

Wenn es manchmal kein String ist, sollte es kein String sein

Bekann­te Situa­ti­on: Sie haben eine Daten­struk­tur, in der ein Ele­ment eines bestimm­ten Typs vor­kommt, aber es gibt Situa­tio­nen, wo die­ses Ele­ment kei­nen Wert hat, z.B. eine optio­na­le Tele­fon­num­mer oder abwei­chen­de Lie­fer­adres­se. Genau die­ses Pro­blem lag der „Erfin­dung“ der Null-Refe­renz zugrun­de. Das Pro­blem ist, dass man die Mög­lich­keit einer Null-Refe­renz an jeder Ver­wen­dungs­stel­le beden­ken muss. Und wenn der Typ Adres­se ist, dann wird es frü­her oder spä­ter (eher frü­her) vor­kom­men, dass ein Pro­gram­mie­rer die­se Varia­ble als Adres­se ver­wen­det, ohne in der Doku­men­ta­ti­on (räus­per!) zu lesen, dass sie auch null sein kann. Und die ver­läss­lichs­te Doku­men­ta­ti­on ist eh der Code. Aber dazu muss er les­bar und aus­drucks­stark sein.

Was kann man gegen NPEs tun? Die Ant­wort steht eigent­lich schon im letz­ten Absatz: Wenn es sich um einen optio­na­len Wert han­delt, dann soll­te der Typ das auch aus­drü­cken. In eini­gen Spra­chen gibt es dafür den Opti­on-Typ, in Elm heißt er (viel­leicht noch etwas deut­li­cher) Maybe:

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

Davon abge­se­hen, dass Sie die­se Syn­tax noch nicht ken­nen (das obe­re Kon­strukt wer­den wir wei­ter unten noch ken­nen ler­nen), soll­ten Sie sie intui­tiv ver­ste­hen: Der Wert deliveryAdresss ist nur viel­leicht eine Adres­se, viel­leicht aber auch nichts. Optio­nal halt.

Der Vor­teil ist nicht nur, dass hier auf den ers­ten Blick ein optio­na­ler Wert als sol­cher zu erken­nen ist, son­dern vor allem, dass ich durch den Com­pi­ler gezwun­gen wer­de, mich damit aus­ein­an­der­zu­set­zen: So wie Elm mir nicht erlaubt, eine Order mit null als customer anzu­le­gen (es gibt kei­nen Wert die­ses Namens, nur den Not­hing-Fall von May­be), ist es mir nicht mög­lich, direkt mit dem ver­meint­li­chen Wert von deliveryAdress zu arbei­ten. Ich muss mich mit den (hier: zwei) Fäl­len aus­ein­an­der­set­zen. Neh­men wir an, es gibt eine Funk­ti­on, die ent­schei­det, wie und wohin zu ver­sen­den ist. In die­sem Zusam­men­hang könn­te die Ermitt­lung der Ver­sand­adres­se dann so aussehen:

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

Ich hät­te das Pat­tern Matching hier auch anders kodie­ren könn­ten, aber ich woll­te durch die­se Schreib­wei­se mög­lichst die Nähe zu den bei­den oben gezeig­ten Fäl­len von Maybe a zei­gen: Just a oder Nothing. a ist eine soge­nann­te Typ­va­ria­ble, die hier durch den Typ Adres­se ersetzt wird. (Übri­gens, falls jemand skep­tisch ist wegen mög­li­cher NPEs bei order.customer.adress: Hier kann ja nichts pas­sie­ren, weil customer und adress in ihren Con­tai­ner­struk­tu­ren nicht als Maybe defi­niert sind – sonst hät­te mir der Com­pi­ler gar nicht erlaubt, das so hinzuschreiben).

Das Gute bei Elm: Der Com­pi­ler zwingt mich, alle Fäl­le abzu­de­cken. Dadurch wird ver­mie­den, dass jemand denkt, „Naja, ich weiß ja, dass immer eine Lie­fer­adres­se ange­ge­ben wird“. Denn genau die­se Annah­me hat ja immer zu NPEs geführt. Und wenn wirk­lich immer eine Lie­fer­adres­se vor­aus­ge­setzt wird, soll­te der Typ nicht Maybe sein …

Übri­gens lässt sich mit Maybe natür­lich auch das alte Pro­blem lösen, wie man mit dem Rück­ga­be­wert einer Look­up-Funk­ti­on o.ä. umge­hen soll. Z. B. ist es doch sinn­vol­ler, wenn eine indexOf-Funk­ti­on als Rück­ga­be­wert Maybe Int angibt statt int (mit dem Kom­men­tar „-1, wenn nicht gefunden“ …).

Manchmal geht’s gut – und manchmal nicht

Man­cher erin­nert sich noch an die „gute“ alte Sit­te von C‑APIs, Feh­ler von Inte­ger­funk­tio­nen mit nega­ti­ven Zah­len zu kodie­ren, weil ein pri­mi­ti­ver Daten­typ nicht null wer­den kann. Spä­ter hat man dafür oft Excep­ti­ons ver­wen­det, was eben­so Miss­brauch ist, denn Excep­ti­ons waren mal für Aus­nah­me­si­tua­tio­nen gedacht, nicht für den Kon­troll­fluss bei zu erwar­ten­den Feh­lern, z. B. durch Benutzereingaben.

Funk­tio­nen, die ihren Erfolg nicht garan­tie­ren kön­nen, soll­ten das im Typ ihres Rück­ga­be­wer­tes aus­drü­cken. Dazu gibt es in Elm den Typ Result (in Sca­la heißt er Try):

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

Die­ser Code soll­te selbst­er­klä­rend sein: Zunächst wie­der die Elm-Defi­ni­ti­on des Typs Result (dies­mal mit zwei Typ­pa­ra­me­tern, dem Typ der Feh­ler­be­schrei­bung und des gewünsch­ten Ergeb­nis­ses). Dann die Typ­de­fi­ni­ti­on einer fach­li­chen Funk­ti­on zum Lesen von Tele­fon­num­mern aus Zei­chen­ket­ten, die im Erfolgs­fall einen Wert vom Typ Telefonnummer lie­fern wird, aber eben auch fehl­schla­gen kann. Und schließ­lich als letz­tes die Ver­wen­dung mit dem schon bekann­ten Mus­ter der Fallunterscheidung.

Übri­gens ist es sinn­voll, dass auch der Typ für den Feh­ler­fall spe­zi­fi­ziert wer­den kann. Das ermög­licht hier auch spe­zi­fi­sche Typen mit z. B. Kom­pen­sa­ti­ons- bzw. Kor­rek­tur­mög­lich­kei­ten („Did you mean …?“) anzu­ge­ben und zu behan­deln. Ein gutes Typ­sys­tem erzwingt nicht Phan­ta­sie, was gemeint ist, son­dern erlaubt, es im Code aus­zu­drü­cken. So waren ja auch Excep­ti­on-Hier­ar­chien gedacht, aber dort war das Grund­kon­zept des Wer­fens falsch.

Ähn­li­che Typen gibt es z. B. auch für die Behand­lung des Erfolgs und mög­li­cher (wahr­schein­li­cher) Feh­ler bei z. B. asyn­chro­nen Auf­ru­fen o.ä. Deren Erör­te­rung wür­de uns hier jetzt aber nicht wei­ter­füh­ren. Schau­en wir uns lie­ber an, was wir als Essenz die­ser Typen für unse­ren Code her­aus zie­hen können.

Lass Typen sprechen – Algebraische Datentypen (ADTs)

Ver­fech­ter der objekt-ori­en­tier­ten Pro­gram­mie­rung ver­tei­di­gen Kon­zep­te wie Ver­er­bung mit dem Begriff des Poly­mor­phis­mus (Viel­ge­stal­tig­keit). Und in der Tat ist die sau­be­re (und eben auch poly­mor­phe) Abs­trak­ti­on von Typen der ursprüng­li­che Trei­ber der OOP gewe­sen, bevor die­se sich völ­lig im Sumpf der Wie­der­ver­wen­dung durch Imple­men­tie­rungs­ver­er­bung ver­lo­ren hat – von ande­ren Miss­stän­den wie ver­än­der­ba­ren Zustän­den etc. abge­se­hen. Funk­tio­na­le Pro­gram­mier­spra­chen wie Elm (auch Has­kell hat ein sehr star­kes Typ­sys­tem) för­dern die­ses Kon­zept (das in den impe­ra­ti­ven Spra­chen neben Unter­klas­sen in komi­schen Zwit­ter­kon­struk­ten wie enum auf­ge­gan­gen ist, was durch die Mischung aus Klas­sen und „pri­mi­ti­ven“ Typen nötig war) wie­der sau­ber zuta­ge. Der in der FP all­ge­mein gebräuch­li­che Begriff lau­tet alge­bra­ische Daten­ty­pen (ADT). In Elm hei­ßen sie Uni­on Types, weil es ja qua­si die Ver­ei­ni­gungs­men­ge spe­zi­fi­scher Typen zu einem gemein­sa­men abs­trak­ten Typ ist.

Inter­es­san­ter als die Benen­nung des Kon­zepts ist sei­ne Arbeits­wei­se. Sie haben die­se in den bis­he­ri­gen Code­bei­spie­len schon ken­nen­ge­lernt: Ein ADT / ein Uni­on Type wird in Elm wie eine Glei­chung über eine mit Oder ver­bun­de­ne Typen-Men­ge defi­niert: Ein Result kann ein Feh­ler oder Ok sein. Wich­ti­ges Detail ist, dass jeder spe­zi­fi­sche Typ eine eige­ne Daten­struk­tur haben kann. Auch das ver­steht man unter polymorph.

Belas­sen Sie es nicht bei den ADTs, die Ihre Pro­gram­mier­spra­che mit­lie­fert. Nut­zen Sie die Aus­drucks­stär­ke und die seman­ti­sche Sicher­heit die­ses wich­ti­gen Kon­zepts auch für eige­ne Typen. In gutem Pro­gramm­code kom­men nicht viel mehr, vor allem aber nicht weni­ger Abs­trak­tio­nen vor als in der abge­bil­de­ten Domä­ne.

Den­ken Sie z. B. an einen Cloud­ser­vice, der Ange­bo­te ohne Anmel­dung, für ange­mel­de­te Gra­tis-User und für Pre­mi­um-User bie­tet. Natür­lich kann man das mit einer User-Klas­se und vie­len if-then-else-Abfra­gen über­all im Pro­gramm­code machen. Bes­ser, les­ba­rer (und siche­rer) model­liert ist es aber mit ADTs:

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

Schö­ne spre­chen­de Typen, oder? Und sie erlau­ben schö­nen spre­chen­den Code. Sehen Sie sich z. B. fol­gen­de Funk­ti­on an, die das UI für eine Pre­mi­um-Opti­on und einen User steu­ert. Sehr schön wird hier deut­lich, auf­grund wel­cher Regeln auf wel­che Ober­flä­chen „dis­patched“ wird. Wie die­se vom Auf­ru­fer ver­wen­det wer­den (als Komponente/Widget irgend­wo ein­ge­bun­den oder als sepa­ra­te Sei­te dar­ge­stellt), spielt für die­se Logik kei­ne Rol­le. So geht Wie­der­ver­wen­dung in FP. Hier übri­gens ermög­licht durch ein Kon­zept, das beson­ders Elm sehr starkt nutzt und das sehr stark mit dem Den­ken in Typen ver­knüpft ist: Func­tions as Data. Der Rück­ga­be­typ Html Msg ist näm­lich noch nicht das Html, son­dern nur des­sen struk­tu­rel­le Beschrei­bung, die erst viel wei­ter „außen“ und ggf. erst nach funk­tio­na­lem Ein­bin­den / Umfor­men / Fil­tern zu Brow­ser-Con­tent wird. Das macht sol­che Funk­tio­nen übri­gens aus­ge­zeich­net test­bar. Zur Erin­ne­rung: das Gan­ze ist rein funk­tio­na­ler Code ohne loka­le Varia­blen o.ä. und eine der vie­len Mög­lich­kei­ten, Funk­tio­nen sicher und belie­big kom­po­nie­ren zu kön­nen – solan­ge die Typen stimmen.

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

Sogar ein Fach­mann (z.B. aus dem Mar­ke­ting) soll­te die­se Code­stre­cke ver­ste­hen und veri­fi­zie­ren kön­nen (sicher ist sie ohne­hin durch FP-Prin­zi­pi­en). Beson­ders Funk­ti­ons­na­men soll­ten klar die Inten­ti­on aus­drü­cken. Guter Code soll­te sich (fast) wie eine Spe­zi­fi­ka­ti­on lesen!

Mit Abstraktionen wachsen und gedeihen Programme!

Mit sau­be­ren Abs­trak­tio­nen und ADTs kön­nen Sie sau­be­ren, siche­ren und aus­drucks­star­ken Code schrei­ben, wie oben gezeigt. Einen wei­te­ren Vor­teil bie­tet Ihnen ein guter Com­pi­ler wie Elm: Stel­len Sie sich vor, Sie fügen spä­ter einen wei­te­ren User-Typen oder – wahr­schein­li­cher – eine wei­te­re Pre­mi­um-Opti­on hin­zu. Bei impe­ra­ti­ver Pro­gram­mie­rung war es immer ein nicht zu ver­nach­läs­si­gen­des Pro­blem, an alle Pro­gramm­stel­len zu den­ken, wo sie eine wei­te­re Fall­un­ter­schei­dung ein­bau­en müs­sen (ich habe mal ein Uralt-Lega­cy-Sys­tem betreut, wo spe­zi­fi­sche Strukturen/Regeln des zen­tra­len Daten­typs Arti­kel­num­mer an ca. 137.000 Code­stel­len in über 1000 COBOL-Pro­gram­men ver­teilt waren, die manu­ell hät­ten gefun­den und ange­passt wer­den müs­sen). Der Elm-Com­pi­ler macht sie sofort und mit sehr gut ver­ständ­li­chen Feh­ler­mel­dun­gen dar­auf auf­merk­sam, wenn in einem Case-Matching ein Fall nicht abge­deckt wird – auch wenn man nur eine Hand­voll Code­stel­len hat, ist das ein gutes Sicherheitsnetz.

Fazit

Gra­dy Booch hat ein­mal sehr schön pos­tu­liert: „The pri­ma­ry task of soft­ware engi­nee­ring is to crea­te the illu­si­on of sim­pli­ci­ty“. Auch wenn er damit die Anmu­tung von Ein­fach­heit gegen­über dem Anwen­der mein­te, so soll­ten wir auch gegen­über Kol­le­gen (und uns selbst) Soft­ware nicht kom­pli­zier­ter machen als unbe­dingt nötig.

Einen Schlüs­sel zu beherrsch­ba­rem Code habe ich Ihnen heu­te nahe­ge­legt: Gute Abs­trak­tio­nen zu model­lie­ren, wobei ein star­kes Typ­sys­tem mit ADTs nicht unab­ding­bar, aber sehr hilf­reich ist (vor allem wegen der Com­pi­ler­un­ter­stüt­zung). Zusam­men mit den Vor­zü­gen der Funk­tio­na­len Pro­gram­mie­rung, die ich in ande­ren Bei­trä­gen erläu­te­re, lässt sich damit ein­fa­cher, siche­rer und pro­duk­ti­ver Code schrei­ben, wie er dem 21. Jahr­hun­dert ange­mes­sen ist, denn: Wenn unse­re Domä­nen kom­ple­xer wer­den, müs­sen wir das mit Ein­fach­heit för­dern­den Imple­men­tie­rungs­tak­ti­ken kom­pen­sie­ren. Genau das war die Inten­ti­on bei Ein­füh­rung des Begriffs „Höhe­re Programmiersprachen“.

Kommentare und Feedback gerne via Social Media:

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