Benutzerspezifische Werkzeuge
Sie sind hier: Startseite Lexikon Technologische und methodische Grundlagen Sprache Programmiersprache Programmierparadigma

Programmierparadigma

Programmierparadigmen bezeichnen unterschiedliche Prinzipien, mit denen die Ausführung von Programmen beschrieben wird. Die wichtigsten sind imperative, objektorientierte, funktionale und logische Programmierung. Die Eigenschaften jeder Programmiersprache und die Methoden Programme zu entwickeln, werden durch die zu Grunde liegenden Paradigmen entscheidend geprägt.

Imperative Programmierung

Imperative Programme sind verzweigte Folgen von Anweisungen. Sie enthalten Zuwei­sungen, die bei ihrer Ausführung Werte von Variablen und damit den Programmzustand verändern. Verzweigungen im Programm werden durch Bedingungen über Variable entschieden. Parametrisierte Funktionen werden verwendet, um  Berechnungen zu abstrahieren und an verschiedenen Stellen im Programm aufzurufen.

Als durchgängiges Beispiel betrachten wir eine Funktion, die die Elemente einer Liste aufsummiert. Sie ist in der Sprache C geschrieben:

int ListSum (IntList l) {

  int sum = 0;

  while (l != NULL)

   { sum = sum + l->val; l = l->next; }

  return sum;

}

Hier wird eine  Liste durch eine Referenz auf ein Paar von Werten repräsentiert: eine ganze Zahl val und eine Referenz auf die Restliste next. Die Zuweisungen im Rumpf der while-Schleife addieren die Zahl val aus dem Listenelement von l zur Variablen sum und setzen l auf das nächste Element. Diese Anweisungen werden iteriert solange l nicht auf die leere Liste zeigt. Dann wird Wert der Variablen sum als Ergebnis des Aufrufes der Funktion geliefert.

Das imperative Paradigma liegt auch der klassischen Formulierung von Algorithmen zu Grunde, in der verzweigte Abfolgen von Schritten angegeben werden, die Berechnungen und Zuweisungen an Variablen ausführen. Auch das abstrakte Strukturprinzip des Von-Neumann-Rechners entspricht dem imperativen Paradigma: Ein Prozessor führt eine verzweigte Folge von Instruktionen aus, die Daten im Speicher verändern.

Objektorientierte Programmierung

Grundprinzipien der objektorientierten Programmierung sind abstrakte Datentypen und Untertyp-Polymorphie. Sie prägen das Programmierparadigma, auch wenn die meisten objektorientierten Sprachen auf den Konstrukten der imperativen Sprachen aufbauen.

Ein abstrakter Datentyp definiert eine Menge von Objekten zusammen mit den darauf ausführbaren Operationen. Wir variieren unser Beispiel und definieren einen Datentyp für lineare Listen, der unter anderen eine Operation hat, die die ganzzahligen Werte der Listenelemente aufsummiert. In der Sprache Java wird dafür eine Klasse definiert:

     class IntListObject {

           IntList first = nil; int lg = 0;

           …

           int listSum () {

               IntList l=first; int sum = 0;

               while (l != nil) {sum = l.val; l = l.next}

               return sum;

           }

     }

     IntListObject ilo = new IntListObject ( );

Jedes Objekt der hier definierten Klasse hat zwei Zustandsvariablen: first zeigt auf das erste Element der Liste oder ist nil; lg gibt die Anzahl der Elemente an. Die Funktion listSum benutzt hier die Zustandsvariable first statt eines Parameters, wie in dem imperativen Beispiel. In der letzten Zeile wird ein neues Objekt zu der Klasse erzeugt und seine Referenz an die Variable ilo zugewiesen. Ein Aufruf der Funktion für dieses Objekt würde dann lauten ilo.listSum().

