Zum Inhalt springen

„Funktionale Programmierung“ – Versionsunterschied

aus Wikipedia, der freien Enzyklopädie
[gesichtete Version][gesichtete Version]
Inhalt gelöscht Inhalt hinzugefügt
K Typo
Keine Bearbeitungszusammenfassung
Zeile 28: Zeile 28:


Viele eigentlich objektorienterte Sprachen wurden in jüngsten Versionen aufgerüstet wie C++(ab Version 11) oder Java(ab Version 8). Allerdings leidet darunter die Syntax, sodass die Kompaktheit von LISP oder Haskell nicht erreicht wird.
Viele eigentlich objektorienterte Sprachen wurden in jüngsten Versionen aufgerüstet wie C++(ab Version 11) oder Java(ab Version 8). Allerdings leidet darunter die Syntax, sodass die Kompaktheit von LISP oder Haskell nicht erreicht wird.

== Einfache Konzepte der funktionalen Programmierung ==
In der funktionalen Programmierung sind bestimmten Arbeitsmuster gängig. Die Wichtigsten werden im folgenden am Beispiel des leicht verständlichen Lisp-Dialekts [[Scheme]] erläutert.
=== Funktions-Definition ===
Die Definiton einer Funktion erfolgt genau wie bei imperativer/struktirierter Programmierung. Zur Definition gehören ein Funktionsname, eine Parameterliste und ein Funktionskörper. Die Funktion '''square'' berechnet das Quadrat einer Zahl.

(define (square k)
(* k k))

(In Lisp/Scheme steht der Operator immer direkt ''hinter'' der sich öffnenden Klammer. Danach folgen die Operanden. Die Form '''(define (foo bar) baz)''' hat also den Operator '''define''' und die beiden Operanden '''(foo bar)''' und '''baz'''. Bei der Form '''(* k k)''' ist er Operator '''*''' und der Operanden sind '''k''' '''k'''. Den Funktionsaufruf <math>f(x)</math> würde man in Scheme/Lisp <tt>(f x)</tt> notieren.)

Ein weiteres Beipiel ist die Funktion '''inc''', die eine Zahl inkrementiert.

(define (inc n)
(+ 1 n))

=== Funktions-Applikation ===

Der Aufruf

(square 6)

ergibt

36

Der Aufruf

(inc 100)

ergibt

101

=== Rekursion ===
In der funktionalen Programmierung existieren keine Schleifen, da Schleifen zeitliche Abfolgen von Zuständen sind. Darum verwendet man Rekursion. Schleifen sind nur ein Spezialfall der Rekursion und deshalb durch diese voll ersetzbar. Die Berechnung der Fakultät <math>! : \mathbb{N}\to\mathbb{N}</math> mit <math> ! : n\mapsto 1\cdot 2 \cdot 3 \cdot .. \cdot n</math> kann so erfolgen:

(define (! n)
(if (< n 2)
1
(* n (! (- n 1)))))

Dann ergibt

(! 20)

den Wert

2432902008176640000

=== Abstraktion ===

Dies ist das erste im engeren Sinne funktionale Konzept. Aufgabe der Funktion '''sort2''' ist es, die beiden Parameter '''a''' und '''b''' in der richtigen Reihenfolge als Liste zurück zu geben. Dabei wird eine Ordungsrelation '''order''' übergeben und ein Funktion '''key''', die aus einem der Datenelemente '''a''','''b''' das Merkmal berechnet, dass dieser Ordung gehorchen soll.

Der Vorteil dieser Herangehensweise ist es, dass die Funktionen '''key''' und '''order''' bei der Definition von '''sort2''' offen bleiben können und erst bei der Applikation mitgegeben werden müssen.

(define (sort2 a b key order)
(if (order (key a) (key b))
(list a b)
(list b a)))

Um nun die beiden Listen '''(2 3)''' und '''(7 2)''' nach dem ersten Element aufsteigend zu ordnen, ist dann folgender Aufruf geeignet:

(sort2 '(2 3) '(7 2) first <)

Es erfolgt die Ausgabe:

'((2 3) (7 2))

Aufsteigende Anordnung nach dem zweiten Element ist dann so berechenbar:

(sort2 '(2 3) '(7 2) second <)

Die Rückgabe ist dann:

'((7 2) (2 3))

=== Synthese ===
Mit Synthese von Funtionen ist gemeint, Funktionen zur Laufzeit zusammenzusetzen und dabei gegebenenfalls vorhandene Funktionen zu verwenden.

==== Komposition ====

Aus den beiden Funktionen '''square''' und '''inc'' kann eine Funktion zusammengesetz werden, die incremente quadriert:

(define inc-sq
(compose square inc))

Die neu geschaffene Funktion ist kann dann so gerufen werden:

(inc-sq 5)

Das Resultat ist

36

Natürlich kann auf die explizite Definition von inc-sq verzichtet werden. Dazu wird die Funktionsapplikation '''(compose square inc)''' in der die Operatortstellung am Anfang der Liste gesetzt:

((compose square inc) 10)

Ausgabe:

121

==== Eigene Kompositionsoperatoren definieren ====

Die folgende Funktion '''twice''' errechnet eine Funktion, die eine Funktion '''f''' zweimal auf einen Operanden anwendet:

(define (twice f)
(compose f f))

Die Berechnung der vierten Potenz kann dann so defniert werden:

(define to-the-forth (twice square))

Der Aufruf

(to-the-forth 3)

ergibt dann

81

=== anonyme Funktionen ===

Häufig wird eine Funktion nur benötigt, um sie direkt zu verwenden. Dann kann sie ohne Namen mit dem Symbol ''''lambda''' vereinbart werden. Hier werden alle Zahlen, die nicht größer als 10 sind, aus einer Liste entfernt:

(filter (lambda (x) (> x 10)) '(4 3232 333 2 1 1))

Das Ergebnis ist:

'(3232 333)

Neben dem '''filter'''-idiom oben gibt es weitere oft verwendete Verfahren, wie das ''Mapping'', das aus einer Liste <math>[x_1,x_2,..,x_n]</math> und einer Funktion <math>f</math> die Liste <math>[f(x_1),f(x_2),..,f(x_n)]</math> bestimmt.

=== Mapping ===

(map square '(1 2 3))

'(1 4 9)

=== Faltungen ===

Die (Rechts-) Faltung einer Liste <math>[x_1,x_2,x_3,..,x_n]</math> durch eine zweistellige Funktion <math>f(a,b)</math> und einen initialwert ist gegeben durch <math>f(x_1,f(x_2,f(x_3,..,f(x_n,i)..)</math>. Viele Schleifen aus der imperativen Programmierung realisieren de facto Faltungen. Daher kommen Faltungen verscheidener Art sehr oft in der Funktionalen Programmierung vor.

(define (reduce init fn list)
(if (null? list) init
(fn (car list)
(reduce init fn (cdr list)))))

Die Summe einer Zahlenliste ist dann so berechenbar:

(reduce 0 + '(3 4 5 6))

18


== Funktionen in Mathematik und Programmierung ==
== Funktionen in Mathematik und Programmierung ==

Version vom 12. Januar 2017, 11:01 Uhr

Funktionale Programmierung ist ein Programmierparadigma, innerhalb dessen Programme aus Funktionen und deren Anwendung zusammengesetzt werden. Dabei ist insbesondere der gleichzeitige Gebrauch dieser Funktionen als reguläre Datenobjekte von Bedeutung. Programmiersprachen, die funktionale Programmierung ermöglichen, heißen funktional.

Die funktionale Programmierung entspringt der mathematischen Grundlagenforschung. In den 1930er Jahren entwickelte Alonzo Church den Lambda-Kalkül als Instrument, um das Entscheidungsproblem zu bearbeiten und dazu den Begriff der berechenbaren Funktion zu definieren. Der Lambda-Kalkül selbst beschäftigt sich nicht mit bestimmten Funktionen, sondern ist nur ein Regelwerk dafür, wie die Anwendung von Funktionen auf ihre Argumente erfolgt und wie dabei mit freien und gebundenen Variablen verfahren wird.

Die besonderen Eigenschaften der funktionalen Programmierung ermöglichen es, auf die in der imperativen Programmierung benötigten inneren Zustände eine Berechnungsprozesses ebenso zu verzichten wie die zugehörigen Zustandsänderungen die auch Seiteneffekte genannt werden. Der Verzicht darauf vereinfacht auf der einen Seite die semantische Analyse eines Computerprogramms erheblich und eröffnet auf der anderen Seite weitreichende Möglichkeiten zur regelbasierten, algebraischen Programmtransformation und -synthese. Daraus ergibt sich die erhebliche praktische Bedeutung der funktionalen Programmierung für die Informatik.

Eine weitere Konsequenz ist, dass es in funktionaler Programmierung besonders einfach ist, Algorithmen ohne Berücksichtigung der Beschaffenheit der bearbeiteten Datanobjekte zu beschreiben und dadurch generischen Programmcode zu erstellen. Viele funktionale Verfahren sind so generisch, dass sie seit den 1950er Jahren keiner Anpassung unterworfen werden mussten.

Die erste bedeutende, in funktionaler Programmierung erstellte Software ist der von John McCarthy im Jahr 1958 erstellte, metazirkuläre LISP-Interpreter mit Namen eval, der die Semantik von LISP aus fünf einfachen Funktionen zusammengesetzt darstellt. Er findet auf einer DIN-A4-Seite Platz und ist bis heute konzeptionelle Grundlage für die Implementierung der meisten Programmiersprachen.

Definition

Die funktionale Programmierung ist durch folgende Eigenschaften gekennzeichnet:

  1. Computerprogramme werden als Funktionen verstanden, die für eine Eingabe eine Ausgabe liefern, die nur von dieser abhängig ist.
  2. Funktionen werden nicht als Abfolge von Anweisungen dargestellt, sondern als ineinander verschachtelte Funktionsaufrufe.
  3. Funktionen sind gegenüber allen anderen Datenobjekten gleichberechtigt. Das bedeutet, dass sie als Parameter in Funktionen eingehen dürfen und ebenso als Berechnungsergebnisse aus Funktionen hervorgehen können. Insbesondere können Funktionen wie andere Datenobjekte zur Laufzeit erstellt werden oder entfallen.
  4. Eine Funktion kann auf Variablen Bezug nehmen, die dem Kontext angehören, in dem ihre Erstellung erfolgt ist. Dies kann sie auch dann noch tun, wenn den Kontext verlassen hat. Die Belegung der Variablen zum Zeitpunkt des Verlassens dieses Kontextes wird dann innerhalb dieser Funktion eingefroren. Eine so entstandene Funktion heißt Closure und die eingefrorenen Variablenbelegungen heißen Closure-Variablen.
  5. Funktionsdefinitionen können ohne explizite Namensgebung literal in der Stellung eines Funktionssymbols verwendet werden. Solche Funktionen heißen anonym und werden oft salopp "Lambdas" genannt.

Funktionale Programmiersprachen

Eine funktionale Programmiersprache ist eine Programmiersprache, die die oben genannten Eigenschaften implementiert. Wichtige Vertreter funktionaler Programmiersprachen sind LISP und Haskell. Fast alle in jüngster Zeit entstandenen Programmiersprachen gestatten funktionale Programmierung.

Zur funktionalen Programmierung gedachte Sprachen sind neben anderen LISP, ML, Haskell, OCaml, F#, Erlang, Clojure und Scala. Sprachen, die funktionale Programmierung neben anderen Paradigmen ermöglichen, sind zum Beispiel Perl, ECMAScript, Dylan, Ruby und Visual Basic.NET. Python hat Einschränkungen bei der Formulierung von anonymen Funktionen.

Populäre Sprachen, die keinerlei Möglichkeiten zur funktionalen Programmierung bieten, sind zum Beispiel C und Delphi.

Unter einer rein funktionalen Programmiersprache wäre eine Programmiersprache zu verstehen, die isomorph zum Lambda-Kalkül ist. Das gilt mit minimalen Einschränkungen beispielsweise für die esoterische Programmiersprache unlambda.

Viele eigentlich objektorienterte Sprachen wurden in jüngsten Versionen aufgerüstet wie C++(ab Version 11) oder Java(ab Version 8). Allerdings leidet darunter die Syntax, sodass die Kompaktheit von LISP oder Haskell nicht erreicht wird.

Einfache Konzepte der funktionalen Programmierung

In der funktionalen Programmierung sind bestimmten Arbeitsmuster gängig. Die Wichtigsten werden im folgenden am Beispiel des leicht verständlichen Lisp-Dialekts Scheme erläutert.

Funktions-Definition

Die Definiton einer Funktion erfolgt genau wie bei imperativer/struktirierter Programmierung. Zur Definition gehören ein Funktionsname, eine Parameterliste und ein Funktionskörper. Die Funktion 'square berechnet das Quadrat einer Zahl.

(define (square k) 
  (* k k))

(In Lisp/Scheme steht der Operator immer direkt hinter der sich öffnenden Klammer. Danach folgen die Operanden. Die Form (define (foo bar) baz) hat also den Operator define und die beiden Operanden (foo bar) und baz. Bei der Form (* k k) ist er Operator * und der Operanden sind k k. Den Funktionsaufruf würde man in Scheme/Lisp (f x) notieren.)

Ein weiteres Beipiel ist die Funktion inc, die eine Zahl inkrementiert.

(define (inc n)
  (+ 1 n))

Funktions-Applikation

Der Aufruf

(square 6)

ergibt

36

Der Aufruf

(inc 100)

ergibt

101

Rekursion

In der funktionalen Programmierung existieren keine Schleifen, da Schleifen zeitliche Abfolgen von Zuständen sind. Darum verwendet man Rekursion. Schleifen sind nur ein Spezialfall der Rekursion und deshalb durch diese voll ersetzbar. Die Berechnung der Fakultät mit kann so erfolgen:

(define (! n)
  (if (< n 2)
      1
      (* n (! (- n 1)))))

Dann ergibt

 (! 20)

den Wert

2432902008176640000

Abstraktion

Dies ist das erste im engeren Sinne funktionale Konzept. Aufgabe der Funktion sort2 ist es, die beiden Parameter a und b in der richtigen Reihenfolge als Liste zurück zu geben. Dabei wird eine Ordungsrelation order übergeben und ein Funktion key, die aus einem der Datenelemente a,b das Merkmal berechnet, dass dieser Ordung gehorchen soll.

Der Vorteil dieser Herangehensweise ist es, dass die Funktionen key und order bei der Definition von sort2 offen bleiben können und erst bei der Applikation mitgegeben werden müssen.

(define (sort2 a b key order)
   (if (order (key a) (key b))
       (list a b)
       (list b a)))

Um nun die beiden Listen (2 3) und (7 2) nach dem ersten Element aufsteigend zu ordnen, ist dann folgender Aufruf geeignet:

(sort2 '(2 3) '(7 2) first <)

Es erfolgt die Ausgabe:

'((2 3) (7 2))

Aufsteigende Anordnung nach dem zweiten Element ist dann so berechenbar:

(sort2 '(2 3) '(7 2) second <) 

Die Rückgabe ist dann:

'((7 2) (2 3))

Synthese

Mit Synthese von Funtionen ist gemeint, Funktionen zur Laufzeit zusammenzusetzen und dabei gegebenenfalls vorhandene Funktionen zu verwenden.

Komposition

Aus den beiden Funktionen square' und inc kann eine Funktion zusammengesetz werden, die incremente quadriert:

(define inc-sq
   (compose square inc))

Die neu geschaffene Funktion ist kann dann so gerufen werden:

(inc-sq 5)

Das Resultat ist

36

Natürlich kann auf die explizite Definition von inc-sq verzichtet werden. Dazu wird die Funktionsapplikation (compose square inc) in der die Operatortstellung am Anfang der Liste gesetzt:

((compose square inc) 10) 

Ausgabe:

121

Eigene Kompositionsoperatoren definieren

Die folgende Funktion twice errechnet eine Funktion, die eine Funktion f zweimal auf einen Operanden anwendet:

(define (twice f)
  (compose f f))

Die Berechnung der vierten Potenz kann dann so defniert werden:

(define to-the-forth (twice square))

Der Aufruf

(to-the-forth 3)

ergibt dann

81

anonyme Funktionen

Häufig wird eine Funktion nur benötigt, um sie direkt zu verwenden. Dann kann sie ohne Namen mit dem Symbol 'lambda vereinbart werden. Hier werden alle Zahlen, die nicht größer als 10 sind, aus einer Liste entfernt:

(filter (lambda (x) (> x 10)) '(4 3232  333  2 1 1))

Das Ergebnis ist:

'(3232 333)

Neben dem filter-idiom oben gibt es weitere oft verwendete Verfahren, wie das Mapping, das aus einer Liste und einer Funktion die Liste bestimmt.

Mapping

(map square '(1 2 3))
'(1 4 9)

Faltungen

Die (Rechts-) Faltung einer Liste durch eine zweistellige Funktion und einen initialwert ist gegeben durch . Viele Schleifen aus der imperativen Programmierung realisieren de facto Faltungen. Daher kommen Faltungen verscheidener Art sehr oft in der Funktionalen Programmierung vor.

(define (reduce init fn list)
  (if (null? list) init
      (fn (car list)
          (reduce init fn (cdr list)))))

Die Summe einer Zahlenliste ist dann so berechenbar:

(reduce 0 + '(3 4 5 6))
18

Funktionen in Mathematik und Programmierung

Der Begriff der Funktion wird in der Programmierung und der Mathematik mit unterschiedlicher Bedeutung verwendet (vergleiche Funktion (Programmierung) und Funktion (Mathematik)). In der Mathematik wird eine Funktion als eine Abbildung der Funktionsargumente auf das jeweilige Funktionsergebnis betrachtet, so dass eine Funktion im mathematischen Sinne bei jeder Verwendung mit denselben Funktionsargumenten dasselbe Ergebnis liefert. Demgegenüber wird in der Informatik Funktion häufig als gleichbedeutend mit Unterprogramm verwendet. Anders als in der Mathematik brauchen in der Informatik Funktionen im allgemeinen Sprachgebrauch weder werttreu noch frei von einer Wirkung (auch als Nebenwirkung oder Nebeneffekt bezeichnet) zu sein: Erstens kann das Ergebnis eines Aufrufs neben den beim Aufruf übergebenen Funktionsparametern auch von der Vorgeschichte der Funktionsaufrufe oder allgemeiner vom augenblicklichen Zustand des umgebenden Computerprogramms abhängen, und zweitens kann der Funktionsaufruf selbst Auswirkungen auf das zukünftige Verhalten anderer Funktionen haben.

Die Funktionen funktionaler Programmiersprachen verhalten sich hingegen wie mathematische Funktionen. Dies wird unter anderem erreicht, indem eine Zuweisung der Form

x = x + 1

ausgeschlossen wird.

Mathematisch gelesen ergibt diese Zeile auch wenig Sinn, denn das wäre eine Gleichung und es käme

heraus, was unwahr ist. Gemeint ist diese Zuweisung als

,

wobei das auf der linken Seite ein anderes ist, als das auf der rechten Seite. Das ist zwar für die Programmierung nützlich, entspricht aber nicht dem Verhalten von Variablen in der Mathematik, wo eine Variable , wenn sie einen Wert zugewiesen bekommen hat, genau diesen Wert immer behält und nicht mal den einen und mal den anderen Wert im Verlauf einer Rechnung annimmt (referenzielle Transparenz).

Ein Ansatz der funktionalen Programmierung ist, dass ihre Funktionen sich mehr wie mathematische Funktionen verhalten, damit die Rechen- und Beweismethoden der Mathematik besser auf Programme angewendet werden können (um vor allem ihre Korrektheit zu beweisen).

Sie vertritt weiter die Auffassung, dass Funktionen ein geeignetes Mittel zur Modularisierung von Programmen darstellen, indem die Hauptfunktionen aus einer Vielzahl anderer Funktionen zusammengesetzt (komponiert) werden. Hierfür bieten funktionale Programmiersprachen verstärkte Unterstützung, z. B. verhalten sich Funktionen weitestgehend wie andere Datentypen, können als Argumente und Rückgabewerte verwendet werden (Funktionen höherer Ordnung).

Der Programmierer legt fest, wie die Funktionen zusammengesetzt sind, es gibt jedoch noch Freiheiten in der Auswertung, die von Übersetzern zu Optimierungszwecken genutzt werden können.

Abgrenzung von imperativer Programmierung

In der imperativen Programmierung sieht man ein Programm als Folge von Anweisungen (engl. statements), die etwas tun, und dabei meistens Variablen verändern. In der funktionalen Programmierung besteht ein Programm aus Ausdrücken, die ausgewertet werden (eval von Ausdrücken, apply von Funktionen). Beispiel: Eine imperative if-then-else-Konstruktion führt je nach Gültigkeit der Bedingung verschiedene Aktionen aus. Ein funktionaler if-then-else-Ausdruck ist eine Funktion, die je nach Gültigkeit der Bedingung einen if- oder einen else-Wert zurückgibt.

Im Gegensatz zu imperativen Programmen, die aus Rechenanweisungen bestehen, sind funktionale Programme eine Menge von (Funktions)-Definitionen, die man mathematisch als partielle Abbildungen von Eingabedaten auf Ausgabedaten auffassen kann.

Ein typisches Beispiel ist die Berechnung der Fakultät n! einer Zahl n (n! = 1 · 2 · 3 · ... · n), hier eine imperative Lösung:

Eingabe
Zahl n
Ausgabe
Zahl b (=1 · 2 · 3 · ... · n)
Algorithmus
b := 1 (Zuweisung)
solange n > 0 führe aus:
b := n · b
n := n − 1
Ausgabe: b

Charakteristisch für die imperative Programmierung sind hier die Zuweisungen (Änderung von Werten, durch das Symbol := im Pseudocode repräsentiert). Zwar berechnet der Algorithmus die Fakultät der Zahl n, aber die Korrektheit dieses Rechenweges ist nicht offensichtlich.

Nun kann man die Fakultät jedoch auch mithilfe von rekursiven Funktionen definieren, was zur funktionalen Lösung führt.

Funktion f(n)
wenn n > 0 dann:
Ausgabe: n·f(n-1)
sonst wenn n = 0 dann:
Ausgabe: 1

Die funktionale Programmierung kommt also ohne Schleifen und Zuweisungen aus, benötigt dafür aber Rekursion.

Mächtigkeit funktionaler Sprachen

Aus dem anderen Programmierparadigma ergibt sich im Vergleich zu den verbreiteteren imperativen Sprachen eine andere Herangehensweise bei der Konzeption von Programmen. Die wesentlichen Unterschiede manifestieren sich im Software-Engineering und in der Performance. Im Allgemeinen ergeben sich Vorteile bei der Konzeption und Wartbarkeit von Programmen, während sich die hohe Abstraktion von der maschinellen Implementierung in performance-kritischen Anwendungen als Nachteil erweist.

Lambda-Kalkül

Als historische theoretische Grundlage dient der λ-Kalkül (Lambda-Kalkül) von Alonzo Church. Jeder Ausdruck und jeder Wert wird dabei als auswertbare Funktion betrachtet, so dass die ganze Programmierung auf der Übergabe von Funktionen als Parameter an Funktionen fußt.

Der Lambda-Kalkül erlaubt es, vollständige Teilausdrücke separat auszuwerten. Dies ist der wichtigste Vorteil gegenüber der imperativen Programmierung. Dieses Konzept vereinfacht die Programmverifikation und Programmoptimierung, beispielsweise die Überführung der Programme in eine parallel auswertbare Form.

Typsystem

Historisch ist Lisp als die erste funktionale Programmiersprache aufzufassen; Sprachen der LISP-Familie (wie auch Scheme) sind dynamisch typisiert. Seit der Entwicklung von Standard-ML (SML) konzentriert sich die Forschung auf dem Gebiet der funktionalen Programmiersprachen auch auf statisch typisierte Sprachen, insbesondere auf solche, die das Typsystem nach Hindley und Milner verwenden. Der Vorteil dieses Typsystems ist die Verfügbarkeit von parametrischem Polymorphismus zusammen mit Typinferenz: Programmierer müssen die Typen ihrer Funktionen und anderen Werte nicht angeben, sondern bekommen sie gratis vom Übersetzer ausgerechnet, der zugleich noch während der Übersetzung Typfehler monieren kann. Dies wird allgemein als wesentlicher Vorteil gegenüber dynamisch typisierten Sprachen (Lisp, Python) aufgefasst, die zwar ebenfalls keine Typannotationen benötigen (im Gegensatz z. B. zu Java oder C), dafür aber Typfehler erst zur Laufzeit anmahnen können. Letzteres hat jedoch wiederum den Vorteil einer breiteren Anwendbarkeit bereits definierter Funktionen auf ggf. zum Entwicklungszeitpunkt noch nicht vorhergesehene neue Einsatzgebiete.

Das Hindley-Milner-System erlaubt allerdings nur Polymorphismus ersten Ranges; Erweiterungen für Polymorphismus zweiten und allgemein k-ten Ranges sind inzwischen in dem Haskell-Übersetzer GHC als Erweiterungen verfügbar, bedingen jedoch wieder explizite Annotationen (Typinferenz ab Polymorphismus zweiten Ranges ist unentscheidbar).

Rein funktionale Programmiersprachen

Rein (engl. pure) funktionale Programmiersprachen fassen Programme als mathematische Funktion auf: Ein Ausdruck hat dort während der Programmausführung immer den gleichen Wert. Es gibt keine Zustandsvariablen, die während einer Berechnung geändert werden. Um erwünschte Wirkungen (Benutzerinteraktion, Eingabe/Ausgabe-Operationen) beschreiben zu können, sind meist besondere Vorkehrungen notwendig. Die meisten funktionalen Programmiersprachen (Standard ML, Caml, Scheme und weitere) erlauben allerdings solche Wirkungen und sind daher keine reinen funktionalen Programmiersprachen.

Um Programmierung auch mit Wirkungen zu erlauben, ohne dabei zu einer sog. unreinen (engl. impure) Sprache zu werden, wurde bei der Entwicklung der Sprache Haskell das aus der Kategorientheorie stammende Konzept der Monaden entwickelt (insbesondere von Eugenio Moggi und Philip Wadler), welches Wirkbehaftung durch parametrische Typen ausdrückt und somit das Typsystem dazu zwingt, zwischen Ausdrücken mit und Ausdrücken ohne Wirkungen zu unterscheiden. Auch in Clean und Mercury wird das Typsystem verwendet, um solche Wirkungen zu kennzeichnen. Dort benutzt man allerdings das Konzept der "Uniqueness"-Typen.

Funktionen höherer Ordnung

Man unterscheidet Funktionen erster Ordnung und Funktionen höherer Ordnung. Bei Funktionen höherer Ordnung sind Funktionen selbst Werte. Dies erlaubt es insbesondere, Funktionen als Ergebnisse oder Argumente anderer Funktionen zu verwenden. Ein typisches Beispiel ist der Ableitungsoperator: Eingabe ist eine differenzierbare Funktion, Ausgabe ist die Ableitung dieser Funktion. Ein weiteres Standardbeispiel ist die Funktion map, welche als Eingabe eine Funktion f und eine Liste l erhält und die modifizierte Liste zurückgibt, die dadurch entsteht, dass die Funktion f auf jedes Element der Liste l angewendet wird. Definition von map in Haskell:

 map f [] = []
 map f (x:xs) = f x : map f xs
In der ersten Zeile wird das Ergebnis für eine leere Liste [] zurückgegeben; die zweite Zeile wendet die Funktion f auf das erste Listenelement x an und führt dann einen Rekursionsschritt für die restliche Liste xs durch.

Bedarfsauswertung und strikte Auswertung

Funktionale Sprachen kann man auch nach ihrer Auswertungsstrategie unterscheiden: Bei strikter Auswertung (engl. eager bzw. strict evaluation) werden die Argumente von Funktionen zuerst ausgewertet. Dagegen werden bei der nicht-strikten Auswertung zunächst die Ausdrücke als Ganzes übergeben und dann ausgewertet.

Man kann zum Beispiel den Ausdruck auf zwei Arten berechnen:

Im ersten Fall wird der Ausdruck strikt ausgewertet, da erst die Argumente der Potenz-Funktion berechnet werden. Im zweiten Fall werden diese Argumente erst bei Bedarf ausgewertet, also nachdem die Potenzfunktion aufgelöst wurde (nicht-strikte Auswertung).

Eine Variante der nicht-strikten Auswertung ist die Bedarfsauswertung (engl. lazy evaluation), bei der Ausdrücke erst ausgewertet werden, wenn deren Wert in einer Berechnung benötigt wird. Dadurch lassen sich z. B. unendlich große Datenstrukturen (die Liste aller natürlicher Zahlen, die Liste aller Primzahlen, etc.) definieren, und bestimmte Algorithmen vereinfachen sich.

Manche Berechnungen lassen sich mit strikter Auswertung, andere mit Bedarfsauswertung effizienter ausführen. Terminiert die strikte Auswertung eines Ausdruckes, so terminiert auch die nicht-strikte Auswertung. Hintergrund hiervon ist die Konfluenz-Eigenschaft des jeder funktionalen Sprache zugrundeliegenden λ-Kalküls, die aussagt, dass das Ergebnis der Berechnung nicht von der Reihenfolge der Auswertung abhängt.

Funktionale Algorithmen und Datenstrukturen

Algorithmen geben vorteilhafte Verfahren für die Lösung wichtiger Probleme an (z. B. Sortieren) und sind in der Regel gut analysiert, so dass ein Entwickler auf bewährte, einschätzbare Lösungen zurückgreifen kann. Gleiches leisten Datenstrukturen für die Organisation von Daten. Sammlungen von guten Algorithmen und Datenstrukturen haben somit eine große praktische Bedeutung.

Der Verzicht auf Zuweisungen führt dazu, dass etliche klassische Algorithmen und Datenstrukturen, die regen Gebrauch von dieser Möglichkeit machen, so nicht für funktionale Sprachen verwendet werden können und nach neuen Lösungen gesucht werden muss.

Chris Okasaki schreibt:

„Auch wenn die meisten dieser Bücher [über Datenstrukturen] behaupten, dass sie unabhängig von einer bestimmten Programmiersprache sind, so sind sie leider nur sprachunabhängig im Sinne Henry Fords: Programmierer können jede Programmiersprache benutzen, solange sie imperativ ist.“

Gerade rein funktionale Datenstrukturen sind von ihrer Natur her anders als die gewohnten Datenstrukturen, die meist nur eine Version ihrer Daten verwalten (ephemere Datenstrukturen), wohingegen funktionale Datenstrukturen mehrere Versionen verwalten (persistente Datenstrukturen).

Beispiele

Folgende Programme definieren eine Funktion ringarea, die die Fläche berechnet, die zwischen den beiden konzentrischen Kreisen mit den Radien R und r bzw. r1 und r2 mit gemeinsamen Mittelpunkt liegt. Dazu werden die Konstante pi und die Hilfsfunktion sq definiert. Diese werden von ringarea dann für die Berechnung benutzt.

(defun ringarea (r1 r2)
  (flet ((sq (x)
           (* x x)))
    (* pi
       (- (sq r1)
          (sq r2)))))

F# und OCaml

 let ringarea (r1, r2) =
  let pi = 3.14 in
  let sq x = x*x in
  pi*(sq r1 - sq r2)
sq :: (Floating a) => a -> a -- optionale explizite Typangabe
sq x = x^2
ringArea :: (Floating a) => a -> a -> a -- optionale explizite Typangabe
ringArea r1 r2 = pi*(sq r1- sq r2)

Der Typ der Funktionen sq, ringArea ist polymorph und wird durch die Angabe der Typklasse Floating eingegrenzt. Die explizite Spezifikation des Typs ist optional und kann ebenso gut durch den Haskell-Compiler inferenziert werden. Pi ist in Haskell vordefiniert.

Hier eine komplexere Implementation, ausschließlich mit Hilfe von Funktionen höherer Ordnung:

ringArea' :: (Floating a) => a -> a -> a -- optionale explizite Typangabe
ringArea' r1 r2 = foldr (-) 0 $ map ((*pi) . (^2)) [r1, r2]

DEFINE
pi == 3.14;
sq == dup *;
ringarea == sq swap sq swap - pi *.

Joy benutzt die umgekehrte polnische Notation. Man beachte, dass hier alle Variablen Funktionen bezeichnen (auch pi ist eine Funktion).

pi=3.14;
sq(x::Number) = x * x;
ringarea(R::Number, r::Number) = pi*(sq(R) - sq(r));

Implementierung mit höheren Funktionen:

ringarea(R1::Number, r::Number) = pi*((R^2) - (r^2));

Oder als anonyme Funktion mit den beiden Werten R = 4 und r = 3:

((R, r) -> pi*((R^2) - (r^2)))(4,3);

Makefile

Jedes Makefile stellt ein funktionales Programm dar.

  def pi: Double = 3.14
  def sq (x: Double) = x * x

  def ringarea(R: Double, r: Double) = pi * (sq(R) -  sq(r))
(define (ringarea r1 r2)
 (define pi 3.14)
 (define (sq x) (* x x))
 (* pi (- (sq r1) (sq r2)))
)

Alternativ mit Funktionen höherer Ordnung:

(define (ringarea r1 r2)
 (* 3.14 (foldr - 0 (map (lambda (x) (* x x)) (list r1 r2)))))
local
  val pi = 3.14
  val sq = fn (x:real) => x*x
in
  fun ringarea(R, r) = pi*(sq R - sq r)
end

Der Typ von x muss hier explizit angegeben werden, da ein SML97-Übersetzer sonst den Typ int inferieren würde. Das zugrundeliegende Problem ist das der Überladung von Operatoren; dieses Problem wurde erst mit Einführung von Typklassen gelöst, allerdings in Haskell und verwandten Sprachen.

XSLT

XSLT dient dem Transformieren von XML (insbesondere in XHTML) und hat zusammen mit XML stark an Bedeutung gewonnen. Sie ist funktional, wie Dimitre Novatchev gezeigt hat.[1] Die im folgenden Beispiel[2] definierte Funktion kehrt die Reihenfolge der Wörter einer Zeichenkette um. Typisch für funktionale Programmiersprachen ist der rekursive Aufruf. Der zweite Absatz zeigt, wie die Funktion verwendet wird.

<xsl:function name="str:reverse" as="xs:string">
  <xsl:param name="sentence" as="xs:string"/>
  <xsl:choose>
    <xsl:when test="contains($sentence, ' ')">
      <xsl:sequence select="concat(str:reverse(
        substring-after($sentence, ' ')),
        ' ',
        substring-before($sentence, ' '))"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:sequence select="$sentence"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:function>

<xsl:template match="/">
<output>
  <xsl:value-of select="str:reverse('DOG BITES MAN')"/>
</output>
</xsl:template>
let pi = 3.14
let square = {(x: Double) -> Double in x * x }
let ringarea = {(r1: Double, r2: Double) -> Double in
    pi * (square(r1) - square(r2))
}

Siehe auch

Literatur

  • Chris Okasaki: Purely Functional Data Structures. Cambridge University Press 1999, ISBN 0-521-66350-4
  • Patrick M. Krusenotto Funktionale Programmierung und Metaprogrammierung – Interaktiv in Common Lisp Springer 2016, ISBN 978-3-658-13743-4
  • Lothar Piepmeyer: Grundkurs funktionale Programmierung mit Scala. Hanser, 2010, ISBN 978-3-446-42092-2, S. 297 (Seite zum Buch).
  • Fethi Rabhi und Guy Lapalme: Algorithms – A Functional Programming Approach. Addison Wesley 1999, ISBN 0-201-59604-0

Fußnoten

  1. The Functional Programming Language XSLT (Memento vom 5. Dezember 2008 im Internet Archive)
  2. Aus der W3C Recommendation XSL Transformations (XSLT) Version 2.0