Mit dem zweiten objektorientierten Grundprinzip, der Untertyp-Polymorphie, kann man ausdrücken, dass verschiedenartige Objekte einem gemeinsamen Obertyp angehören. Operationen, die im Obertyp beschrieben sind, können für jedes Objekt eines Untertyps ausgeführt werden, auch wenn sie für unterschiedliche Untertypen verschieden implementiert sind. In unserem Beispiel könnte der Obertyp eine Schnittstelle angeben für verschiedene Implementierungen von Listen in den Untertypen; obige Klasse wäre eine davon. Bei der Ausführung des Aufrufes x.listSum() wird dann am Typ des Objektes in x entschieden, welche Funktion listSum  aus einem der Untertypen verwendet wird. Neben der Schnittstellenabstraktion gibt es eine Vielzahl weiterer sehr nützlicher Techniken und Programmmuster, die auf diesem Prinzip basieren.

Die ältesten objektorientierten Sprachen sind SIMULA (1967) und Smalltalk (1980). Jüngere und zur Softwareentwicklung weit verbreitete Sprachen sind C++ (1986), Java (1996) und C# (2000).

Funktionale Programmierung

Die Grundelemente der funktionalen Programmierung sind rekursive Funktionen und deren Aufrufe. Mit Funktionen höherer Ordnung, die Funktionen als Parameter oder als Ergebnis haben, können mächtige und flexibel einsetzbare Berechnungsmuster formuliert werden. Wegen der Abwesenheit von Variablen mit Zuweisungen und Ablaufstrukturen werden Berechnungen nicht mit Begriffen der Zustandsänderung formuliert, sondern indem man sie funktional beschreibt. Deshalb wird die funktionale Programmierung auch als deklarativ bezeichnet.

Eine Funktion, die die Elemente einer Liste aufsummiert, können wir in der funktionalen Sprache SML wie folgt definieren:

     fun  ListSum nil      = 0

     |      ListSum (h::t) = h + (ListSum t)

Anhand des Parameters wird unterschieden: Für eine leere Liste (nil) ist das Ergebnis 0. Für eine nicht-leere Liste bezeichnet h das erste Listenelement und t den Rest der Liste. Das Ergebnis ist dann die Summe aus h und dem rekursiven Aufruf (ListSum t). Ein Aufruf mit einer dreielementigen Liste lautet z. B. (ListSum [2, 4, 6 ]) und liefert 12 als Ergebnis.

Dort, wo in der imperativen Lösung eine Schleife verwendet wird, setzt man in der funktionalen Programmierung Rekursion ein. Auch hier werden Namen für Werte benutzt, wie hier h und t. An solche Variablen wird aber jeweils genau einmal, beim Aufruf, ein Wert gebunden – im Gegensatz zu den imperativen Variablen, deren Wert durch Zuweisung geändert wird.

Die obige Funktion kann man zu einem allgemeiner anwendbaren Berechnungsmuster erweitern, indem man statt des +-Operators eine Funktion f als Parameter einführt:

     fun  ListFold (f, n, nil)    = n

     |      ListFold (f, n, h::t)  = f (h, (ListFold (f, n, t))

Es kommt ein dritter Parameter n hinzu, der das Ergebnis für die leere Liste bestimmt. In dem rekursiven Aufruf werden f und n einfach weitergegeben. Ein Aufruf des Berechnungs­schemas gibt die Verknüpfungsfunktion und den dazu passenden neutralen Wert für die leere Liste explizit an. In den folgenden Aufrufen von ListFold wird die Verknüpfungsfunktion jeweils durch die Notation einer unbenannten Funktion (sog. Lambda-Ausdruck) - hier zur Multiplikation, Addition, bwz. zur logischen Oder-Verknüpfung: 

   ListFold (fn (a, b) => a + b, 0, [2, 4, 6])

   ListFold (fn (a, b) => a * b, 1, [2, 4, 6])

   ListFold (fn (a, b) => a or b, false, [true, false, true])

Diese Funktion kann auf Parameter unterschiedlicher Typen angewandt werden, sie ist polymorph.

Die älteste funktionale Sprache ist Lisp (1960). Sie unterscheidet Datentypen nicht und kommt mit extrem wenigen Sprachkonstrukten aus. In weiterentwickelten Varianten wird sie noch heute eingesetzt. Jüngere, weit verbreitete Sprachen sind ML (1974), SML (1990) und Haskell (1990).

Logische Programmierung

Die logische Programmierung basiert auf der Prädikatenlogik. Ein logisches Programm kann als Folge von prädikatenlogischen Formeln verstanden werden. Um eine Aufgabe zu lösen, beschreibt man Eigenschaften, die eine Lösung erfüllen muss, statt einen Rechenweg, der eine Lösung findet.

Sollen z. B. Listen von Zahlen sortiert werden, würde man Prädikate sortiert(L) und permutation(Lu,Ls) definieren. Dann sucht man z. B. nach einer Liste Ls für die gilt  permutation([3,1,6],Ls) und sortiert(Ls). Natürlich kann ein logisches Programmier­system zu solch einer Anfrage nicht einen Sortieralgorithmus wie z. B. Quicksort erfinden. Stattdessen werden die Antworten durch systematisches Ausprobieren nach dem Backtracking-Verfahren gesucht. In dieser reinen Form enthalten logische Programme keinerlei algorithmische Elemente oder Hinweise auf ihren Ablauf. Sie werden deshalb als deklarativ bezeichnet. In der Praxis kommt man ohne Ablaufsteuerung jedoch nicht aus, wenn man in rekursiven Programmen die Terminierung sicherstellen will und wenn man die langen Ausführungszeiten des Backtracking reduzieren will.

Programme der logischen Sprache Prolog bestehen aus Fakten, Regeln und Anfragen, die zusammengefasst Klauseln heißen. Unser Beispiel für das Aufsummieren der Elemente von Listen kann man wie folgt formulieren:

   ListSum([],0).

   ListSum([H|T],X):- ListSum(T,Y), X is H+Y.

   ?-ListSum([2,4,6],W).

Die beiden ersten Klauseln definieren zusammen die Relation ListSum jeweils zwischen einer Liste und ihrer Summe. Die erste Klausel ist ein Fakt, der der leeren Liste 0 zuordnet. Die zweite Klausel ist eine rekursiv definierte Regel. In Prädikatenlogik bedeutet sie: „Für alle Kombinationen von Werten für H, T, X und Y gilt: Aus ListSum(T,Y) und X is H+Y folgt ListSum([H|T],X)." [H|T] steht für eine Liste mit erstem Element H und der Restliste T. Die dritte Klausel ist eine Anfrage, die in Prädikatenlogik bedeutet: „Gibt es einen Wert für W, sodass ListSum([2,4,6],W) gilt?“.

Der Prolog-Interpretierer wendet auf solch eine Anfrage eine passende Klausel an (das ist hier die zweite), bindet Werte an die Variablen: H=2, T=[4,6] und W=X, löst dann rekursiv die neue Anfrage ListSum([4,6],Y), erhält Y=10 und berechnet durch Addition X=12 und damit die Antwort W=12.

An diesem Beispiel der Summe von Listenelementen sieht man, dass Prolog-Programme außerordentlich kompakt formuliert werden können, da auf Typangaben und Deklarationen völlig verzichtet wird. Aus diesem Grund und wegen der deklarativen Formulierungen eignet sich die Sprache gut für die Entwicklung von Prototypen für Software-Lösungen. Dabei kann man die Ineffizienz der Ausführung durchaus in Kauf nehmen. Typische Anwendungsgebiete von Prolog sind Expertensysteme und Datenbankanwendungen. Prolog ist die bedeutendste logische Programmiersprache. Sie wurde 1972 entwickelt, ist noch heute weit verbreitet und wird in vielen Varianten angewandt.

Literatur

Arnold, K., Gosling, J.: The Java Programming Language. 4th ed. Addison-Wesley, 2005.

Clocksin, W.F., Mellish, C.S.: Programming in Prolog. 5th ed. Springer, 2003.

Harbison, S.P., Steele, G.L.: C: A - Reference Manual. 5rd ed. Prentice Hall, 2002.

Paulson, L. C.: ML for the Working Programmer. 2nd ed. Cambridge University Press, 1996.

Autor


 

Prof. Dr. Uwe Kastens, Universität Paderborn, Institut für Informatik, Fürstenallee 11, 33102 Paderborn

Autoreninfo


Zuletzt bearbeitet: 03.09.2013 13:28
Letzter Abruf: 23.10.2017 00:47
Artikelaktionen