Skriptsprachen-Hassen leichtgemacht
"Es reicht nicht, keine Idee zu haben,
wenn man unfähig ist, sie nicht umzusetzen."
Diese Seite ist den Entwicklern von Übersetzern und Interpretern
und den Sprachstandardisierern in aller Welt gewidmet,
die sich der Flut von Wünschen erwehren müssen,
doch bitte diese oder jene schlechte Programmierergewohnheit
durch geeignete Spracherweiterungen zu unterstützen.
Skriptsprachen allgemein
Wer von C/C++ genügend abgeschreckt wurde,
den treibt es zumeist geradewegs in die Fänge
dubioser Skript-Sprachen oder BASIC-Dialekte.
Ohne die Macken von C wirklich verstanden zu haben,
haben sich etliche Hobbyprogrammierer hingesetzt
und ihre eigene Sprache entwickelt.
Häufig dienen die Sprachen,
die man in der Schule gelernt hat, als Ausgangspunkt:
Pascal
(speziell Borlands verhundstes Turbo-Pascal)
und
C
.
Das Ergebnis sind Sprachen,
die Syntax und Konzepte aus anderen Sprachen zusammenpanschen,
um einfache Aufgaben noch einfacher
und schwierige noch schwieriger zu machen.
Manchmal müssen,
wie bei Rebol,
auch natürliche Sprachen als Vorbild herhalten.
Warum eigentlich?
Natürliche Sprachen wurden niemals auf Konsistenz,
auf Eindeutigkeit,
auf leichte Erlernbarkeit oder
auf irgend etwas anderes getrimmt.
Ja selbst wenn mal ein paar leichte Änderungen
zugunsten von mehr Konsistenz
in Form einer Rechtschreibreform eingeführt werden,
gehen etliche Leute auf die Barrikaden.
Warum also ausgerechnet natürliche Sprachen?
Als einziger Grund fällt mir Gewohnheit ein
und dieser Grund ist ziemlich schwach.
Erlernen muss man Programmiersprachen auch dann,
wenn sie sich stark an der natürlichen Sprache orientieren,
also kann man sich auch gleich mit gut durchdachten Sprachen beschäftigen.
Da lernt man sogar noch was
über die Unzulänglichkeiten natürlicher Sprachen.
Das, was man mit Skriptsprachen bevorzugt macht,
trägt den klangvollen Namen
Rapid Prototyping
(nein nicht Rabbit Prototyping!),
zu deutsch: schnelles Zusammenpfuschen,
damit man Interessenten bald etwas vorführen kann.
Nun ist es aber so,
dass sich der normale Programmierer von einmal geschriebenem Programmtext
nicht wieder trennen kann,
also bleibt es für immer bei der Pfuschversion,
um die herum ständig neue Flicken gesetzt werden.
Um diese Vorgehensweise zu unterstützen,
bieten auch Skriptsprachen rudimentäre Mechanismen
zum Zusammenstellen von Bibliotheken.
Diese Modularisierung ist schon deswegen immer unterentwickelt,
weil es keine ausdrücklichen und typsicheren Schnittstellen gibt.
(siehe unten)
Diese Modularisierung wäre auch völlig unnötig,
wenn man Skriptsprachen nur verwenden würde, wozu sie geeignet sind,
nämlich um Wegwerfprogramme zu schreiben.
Es ist neben dem "Ich mache mein eigenes Betriebssystem."
ein wahrer Sport um die Kreation neuer Skriptsprachen entstanden,
der anscheinend zum Ziel hat,
das kürzeste "hello world"-Programm zu ermöglichen.
Man glaubt es eigentlich fast nicht,
dass so viele Dumme immer wieder die gleichen Gedanken haben.
Ganz egal ob
Perl,
Rexx,
Tcl,
Ruby, Rebol,
MatLab,
Euphoria,
Lua,
Python,
Groovy
- sie sehen alle ein bisschen verschieden aus,
teilen aber die gleichen Macken.
- Interaktivität
Was Skript- oder BASIC-Interpreter sicher attraktiv macht,
ist ein eingebauter interaktiver Modus.
In diesem kann man Befehle eingeben und diese werden sofort ausgeführt.
Es ist klar, dass man in solch einem Ausprobiermodus nicht vom Benutzer erwarten kann,
dass er ein Programm mit sauber getrennter Deklaration von Variablen und anschließenden Anweisungen eingibt.
Deswegen erspart man dem Anwender die Deklaration,
weil sich Name und Typ einer Variable bei der ersten Verwendung von selbst ergeben.
- Deklaration
Ein Skript, sprich: ein Programm,
besteht aus einer Aneinanderreihung von Anweisungen,
wie man sie auch im interaktiven Modus ausführen könnte.
Was für die Interaktion ganz nützlich ist,
kann für das Erstellen von ganzen Programmen sehr lästig sein.
Will man zum Beispiel beim Lesen eines Skriptes den Typ einer Variablen herausfinden,
muss man die Stelle im Programm finden, wo sie das erste Mal benutzt wird.
Einen Deklarationsteil an leicht auffindbarer Stelle gibt es nicht!
Aber die erste Zuweisung an eine neue Variable kann auch das Ergebnis eines Unterprogrammes sein.
Dann kann man nur hoffen, dass deren Rückgabewert erstens dokumentiert ist,
zweitens die Dokumentation mit der Wirklichkeit übereinstimmt,
drittens falls die Dokumentation nicht hilft,
dass die Routine übersichtlich ist und nicht zu weit weiterverzweigt und
viertens, der Typ unabhängig vom Kontext zur Laufzeit ist und sich nicht laufend ändert.
- Dynamische Datenverbünde
Da man in Skriptsprachen nichts deklarieren kann
und es auch keine Typenkontrolle gibt,
gilt für Datenverbünde in Skriptsprachen eigentlich immer
(sofern sie von der Sprache angeboten werden),
dass die Datenverbundelemente erst zur Laufzeit eingerichtet werden.
Das schließt das Hinzufügen neuer Elemente
während der Existenz eines Datenverbundes mit ein,
genau wie man Variablen immer zur Laufzeit anlegt.
(Es sollte mich nicht wundern,
wenn man in manchen Sprachen zur Laufzeit
auch Klassen um neue Methoden erweitern kann.)
Es ist also nur schwer möglich, anhand des Programmtextes herauszufinden,
welche Elemente ein Datenverbund tatsächlich enthält.
Dieser Mangel kann natürlich durch eine leicht überschaubare Palette
einfach zu bedienender Programmierwerkzeuge behoben werden.
Was aber noch viel schlimmer ist:
Ändert man die Struktur eines Datenverbundes
oder ändert man einfach nur die Namen der Elemente,
steht einem viel Arbeit bei der Suche der anzupassenden Programmstellen bevor
und man hat zudem keine Garantie, dass man alle Stellen erwischt hat.
Sicher gibt es auch dafür praktische Werkzeuge ...
Aber wie gesagt,
Programmänderungen stehen bei vielen Programmierern anscheinend nicht auf dem Plan,
entweder weil sie die perfekten Softwarearchitekten sind
oder aber weil sie sich schnell mit den Unzulänglichkeiten
des ersten Versuches anfreunden können.
Für letzteres spricht die Architektur vieler Bibliotheken
und dass viele Programmierer ersteres glauben,
dafür spricht die Bestimmtheit, mit der sie ihre Architekturen verteidigen.
- Schnittstellen
Genau wie die Deklaration weggelassen wird,
gibt es auch keine Typbeschränkung bei Parametern für Unterprogramme.
Zum Beispiel eine Routine dup(str,n),
welche die Zeichenkette str n-mal aneinanderhängen soll,
muss immer damit rechnen, Werte anderer Typen übergeben zu bekommen.
Das kann sinnvoll sein, wenn zum Beispiel eine Liste von Zahlen
statt einer Zeichenkette übergeben wird.
Das kann aber auch komplett daneben gehen,
wenn ein Anwender bei der Übergabe die beiden Parameter aus Versehen vertauscht.
Es gibt zwei Möglichkeiten zur Lösung:
Entweder der Programmierer der Routine weist Werte falscher Typen zurück,
sofern die Sprache das Testen auf Typen erlaubt,
oder aber er lässt es bleiben.
Die zweite Lösung wird die häufigere sein und
äußert sich beim Anwender der Routine dadurch,
dass bei falscher Parameterübergabe eine
weit im Innern der Bibliothek versteckte Routine irgendeinen Fehler ausgibt,
der beim ersten Anblick nichts mit dem ursprünglichen Aufruf zu tun hat.
- statische Sicherheit
Da Variablen immer erst zur Laufzeit angelegt werden,
gibt es keine Möglichkeit allein durch Analyse des Quelltextes
die Typen der Variablen auf Konsistenz zu prüfen.
Wenn also an einer Stelle im Programm Äpfel und Birnen addiert werden sollen,
so findet man diesen Fehler erst, wenn man das Programm ausführt
und insbesondere diesen Teil des Programmes testet.
- Schreibfehler
Sehr erquicklich ist es,
durch Verändern eines Variablenwertes den Lauf eines Programmes ändern zu wollen,
und es passiert trotzdem immer das gleiche.
Das kann zum Beispiel auf eine falsche Schreibweise zurückzuführen sein.
Wenn ich die Variable schiffahrt überschreiben will,
es im Programm aber nur schifffahrt gibt,
wird der Interpreter nichts bemängeln,
sondern einfach eine zweite Variable anlegen.
Mit Deklarationspflicht passiert das nicht.
(Bei perl übrigens mit "use strict" erreichbar.)
- Automatische Tests
Natürlich sind alle hier dargestellten Fehlerquellen maßlos überzogen
und treten in der Praxis kaum auf,
denn der seriöse Skriptsprachenbenutzer hat noch einen gewaltigen Trumpf in der Hand:
Automatische Tests, die er geschrieben hat,
um alle Funktionen auf korrekte Ergebnisse zu überprüfen.
Er nimmt sich zwar keine Zeit, um über Schnittstellen und Typen nachzudenken,
dafür umso mehr Zeit für die Kreation automatischer Tests - Ehrenwort!
Dass man mit Typen vielerlei dumme Fehler erkennen kann,
ohne einen Riesenhaufen Tests zu fahren
(die einem auch nur sagen würden,
dass ein Fehler vorhanden ist, aber nicht so genau, wo),
überzeugt ihn nicht,
denn schließlich kann man mit statischer Typenprüfung nicht alle Fehler entdecken.
Viel schneller hat er eine Reihe von Tests entwickelt,
die alle Typenfehler abfangen, und er vergisst keinen!
Außerdem steht statische Typenprüfung dem Ziel
möglichst kurzer Programmtexte direkt entgegen,
wie man an C++ und Java sehen kann,
den bekanntermaßen einzigen Programmiersprachen mit einem Typensystem
(oder so etwas ähnlichem).
- Ordinale Typen
Genau wie C/C++ unterscheiden Skriptsprachen in der Regel nicht zwischen
Wahrheitswerten und Zahlen
und kennen auch keine Aufzählungen oder etwas vergleichbares.
Das führt natürlich zu den gleichen Problemen wie bei C/C++.
Manche Sprachen wie Tcl und Rexx unterscheiden nicht einmal
zwischen Zahlen und Zeichenketten!
Deswegen Obacht: Wann immer jemand seine neue Programmiersprache
als besonders einfach für Anfänger zu erlernen oder zu bedienen bewirbt,
ist äußerste Vorsicht geboten,
denn die Sprache wurde meist auch von einem solchen entwickelt!
Funktionale Sprachen
Es gibt tatsächlich auch sehr hohe Programmiersprachen,
die sich bislang den üblichen "Vereinfachungen" widersetzen konnten,
sehr mächtige Sprachen, mit denen man schnell Programme schreiben kann,
die zwar nicht effizient, aber auf jeden Fall wartbar sind.
Funktionale statisch getypte Sprachen fallen in diese Kategorie
und ich möchte hier exemplarisch
Haskell
nennen.
Funktionale Sprachen haben wirklich das Zeug,
um der Skriptsprachenbewegung den Rang abzulaufen.
Während imperative Sprachen wie Modula, C oder Skriptsprachen
Befehle Schritt für Schritt abarbeiten,
berechnen funktionale Sprachen das Ergebnis von Funktionen,
welche ihrerseits Funktionen aufrufen usw. usf.
Haskell-Funktionen kann man sowohl interaktiv austesten,
als auch lauffähige Programme daraus herstellen.
Trotzdem ist Haskell statisch typsicher und
erlaubt auf einfache Weise die Implementation von Funktionen für beliebige Typen.
Es beweist damit,
dass Interaktion und statische Sicherheit gleichzeitig möglich sind.
Die funktionale Grundidee vereinfacht einiges.
Man braucht keine Unterscheidung zwischen Konstanten und Variablen,
kein "Call by reference", keine Zuweisungen,
keine vordefinierten Schleifenstrukturen,
alles ist überschaubar und die Wirkung der Funktionen kontrollierbar.
Es ist klar, das das nicht so bleiben darf.
Nach und nach entdecken Perl- und Python-Jünger Haskell und erwarten,
dass man Haskell so weit mit syntaktischem Zucker verkleistert,
dass die Schlichtheit des funktionalen Ansatzes nicht mehr erkennbar ist.
Leider scheinen sie damit Erfolg zu haben,
wie Syntaxerweiterungen wie die parallele Listenkomprehension beweisen.
Viele Haskell-Anfänger scheinen sich nicht die Mühe zu machen,
sich mit dem Konzept von Funktionen und
den vielfältigen Einsatzmöglichkeiten von Funktionen höherer Ordnung zu befassen.
Stattdessen meiden sie alles systematische und
errichten ihre Programme hauptsächlich aus syntaktischem Zucker
wie Listenkomprehension, Guards, Pattern-Guards, Infix-Operatoren und do-Notation,
wahlweise auch mit kleinkarierten Rekursionen.
Da sie nichts anderes kennen, werden sie auch nicht müde zu betonen,
wie wichtig solcher syntaktischer Zucker ist.
Zum anderen gibt es immer wieder Angriffe
auf die durch die Funktionen streng geordneten Datenflüsse in Haskell-Programmen.
Ja, die globalen Variablen sind einfach nicht totzukriegen!
"Wenn man ganz sicher gehen will,
eine schlechte Lösung für sein Problem zu finden,
dann sollte man auf das zurückgreifen,
was alle benutzen."
Wie wir im ersten Abschnitt gesehen haben,
sind Allzweck-Skriptsprachen für alles mögliche ungeeignet.
Aber es gibt auch Skriptsprachen, die für manche Anwendungen besonders ungeeignet sind.
Eine davon ist MatLab von MathWorks, welche
- alle oben genannten Nachteile gängiger Skriptsprachen in sich vereint,
- kreativ eigene logische Aussetzer in Typsystem und Standardbibliotheken einbringt.
Um alle Kritiker unweigerlich zum Schweigen zu bringen,
wird MatLab von ungezählten Wissenschaftlern weltweit mit Funktionsbibliotheken versorgt,
die zwar die Sprache kein bisschen aufwerten können,
aber anscheinend Grund genug sind, nicht zu wechseln.
Wohin sollte man auch wechseln.
Es gibt zahlreiche MatLab-Nachbauten wie
RLab,
SciLab,
Octave
und andere.
Die damit verbundenen Skriptsprachen sind so angelegt,
dass sie möglichst keine Macken von MatLab ausräumen,
dafür aber ausreichend inkompatibel zu MatLab-Programmen sind.
Organisation
Integrierte Datentypen
-
Raum für Vektoren
Die schlechte Eignung von MatLab für Mathematik
manifestiert sich schon in einigen Begriffsverwirrungen.
In der Mathematik bezeichnet man diejenigen Objekte
als Vektoren bezüglich eines Körpers von Skalaren und bezüglich gewisser Operationen,
wenn diese Operationen ein paar Eigenschaften erfüllen.
Die grundlegenden Vektorrechenoperationen sind Vektoraddition und Streckung.
Eine wichtige Eigenschaft ist zum Beispiel,
dass die Streckung bezüglich der Addition distributiv sein muss.
Wenn also a ein Skalar und x und y Vektoren sind,
dann muss a*(x+y) = a*x + a*y gelten.
Daran erkennt man schon,
dass ein Körper wie die Menge der rationalen Zahlen
bezüglich der üblichen Addition und Multiplikation
bereits selbst ein Vektorraum ist.
Im Computer hat man es meistens mit endlichdimensionalen Vektoren zu tun
und stellt diese einfach mit einem eindimensionalen Feld dar.
Vermutlich hat die Vektorrechnung auch
mit diesem engen Verständnis von Vektoren begonnen.
Für viele Menschen ist das Verständnis sogar noch enger
und beschränkt sich auf dreielementige Felder.
Nun definiert man noch Matrizen in Form von zweidimensionalen Feldern.
Matrizen werden als Darstellung für lineare Operatoren auf Vektoren verwendet.
Nun sind aber Matrizen bezüglich der Matrizenaddition und der Skalierung
selbst wieder Vektoren.
Auch die linearen Operatoren, welche durch Matrizen dargestellt werden,
bilden mit geeigneter Operatoraddition und -skalierung einen Vektorraum.
Daher ist es eigentlich nicht zweckmäßig
eindimensionale Felder als Vektoren zu bezeichnen
(Was sie nämlich auch nicht immer sind,
zum Beispiel wenn die Elemente keinem Körper angehören.)
und alles andere nicht.
Besser passt hier das Wort Tupel oder computergerecht "Feld".
In der linearen Algebra haben sich auch die Worte Zeilen- und Spaltenvektor eingebürgert,
welche dem mathematischen Verständnis von Vektoren eigentlich zuwiderlaufen.
Wenn eine lineare Struktur einem mathematischen Objekt den Beinamen "Vektor" erlaubt,
was muss es dann für zusätzliche Strukturen geben,
damit dieses Objekt den Namen Zeilen- oder Spaltenvektor tragen darf?
Nein, ich denke, dass die Worte Zeilen- und Spaltenvektoren
einer Vermengung von Vektoren mit Matrizen entspringen.
Ich habe im Folgenden das Problem,
dass ich mich in die Philosophie hinter MatLab hineindenken muss
und dass ich zur Erklärung solche Begriffe wie Zeilen- und Spaltenvektor übernehmen muss
und dass ich das Wort Vektor verwenden muss,
obwohl ich eigentlich Tupel oder Polynom meine.
-
Skalare sind Matrizen
Unbestätigten Gerüchten zufolge
soll MatLab dereinst als Programm entwickelt worden sein,
das die Arbeit mit Matrizen vereinfacht,
also all das, was man unter (numerischer) Linearer Algebra zusammenfasst.
Deswegen ist in MatLab der Typ Matrix
der Stoff aus dem die Variablen sind.
Na gut, aber ohne Vektoren
sind die Matrizen nicht viel wert.
Vektoren gibt es aber nicht,
nur einzeilige oder einspaltige Matrizen.
Nicht einmal Skalare gibt es,
es gibt nur 1x1-Matrizen.
Sollte eine Matrix aus irgendeinem Grunde
auf die Dimension 1x1 zusammenschrumpfen,
verhält sie sich völlig anders.
Zum Beispiel ist die Matrixmultiplikation
einer 1x1-Matrix A mit einer 2x2-Matrix B nicht definiert.
MatLab dagegen betrachtet A einfach als Skalar und
skaliert B mit der skalaren Interpretation von A.
Jedem Erstsemesterstudenten erklärt man lang und ausführlich,
worin der Unterschied besteht zwischen einer Zahl und
einer einelementigen Menge, die genau diese Zahl enthält.
Ob die MatLab-Entwickler nicht einmal bis zum ersten Semester ... ?
Merke:
In MatLab wird nicht das Komplizierte aus dem Einfachen aufgebaut,
sondern das Einfache als Spezialfall des Komplizierten angesehen.
-
Matrizen sind irgendwie auch Vektoren
Wenn es keine Vektoren gibt,
dann kann es auch keine Indizierung der Form A(5) geben, stimmt's?
Weit gefehlt, diese Schreibweise wird von MatLab akzeptiert.
MatLab denkt sich, dass die Matrixeinträge einer n x m-Matrix
ohnehin hintereinander im Speicher liegen
und man diesen Speicherbereich auch als Vektor der Länge n*m auffassen könnte.
Und von genau diesem Vektor wird der fünfte Eintrag zurückgeliefert.
Das ganze läuft auf Spalten- und Zeilenvektoren
tatsächlich auf die Abfrage einer Vektorkomponente hinaus.
Bei Matrizen ergibt es Humbug,
und schon ist ein neues Feature geboren!
Merke:
Falls ein Programm ohne Fehlermeldung läuft,
aber nicht tut was es soll,
durchkämme einfach alle Indizierungen von Matrizen
und schaue ob du irgendwo einen Index vergessen hast!
-
Vektoren, auch quadratisch
Wir haben gesehen, dass die Vektorkomponentenadressierung
funktioniert, ohne dass MatLab Vektoren kennt!
Wie sieht es mit der Funktion zeros aus?
Der Funktionsaufruf zeros(dim1,dim2,dim3,...,dimn)
legt einen Tensor der Stufe n
mit den Dimensionen dim1, dim2, dim3, ..., dimn an.
Erschafft man mit zeros(3)
also einen Vektor der Größe drei?
Ergibt dieser Funktionsaufruf einen
- Spaltenvektor
- Zeilenvektor
- Fehlermeldung zur Laufzeit
?
Na? Wettgebot?
Alles falsch.
Der gezeigte Aufruf erzeugt eine (quadratische) 3x3-Matrix.
Merke:
Hast du drei Alternativen, wähle die vierte.
-
Zeile oder Spalte?
Da MatLab Vektoren als Matrizen betrachtet,
muss man sich als Programmierer jedesmal die Frage stellen,
ob man lieber zum Zeilen- oder lieber zum Spaltenvektor greift.
Manche Funktionen haben es lieber so, andere lieber anders,
und wieder andere handhaben es irgendwie sonst.
Welches Format eine Funktion der Standardbibliotheken erwartet,
hängt in erster Linie vom Programmierer, von der MatLab-Version
und nicht zuletzt vom Wetter ab.
Die wichtigste Funktion ist deshalb die size-Funktion,
mit der man nachforschen kann, warum an einer Stelle
zwei Vektoren der gleichen Länge nicht miteinander verrechnet werden konnten.
Merke:
Mache dir niemals die Mühe in deinem Programm nach einem Fehler zu suchen,
der nur bei kurzen Datensätzen auftritt.
Es handelt sich meist um das Problem,
dass sich ein Vektor der Länge 1 gleichzeitig als
Skalar, Spaltenvektor, Zeilenvektor und 1x1-Matrix interpretieren lässt!
-
Länge von Matrizen
Die MatLab-Funktion length bestimmt die Länge eines Vektors,
ganz wie man es erwartet:
>> length(zeros(1,10))
ans =
10
>> length(zeros(10,1))
ans =
10
Was aber berechnet length für eine Matrix als Eingabe?
Hier die ultimative Quizfrage an den mitdenkenden Leser:
Was ergibt [length(zeros(4,3)), length(zeros(3,4))]?
- undefiniert
- [12,12]
- [4,3]
- [3,4]
- [4,4]
- [3,3]
Was wird wohl die richtige Antwort sein?
Undefiniert? Ihr Spaßvögel!
MatLab gibt immer eine Antwort, notfalls eine blöde.
Ihr meint 12, weil MatLab Matrizen manchmal auch zu Vektoren aufreiht?
(Siehe oben)
Einen Versuch gebe ich euch noch.
Die richtige Antwort ist natürlich 4.
MatLab nimmt die Tensorstufe mit der höchsten Dimension.
Danke an Helmut Podhaisky für diesen Beitrag!
Ihm hat diese Eigenart von MatLab dabei geholfen,
sich in einer Vorlesung vor seinen Studenten zu blamieren.
Merke:
MatLab ist extra unlogisch entworfen, um die Benutzer dazu zu erziehen,
auch für triviale Funktionen das Handbuch zu konsultieren.
-
3 waagerecht oder 4 senkrecht: Französischer Mathematiker
Da gibt es zum Beispiel die Fouriertransformation,
eine lineare Transformation die man zunächst für Vektoren definieren kann.
Nun möchte MatLab zwischen Spalten- und Zeilenvektoren unterscheiden,
obwohl die eigentliche Berechnung in beiden Fällen die gleiche ist.
>> fft([1,0])
ans =
1 1
>> fft([1;0])
ans =
1
1
Wie kann man die Fouriertransformation für eine Matrix definieren?
Gefragt ist eine halbwegs natürliche Erweiterung,
deren Einschränkungen auf einzeilige oder einspaltige Matrizen
zu den obigen Ergebnissen führt.
Dazu fällt mir eigentlich nur ein,
die Matrix erst zeilenweise und dann spaltenweise zu transformieren.
Dabei ist die Reihenfolge egal und das Ergebnis ist das,
was man gemeinhin als die Fouriertransformation von zweidimensionalen Daten
(beispielsweise Graustufenbildern) betrachtet.
Es verträgt sich darüber hinaus mit obigen Ergebnissen,
weil die 1D-Fouriertransformation einen Vektor der Länge 1 nicht verändert.
Es spricht also vieles dafür, unter dem Namen fft
sowohl die eindimensionale, als auch die zweidimensionale,
oder am besten gleich jede beliebig höherdimensionale Fouriertransformation anzubieten.
Und genau deswegen macht es MatLab anders!
Mit 2D-Interpretation wäre fft([1,0;0,0]) = [1,1;1,1],
während MatLab sagt:
>> fft([1,0;0,0])
ans =
1 0
1 0
MatLab führt die Transformation nämlich normalerweise immer spaltenweise aus.
Nur wenn die Spaltenvektoren zufällig die Größe 1 besitzen,
dann macht es etwas ganz anderes,
nämlich eine Transformation der einzigen Matrixzeile.
Merke:
Automatismen sind besonders hilfreich,
wenn man zusätzliche Abfragen einbauen muss,
um ein konsistentes Verhalten herzustellen.
-
Zitronenfalter
Noch lustiger geht es beim Falten zu,
welches von MatLab durch die Funktion conv erledigt wird.
Falten ist salopp gesagt wie die Schulmultiplikation nur ohne Übertrag
oder auch das, was mit Polynomkoeffizienten bei der Polynommultiplikation passiert.
(Algebraisch gesehen sind Polynome einfach Tupel
und die Polynommultiplikation wird als Faltung der Tupel definiert.)
Auch die Faltung wird zunächst für Vektoren definiert
und dann stellt sich wieder die Frage,
wie man mit Zeilen- und Spaltenvektoren verfährt
und wie man sie auf Matrizen verallgemeinert.
Was die Sache im Gegensatz zur Fouriertransformation etwas vertrackter macht, ist,
dass es zwei Vektoren als Operanden gibt
und diese nicht beide gleich orientiert sein müssen.
Dennoch ließen sich alle Fälle elegant und konsequent
durch eine zweidimensionale Faltung abdecken.
Demnach wäre conv([1,1],[1;1]) gleich [1,1;1,1].
Aber MatLab ist konsistent inkonsistent, und deswegen bekommen wir:
>> conv([1,1],[1;1])
ans =
1
2
1
Also kommen im Zweifelsfalle Spaltenvektoren heraus?
>> conv([1,1,1],[1;1])
ans =
1 2 2 1
Anscheinend nicht. Gibt der längere der Vektoren den Ausschlag?
Wo liegt der Sinn?
Ich weiß es nicht, die Online-Hilfe (Version 6.5.1.199709) weiß es auch nicht.
Vermutlich wissen es auch die MatLab-Entwickler nicht.
Doch das Mysterium conv bietet noch weit mehr:
>> conv(zeros([1 1 2]), zeros([1 2]))
ans =
0 0 0
>> conv(zeros([1 1 3]), zeros([1 2]))
??? Attempt to grow array along ambiguous dimension.
Error in ==> /usr/local/pkgs/matlab-6.5.1.199709/toolbox/matlab/datafun/conv.m
On line 30 ==> a(na+nb-1) = 0;
Merke:
Je gründlicher man faltet,
desto aufwendiger kann man entwickeln.
-
Tensoren mit flüchtigen Dimensionen
MatLab beherrscht nicht nur Matrizen sondern auch Tensoren!
Sie sind genauso räusper einfach zu benutzen wie Matrizen.
Wir halten fest,
dass es in MatLab Tensoren ab Stufe zwei aufwärts gibt,
dagegen gibt es keine der Stufe 0 oder 1.
Also ist bei Tensoren ab Stufe 2 alles in Butter und logisch?
Logisch wäre zum Beispiel, dass sich die Stufenzahl des Tensors verringert,
wenn ich in einer Stufe den Index festhalte.
Es müsste demnach A(:,1,:) eine Matrix sein,
wohingegen A(:,1:5,:) wiederum ein dreistufiger Tensor sein müsste.
So ist es aber nicht, denn wir erinnern uns, dass es keine Skalare gibt,
sondern bestenfalls Vektoren, welche eigentlich auch Matrizen sind.
Damit sind aber auch einfache Indizes Matrizen, oder Vektoren, je nach dem.
Daher ist nicht nur A(:,1:5,:)
sondern auch A(:,1,:)
ein dreistufiger Tensor.
Letzterer hat dann in der zweiten Stufe die Dimension 1.
Und wie bekomme ich die weg?
Ganz einfach: squeeze entfernt alle Stufen mit Dimension eins!
Und wenn der Tensor noch in einer anderen Stufe
gewolltermaßen die Dimension 1 besitzt?
Na ratet mal!
Außerdem kann zum Beispiel ein 1x1x5-Tensor
gar nicht zu einem Vektor der Länge 5 geschrumpft werden,
weil MatLab die doch nicht kennt.
Wird das dann eine 1x5-Matrix oder eine 5x1-Matrix,
sprich: Wird das ein Zeilen- bzw. ein Spaltenvektor?
Macht squeeze aus einem Spalten- einen Zeilenvektor oder umgekehrt?
Zum Glück gibt es die reshape-Funktion,
welcher man das Format das Tensors genau mitteilen kann.
Diese achtet aber nur darauf,
dass die Gesamtanzahl der Elemente erhalten bleibt.
Sie ist also eine weitere Gelegenheit,
um Fehler in das Programm einzubauen.
Merke:
Suche in der MatLab-Hilfe niemals nach Antworten auf solche Detailfragen
wie der genauen Funktionsweise von squeeze.
Probier's aus und hoffe, dass in der nächsten MatLab-Version
diese Funktionsweise erhalten bleibt.
-
Automatische Anpassung von Matrizengrößen
Hin und wieder macht MatLab passend, was nicht passt.
Wenn man als einen Operanden einen Skalar wählt,
dann wird dieser Skalar auf die Größe des anderen Operanden aufgeblasen.
In 1 + [1,2,1] wird
die 1 zu [1,1,1] umgedeutet und schon passt es!
Da Skalare nur 1x1-Matrizen sind,
tut auch A - lambda * eye nicht,
was es suggeriert.
Zwar steht eye für die Identitätsmatrix,
aber das fehlende Argument wird nicht bemängelt,
sondern als 1 angenommen.
Also ist eye der skalare Wert 1,
so dass von allen Elementen von A
der Wert lambda abgezogen wird.
Merke:
Manche müssen aus jeder Mücke einen Elefanten machen!
-
Indizes beginnen bei 1
Komfortabel wäre es, wenn MatLab die Feldgrenzen beliebig vorgeben ließe.
So wie in Modula zum Beispiel.
Dann könnte man eine Filtermaske bequem in einem Feld mit dem Indexbereich
(-10:10) unterbringen.
Das geht leider nicht.
Wenigstens könnte MatLab bei 0 beginnen zu
zählen,
weil man dann alle abweichenden Indexmengen durch einfache Subtraktion
des gewünschten Startindexes realisieren könnte.
Ich würde also ein Feld mit den Indizes 0 bis 20 nehmen
und bei jeder Indizierung n-(-10) schreiben (oder gleich n+10),
wobei n von -10 bis 10 laufen darf.
Ihr kennt MatLabs Antwort auf solche Ansinnen ...
Merke:
Bereits im tiefen Mittelalter hatte man die Null nicht als Zahl betrachtet.
-
junge, dynamische Felder
Ist euch ein bisschen schwindelig geworden,
vor lauter falschen und richtigen Tensoren, Matrizen, Vektoren und Skalaren und
mit Stufen, die manchmal keine sind?
Es kommt noch besser:
Matrizen und Vektoren wachsen mitunter auch von alleine.
Sobald man einen zu großen Index angibt,
wird das Feld stillschweigend vergrößert.
Eigentlich praktisch, nur leider kann man es nicht abstellen.
Ich weiß, dass viele Anwender nicht verstehen,
was an so einem Mechanismus schlecht sein soll.
Meine Gründe sind die folgenden:
-
Endlich-dimensionale Vektoren (im Computer meist als Felder implementiert)
sind von der Idee her eigentlich in der Länge beschränkt,
deshalb sollte man Vektoren einmal mit der gewünschten Länge anlegen
und dann nur noch elementweise ändern.
Für Notfälle könnte man noch eine Funktion bereitstellen,
die einen kleinen Vektor in einen größeren umkopiert.
Mit einer Längenänderung
allein durch Beschreiben eines nicht-existenten Elementes
rechnet der Programmierer allerdings in der Regel nicht.
Taucht im Programm ein zu großer Index auf,
ist das eher ein Hinweis auf einen Programmierfehler.
-
Für denjenigen der wirklich eine dynamisch wachsende Struktur braucht,
sollte so etwas auch angeboten werden,
doppelt verkettete Listen zum Beispiel.
Nutzt man solch eine dynamische Struktur,
ist für jeden klar, dass es sich um eine Datensammlung
variabler Größe handelt.
Ein Feld als dynamische Struktur ist, ganz nebenbei bemerkt, sehr ineffizient,
weil für jede Vergrößerung des Feldes der gesamte Feldinhalt
umkopiert werden muss.
Studenten nehmen diesen Fettnapf tatsächlich gerne mit und
verwandeln ohne Skrupel einen Linearzeitalgorithmus
in einen mit kubischer Laufzeit.
Merke:
Erst wenn die letzte Cache-Zeile entleert,
die letzte Speicherzelle belegt und
das letzte Byte der Auslagerungspartition ausgeschöpft ist,
wird man merken, dass man Feldindizes nicht endlos groß wählen kann.
-
Simultane Änderung mehrerer Feldeinträge
MatLab verwöhnt seine Benutzer mit vielerlei praktischen Einrichtungen
wie zum Beispiel dem simultanen Beschreiben von Vektorkomponenten.
>> x=0:10
x =
0 1 2 3 4 5 6 7 8 9 10
>> x([5,7,9])=[-1,-2,-3]
x =
0 1 2 3 -1 5 -2 7 -3 9 10
Wie ihr seht, werden die fünfte, die siebente und die neunte Komponente
mit den korrespondierenden Werten aus dem Vektor [-1,-2,-3] belegt.
Wie man das von einer Hochsicherheitssprache wie MatLab nicht anders erwartet,
können einem da auch keine Fehler unterlaufen.
Tippfehler werden souverän zurückgewiesen.
>> x([5,7,9])=[-1,-2]
??? In an assignment A(I) = B, the number of elements in B and I must be the same.
Selbstverständlich sind solche Zuweisungen nicht erlaubt!
Wie konnte ich MatLab nur misstrauen, tse, tse.
>>x([5,7,9])=[-1]
x =
0 1 2 3 -1 5 -1 7 -1 9 10
Aber was ist das?
Arrrg, nein, natürlich die Elefantenmücken, wie konnte ich das vergessen!
Wenigstens wird er doch meckern, wenn ich einen nulldimensionalen Vektor übergebe?
>>x([5,7,9])=[]
x =
0 1 2 3 5 7 9 10
Uff, was ist das? Gleich einen Fehlerbericht an MathWorks schicken?
Mitnichten! Ihr habt soeben eine großartige Eigenschaft von MatLab entdeckt:
Das "empty matrix assigment" erlaubt es, Komponenten zu löschen
(und die nachfolgenden nachrücken zu lassen).
Fassen wir zusammen:
Die logische Interpolation zwischen dem Zuweisen leerer Vektoren
und dem Zuweisen von Vektoren an gleichlange Zielvektorabschnitte wäre:
Bei der Zuweisung x(indexfeld) = y
werden alle durch indexfeld adressierten Werte in x
durch die korrespondieren Werte von y überschrieben.
Reichen die Werte in y nicht aus,
werden die entsprechenden Komponenten in x gelöscht.
Für den Fall, dass y länger als indexfeld ist,
könnt ihr euch ein originelles Verhalten ausdenken.
Diese Lösung wäre konsistent und
die MatLab-Benutzer müssten trotzdem nicht
auf eine ihrer geschätzten Fehlerquellen verzichten!
Merke:
Zum Löschen von Vektorkomponenten so etwas schlichtes wie
remove(x,indexfeld) zuzulassen,
war natürlich völlig indiskutabel, weil überhaupt nicht hip.
-
Logische Zahlen
Ich erwähnte bereits, dass Skriptsprachen in der Regel
Wahrheitswerte und Zahlen
nicht unterscheiden.
Das ist im Prinzip bei MatLab auch so,
aber in neueren Versionen (mir liegt zum Test vor: R12, 6.0.0.88)
unterscheidet MatLab zwischen "logischen" und "numerischen" Einsen.
Eine "logische" Eins ist sowas wie "wahr", ist doch logisch!
Natürlich wird meistens trotzdem implizit konvertiert,
sowohl von "logisch" nach "numerisch" ((0<1)+2),
als auch von "numerisch" nach "logisch" (and(2,3)).
Manchmal aber auch nicht.
Zum Beispiel verhalten sich bei Indizierung
logische und numerische Einsen völlig anders,
obwohl sie identisch angezeigt werden.
Wenn v=1:10 gesetzt wurde, dann ergibt
- v(4:6), es werden das 4. bis 6. Element ausgegeben,
also [4 5 6]
- v([0 0 0 1 1 1 0 0 0 0]), Fehler wegen Index 0
- v(logical([0 0 0 1 1 1 0 0 0 0])),
es werden alle Einträge von v ausgegeben,
für die das korrespondierende Element im Indexvektor eine (logische) Eins ist,
also ebenfalls [4 5 6]
Merke:
Wenn du die Benutzer deiner Funktionen mal so richtig zur Verzweifelung treiben willst,
dann gib doch einfach relativ zufällig mal logische und mal numerische Zahlen zurück.
-
Ganze und kaputte Zahlen
Als ich weiter oben zeigte,
wie man mit der Näherung pi = 3 arbeiten kann,
habt ihr euch bestimmt gedacht:
"Wenn man Müll hineinsteckt, kommt natürlich Müll heraus -
Das ist nicht MatLabs Schuld!"
Das war aber nur die halbe Wahrheit,
denn MatLab kommt im Zweifelsfall auch selbst auf die Näherung für π.
>> [pi exp(1) int8(3)]
ans =
3 3 3
Was ist passiert?
MatLab kann in einer Matrix nur Zahlen eines einzigen Typs halten.
Das sehe ich gerne ein,
eine Matrix soll ja Matrixmultiplikationen und dergleichen unterstützen.
Das heißt,
selbst wenn eine Matrix unterschiedliche Zahlentypen in den Elementen erlauben würde,
müsste sich MatLab spätestens bei einer Multiplikation
eines ganzzahligen mit einem gebrochenen Matrixelement überlegen,
ob es ganzzahlig oder gebrochen weiterrechnen will.
Wenn man in eine Matrix eine gebrochene Zahl und eine ganze Zahl steckt
und MatLab nur einen Zahlentyp erlaubt,
dann kann MatLab im Wesentlichen auf drei verschiedene Weisen darauf reagieren:
-
MatLab rundet die gebrochene Zahl und erhält eine ganze Zahl.
Da die Gleitkommazahlendarstellung größere Zahlen zulässt,
als die Ganzzahldarstellung, muss man auch den Fall eines Überlaufes behandeln.
Bei kleinen Zahlen, gar Zahlen nahe null, ist der relative Rundungsfehler sehr groß.
-
MatLab konvertiert die ganze Zahl in eine gebrochene Zahl.
Konvertiert man eine ganze 32-Bit-Zahl in eine 32-Bit-Gleitkommazahl,
so lassen sich große ganze Zahlen nicht ganz genau abbilden,
weil das Gleitkommaformat nur 23 Binärstellen Genauigkeit bietet.
-
MatLab konvertiert die Zahlen überhaupt nicht automatisch
und gibt einen Fehler aus,
damit der Programmierer ausdrücklich hinschreibt,
welchen Typ die Elemente der Matrix haben sollen.
Weil die Konvertierung in beide Richtungen mit Verlust behaftet ist,
würde ich überhaupt nicht automatisch konvertieren.
Mein Favorit ist also Option 3.
MatLab will aber dynamisch sein und den Programmierer nicht mit Fehlermeldungen belästigen.
Dann ist sicher Option 2 die weniger problematische von den beiden verbleibenden.
Von daher ist klar, dass MatLab den Weg Nummer 1 beschreitet
und damit das gezeigte Ergebnis liefert.
Danke an Christine Staiger für diesen Tip!
Sie hat eine Woche ihres Arbeitslebens damit vergeudet,
einen Fehler in einem komplexen Programm mit riesigen Matrizen zu finden,
der auf dieser automatischen Konvertierung beruhte.
-
Überläufer und Unterläufer
So jetzt wollen wir mal testen,
ob ihr das eben erlernte Wissen auch anwenden könnt.
Ja ja, nicht einfach wegducken und hinter dem Rechner verstecken!
Was ergibt
>> 5 - uint8(7)
?
Welche der folgenden Antworten trifft zu? - Und ohne Ausprobieren bitte!
- -2
- 254
- 0
- undefiniert
Was, "0" kommt heraus?
Das ist richtig - ihr habt es doch ausprobiert!
Tatsächlich konvertiert auch hier MatLab
die Operanden der Subtraktion zu dem Typ
mit dem kleinsten Zahlenbereich (uint8) -
meldet dann aber keinen Unterlauf,
sondern verwendet Arithmetik mit Sättigung.
Für diesen Tip gilt mein Dank Johannes Engels,
der damit ebenfalls einen schwer zu findenden Fehler fabriziert hat.
Funktionen
-
Austauschbarkeit von Ausdrücken
Wir sind aus der Mathematik ein Prinzip gewohnt,
das man vielleicht funktionales Prinzip oder Baukastenprinzip nennen kann.
Wir haben Bausteine, etwa "1" und "3" und
können diese auf verschiedene Weisen zusammensetzen, zum Beispiel zu "1+3".
Eine zentrale Eigenschaft des Zusammensetzens ist,
dass sich die Bedeutung der Bausteine durch das Zusammensetzen nicht ändert.
Also überall, wo ich in einer mathematischen Formel "1" hinschreibe,
könnte ich genausogut "(cos 0)" hinschreiben.
"1" und "(cos 0)" bezeichnen beide den gleichen Wert
und das völlig unabhängig vom Ergebnis anderer Berechnungen.
"1 + gamma 2" und "(cos 0) + (gamma 2)" bezeichnen den gleiche Wert.
Wir können uns dessen sicher sein, auch wenn wir nicht einmal wissen,
was "gamma 2" überhaupt bedeutet.
Wir haben uns auch daran gewöhnt,
dass in imperativen Sprachen dieses Prinzip häufig unterwandert wird.
So gibt es einen Zufallszahlengenerator (in MatLab rand),
der wie eine Funktion aufgerufen wird,
aber ständig andere Werte zurückliefert.
Also gut, MatLab ist keine funktionale Sprache wie Haskell sondern "nur" eine imperative.
MatLab hat aber noch mehr zu bieten.
Das Baukastenprinzip ignoriert es nämlich selbst dann,
wenn man es mit reinen Funktionen zu tun hat.
Bei einer reinen Funktion hängt das Ergebnis ausschließlich von den Argumenten ab
und die Operation hat keine Nebenwirkungen.
Wenn zum Beispiel folgendes eingegeben wurde
>> A = [1 2; 3 4]
A =
1 2
3 4
>> A(1,:)
ans =
1 2
dann müsste man in dem Ausdruck A(1,:)
das A durch die ausgeschriebene Matrix ersetzen dürfen.
Nun passt mal auf:
>> [1 2; 3 4](1,:)
??? [1 2; 3 4](1,:)
|
Error: Missing operator, comma, or semicolon.
Merke:
Kein Zwischenergebnis ist so irrelevant,
dass man dafür auf eine eigene Variable verzichten könnte.
-
Leerzeichen statt Komma
Das Baukastenprinzip wird noch an anderer Stelle verletzt.
Außerhalb von eckigen Klammern werden Leerzeichen ignoriert,
innerhalb ändert sich das aber.
Während 3 -1 für den Zahlenwert 2 steht,
wird [3 -1] nicht als 1x1-Matrix mit dem Wert 2 interpretiert,
sondern als 1x2-Matrix mit den Werten 3 und -1.
Und [3 - 1]? Wieder als 2, wer hätte das gedacht!
Merke:
Bei der Interpretation von Leerzeichen
waren andere
Sprachentwickler
deutlich kreativer!
-
Funktionsaufruf ohne Klammern
In der Mathematik ist man sich nicht so ganz einig,
ob man bei der Auswertung einer Funktion das Argument generell klammern soll oder nicht.
So schreibt man meistens "f(2)" aber "sin 0" und
in der Funktionalanalysis "D f" wenn "D" ein Operator ist.
In sehr vielen Programmiersprachen sind Klammern um die Argumente Pflicht.
Mit ihnen wird auch unterschieden,
ob eine Funktion nur benannt oder auch aufgerufen werden soll.
Bei maschinennahen Sprachen gibt es hier einen handfesten
Unterschied.
Im BASIC des Sinclair-ZX-Spectrums
(auch die Ahnen sollen gebührend erwähnt werden)
zum Beispiel sind sie nicht Pflicht,
ebenso in funktionalen Sprachen wie LISP und Haskell.
Auch in MatLab ist es möglich,
Funktionen ohne Klammern aufzurufen.
Man darf
save '/tmp/schach'
schreiben, aber auch
save('/tmp/schach')
.
Diese Schreibweisen sind im wesentlichen gleichwertig.
Dass es beide gibt, ist ein Zugeständnis an manche Programmierergewohnheiten.
So ist es auch nicht mit Logik zu erklären,
dass manche Funktionen bevorzugt auf die eine
und andere Funktionen bevorzugt auf die andere Weise aufgerufen werden.
Aber ach, was hat denn das zu bedeuten
>> sin 0
ans =
-0.7683
(entdeckt in Version 6.5.1.199709, Release 13 (Service Pack 1), 2003-0804) ?
Kleiner Tip es gilt auch
>> sin '0'
ans =
-0.7683
Dämmert's?
In der Version 7.0.1.24704, Release 14, 2004-09-13
gibt es wenigstens eine Fehlermeldung:
??? Function 'sin' is not defined for values of class 'char'.
Error in ==> sin at 12
builtin('sin', varargin{:});
Jetzt alles klar?
"0" wird offensichtlich nicht als Zahl verstanden, sondern als Text.
Ein Text ist für MatLab ein Feld von Zeichen.
Also rechnet MatLab 6.5.1 den Sinus der Nummer des Zeichens "0"
in der ASCII-Zeichentabelle aus.
Jeder der sich mit Naturkonstanten auskennt, weiß, dass "0" die Nummer 48 trägt.
Für "sin 0" ist "sin(48)" sicher die sinnvollste Interpretation,
die man sich überhaupt vorstellen kann.
Wozu das gut ist? Damit man statt
save '/tmp/schach'
auch
save /tmp/schach
schreiben kann,
wird bei der klammerlosen Schreibweise ein Ausdruck ohne Anführungszeichen wie ein Text gewertet.
Das Analogon
save(/tmp/schach)
wird dagegen zurückgewiesen.
Merke:
Texte und Zahlen - warum soll man dazwischen unterscheiden?
Es hängt doch ohnehin alles miteinander zusammen.
-
Polymorphe Funktionen
Es gibt in MatLab nicht wirklich Deklarationen,
insbesondere sind auch die Funktionsdeklarationen
nur rudimentär entwickelt.
Zum Beispiel leitet
function z=f(x,y)
eine Funktionsdefinition ein,
die von höchstens zwei Variablen abhängt.
Es können auch weniger Variablen sein und
die Typen sind völlig beliebig.
Alles wird erst zur Laufzeit geprüft,
führt dann zu den beliebten Verwechslungen
von Skalaren und Matrizen oder
zu unverständlichen Fehlermeldungen.
Das alles nur zu dem Zweck,
dass die Funktion
je nach Anzahl und Art der Argumente und
je nach Anzahl der Rückgabewerte
völlig verschiedene Operationen ausführen kann.
Um sich einen unauffindbaren Fehler einzuhandeln,
reicht es völlig aus,
ein Argument bei der Übergabe zu vergessen
oder zu wenig Rückgabevariablen anzugeben.
Ganz so kaputt hat man es noch nicht einmal in C++ geschafft.
Dort erfüllt das Überladen von Bezeichnern weit gehend diesen Zweck.
Merke:
Man muss sich einiges einfallen lassen,
um den Vorrat an dreibuchstabigen Funktionsbezeichnern
nicht zu schnell zu erschöpfen.
-
Mehrere Rückgabewerte
In vielen Sprachen gibt es eine Unsymmetrie bei Funktionen:
Zwar können beliebig viele Parameter vereinbart werden,
aber immer nur ein Rückgabewert.
Dass nur ein Rückgabewert erlaubt ist,
liegt daran, dass man das Ergebnis einer Funktion
oft direkt einer anderen Funktion übergeben möchte.
Um die Symmetrie zu erhalten,
sollte eine Funktion also immer genau einen Parameter
und ein Ergebnis besitzen.
Da sowohl Parameter als auch Ergebnis Datenverbünde sein dürfen,
kann man auf diesem Wege mehrere Werte eingeben und wieder herausbekommen.
Dieses Konzept ist zum Beispiel in
Haskell
verwirklicht.
Nun sprechen wir aber über MatLab.
Da gibt es zwar auch Datenverbünde,
aber diese werden in der Regel nicht für mehrere Rückgabewerte verwendet.
Sicher aus gutem Grund, denn bei den dynamisch angelegten Datenverbünden
weiß der Leser des Programmes nicht,
welche Elemente der Datenverbund enthalten mag.
Daher gibt es in MatLab noch die Möglichkeit,
ausdrücklich mehrere Ergebnisse zu vereinbaren.
Der Kopf einer solchen Funktion sieht beispielsweise so aus
function [L, U] = LU(A)
und die Funktion wird ganz analog aufgerufen
[L, U] = LU(A)
.
Aus der Vektorschreibweise [L, U]
folgert der versierte MatLab-Anwender messerscharf,
dass es sich hier nicht um einen Vektor
(also eine 1x2-Matrix) handelt
sondern um etwas ganz anderes.
Dieses ominöse andere ist so beschaffen,
dass es Objekte ganz verschiedenen Typs enthalten kann,
sich also ähnlich wie ein Datenverbund verhält.
Ein genauerer Blick verdeutlicht aber,
dass dieses ominöse Dingens nur ein Phantom ist,
es lässt sich nicht als ganzes fassen!
Schreibt man
X = LU(A)
dann bekommt X im besten Fall den Wert von L
und in dem Ausdruck LU(A) * B
besitzt das LU(A) ebenfalls nur den Wert von L.
Noch schlimmer: Die aufgerufene Funktion "sieht"
wie viele ihrer Ergebnisse verwendet werden
und kann darauf reagieren.
Es ist also durchaus denkbar, dass nach
L = LU(A)
L den Wert A * A besitzt und nach
[L, U] = LU(A)
die linke Dreiecksmatrix der LR-Zerlegung von A.
Merke:
Falls ein Programm ohne Fehlermeldung läuft
aber nicht tut was es soll,
durchkämme einfach alle Funktionsaufrufe
und schaue ob du irgendwo noch ein unnützes Argument übergeben
oder ein überflüssiges Ergebnis einholen musst,
damit die aufgerufene Funktion den richtigen Algorithmus anstößt!
-
Kommaoperator
Jeder der den
Kommaoperator
von C schätzen gelernt hat,
wird sich fragen,
ob es vergleichbares auch in MatLab gibt.
Ja, aber natürlich:
pow=1;
while pow>0,001
pow=pow/10;
end
Als unabhängiger Betrachter ahnt man schon,
was da nicht hinhaut.
Fakt ist aber, dass dies ein lauffähiges MatLab-Programm ist!
Eigentlich sollte es statt 0,001 natürlich 0.001 heißen,
und die Schleife sollte nach drei Durchläufen mit pow = 0.001 abbrechen.
So wie der Vergleich da steht, bedeutet er aber:
"Vergleiche pow mit 0 und gib 001 auf der Konsole aus".
Die Schleife läuft bis zum Ende des darstellbaren Zahlenbereiches
und hört bei pow = 0 auf.
Merke:
Manche Effekte sind so atemberaubend,
dass ich unmöglich von alleine darauf kommen kann.
Obiges Beispiel wurde mir als Lösung einer Übungsaufgabe abgegeben.
Standardbibliotheken
-
Verdrehte Polynome
Felder zur Darstellung endlichdimensionaler Vektoren, Matrizen, Tensoren
sind eine wirklich universell verwendbare Datenstruktur.
Polynome sind bezüglich Addition und Skalierung auch Vektoren
und sie lassen sich einfach mit Feldern implementieren,
in dem man die Polynomkoeffizienten in einem Feld ablegt.
Nur wie macht man das in MatLab vernünftig?
Die Indizierung beginnt schließlich bei 1.
Ganz klar:
Die Polynomfunktionen polyder, polyint, polyval
nehmen einfach den Feldeintrag mit Index 1 als höchsten Koeffizienten,
Index 2 für den zweithöchsten Koeffizient, usw.
Das Absolutglied ist das absolut letzte!
Das mag der Lesegewohnheit mancher Anwender entgegenkommen,
ist zum Programmieren aber total hirnrissig.
Merke:
Wenn du nur eine unbefriedigende Möglichkeit für die Indizierung hast,
dann nimm wenigstens eine völlig unbefriedigende.
-
Polynome polymorph
Schön wenn die Automatiken in MatLab perfekt ineinander greifen,
um absolut sehenswerte Resultate zu erzielen.
-
Erwähnte ich schon, dass MatLab Skalare notfalls zu Vektoren expandiert,
falls etwas nicht auf Anhieb klappt?
Will man zum Beispiel ein Skalar zu einem Vektor addieren,
was nun gar keine sinnvolle Interpretation zulässt,
dann wandelt MatLab den skalaren Wert in einen Vektor passender Länge
gefüllt mit diesem Wert um und
addiert diesen zu dem zweiten Vektor.
-
Die Funktion polyder leitet ein Polynom ab.
Man könnte erwarten, dass der Koeffizientenvektor dadurch
eine Komponente kürzer wird oder aber gleich lang bleibt.
Irgendwas vorhersehbares eben.
Beides ist nicht der Fall:
Der Vektor wird so gekürzt, dass der führende Koeffizient nicht null ist.
Diese beiden Automatiken zusammen ergeben den netten Effekt,
dass die Ableitung bezüglich der Addition nicht mehr distributiv ist.
Boah. Beispiel gefällig?
Die beiden Ausdrücke
polyder([1 0 0 0 0]) + polyder([0 0 0 1 0]) und
polyder([1 0 0 0 0] + [0 0 0 1 0])
liefern nicht das gleiche Ergebnis.
(Eine Funktion, die Vektoren als ganzes vergleicht und nicht elementweise,
konnte ich mit Hilfe der MatLab-Dokumentation gar nicht finden ...)
Merke:
Weniger ist manchmal mehr.
-
Gemeine Mittel
Stellt euch folgende Situation vor:
Wir haben ein RGB-Bild und wollen daraus die Bildpunkte extrahieren,
die zu einem bestimmten dargestellten Objekt gehören.
Vielleicht ist auf dem Bild eine Vase zu sehen
und es sind 1000 Punkte, die das Abbild der Vase formen.
Jetzt wollen wir die Farbe der Vase bestimmen.
Das ist nicht so einfach,
weil jeder Bildpunkt der Vase eine etwas andere Farbe besitzt.
Wir könnten eine mittlere Farbe bestimmen,
indem wir die Helligkeitswerte für jede Farbkomponente mitteln.
Dazu tragen wir alle Helligkeitswerte in eine 1000×3-Matrix ein
und rufen die Standardfunktion mean auf.
Ich demonstriere das mal an einer Vase, die 5 Bildpunkte groß ist:
>> A=[98 48 0; 99 49 0; 100 50 0; 101 51 0; 102 52 0]
A =
98 48 0
99 49 0
100 50 0
101 51 0
102 52 0
>> mean(A)
ans =
100 50 0
Soweit ist alles in Ordnung.
Allerdings heißt mean nicht nur "Durchschnitt", sondern auch "fies".
Das bekommen wir zu spüren,
wenn ein Objekt nur aus einem Bildpunkt besteht.
Dann erhalten wir nämlich
>> A=[100 50 0]
A =
100 50 0
>> mean(A)
ans =
50
weil jetzt die einzeilige Matrix als dreidimensionaler Vektor angesehen wird.
Das Ergebnis, die skalare 50, wird,
wir wissen es schon aus dem vorangegangenen Beispiel,
in nachfolgenden Operationen auch gerne
wieder auf einen dreidimensionalen Vektor aufgeblasen,
so dass man den Fehler lange nicht bemerkt.
Will man den Fehler vermeiden, muss man mean(A,1) schreiben,
um die Funktion darauf hinzuweisen,
dass man wirklich in jedem Fall die Durchschnittswerte der Spalten haben will.
Auch diesen Tip steuerte Johannes Engels bei,
der sich mit der Suche nach diesem Problem die Zeit vertrieben hat.
-
LogSpace - Raum für Logik?
Ohne weitere Erklärung sei folgendes vermerkt:
>> help logspace
LOGSPACE Logarithmically spaced vector.
LOGSPACE(d1, d2) generates a row vector of 50 logarithmically
equally spaced points between decades 10^d1 and 10^d2. If d2
is pi, then the points are between 10^d1 and pi.
LOGSPACE(d1, d2, N) generates N points.
See also LINSPACE, :.
Die Konkurrenten
RLab,
Octave und
SciLab
haben die Ausnahmeregelung für pi
übrigens der Kompatibilität zu MatLab wegen übernommen.
Merke:
Dass eine Funktion in 99.99% aller Fälle das Erwartete tut, reicht völlig aus.
-
Grafikausgabe: drawnow
Jeder, der das erste Mal aus einem MatLab-Programm heraus
Funktionen grafisch veranschaulichen will,
wird sich darüber wundern,
dass die Grafik nicht sofort ausgegeben wird,
sondern erst irgendwann, wenn MatLab Lust dazu hat.
Experten wissen, dass MatLab mit dem Befehl drawnow
zur Ausgabe gezwungen werden kann.
Die Grafikausgabe zu verzögern, ist zwar ein wirksames Mittel
um häufiges Auffrischen der Darstellung zu verhindern,
deutlich sauberer wäre aber die Technik gewesen,
mit zwei Funktionsaufrufen, wie
lockdrawing(figure) und
unlockdrawing(figure),
die Grafikausgabe vorübergehend zu unterdrücken.
Eine Schachtelung dieser Aufrufe müsste auch erlaubt sein.
So könnte man den Grafikaufbau selbst dann gut steuern,
wenn man viele Routinen aufruft, die ihrerseits zur Grafik beitragen
und Grafikausgaben bündeln wollen.
-
Grafikausgabe: hold
Manchmal sollen mehrere Funktionskurven übereinandergelegt werden.
Auch dafür bietet sich eine Technik wie die obige an.
Wiederum legt MatLab eine unerwartet atemberaubende Lösung vor.
Der Befehl hold on, der das Bündeln der Kurven einleitet
(also im wesentlichen das Löschen der vorherigen Kurve unterdrückt)
muss nach der ersten Grafikanweisung stehen!
Wenigstens steht hold off korrekt nach der letzten Grafikausgabe.
Mit diesem raffinierten Kunstgriff ist auch hier eine Schachtelung unterbunden.
Daher bereitet das Umordnen oder Ergänzen solcher Grafikanweisungen viel Freude,
denn das hold on muss immer
exakt nach der ersten Grafikanweisung stehen.
Aktualisierung:
Ab MatLab R2011b, evtl. auch früher, darf man hold on
auch vor die erste plot-Anweisung setzen.
-
Bildverarbeitung und Zahlentypen
MatLab wird gerne zur Signal- und Bildverarbeitung verwendet,
was sicher an den erhältlichen Bibliotheken und
den integrierten Matrizenmanipulationen liegt.
Mit der genialen Image-Toolbox
kann man solch unschuldige Programme wie das folgende schreiben.
Dieses kopiert ein Graustufenbild namens test.tif.
Natürlich kopiert man so keine Bilder.
Also stellt euch vor, ich würde das Bild nicht nur kopieren,
sondern auch noch irgendwie bearbeiten.
x = imread('test.tif');
y = zeros(size(x)); % reserviere Speicher für neues Bild
for i = 1:size(x,1)
for j = 1:size(x,2)
y(i,j) = x(i,j);
end
end
imwrite(x, 'toast0.tif','tif');
imwrite(y, 'toast1.tif','tif');
Man sollte jetzt erwarten,
dass test.tif, toast0.tif, toast1.tif
in etwa die gleichen Bilder enthalten.
Dem ist nicht so.
Tatsächlich enthalten test.tif und toast0.tif
das gleiche Bild,
wohingegen toast1.tif komplett weiß ist.
Interessant.
Man fragt sich erstens, warum das nicht funktioniert
und zweitens, warum MatLab keinen Fehler ausgibt.
Auch dieses Phänonem beruht auf raffiniert gestrickten Automatismen,
die im Zusammenspiel viel weniger ergeben
als die Summe der einzelnen Bestandteile.
Oder so ähnlich.
Das Problem ist, dass imread gerne ein Bild
als Matrix von 8-Bit-Ganzzahlen speichert.
Das ist ungewöhnlich,
weil MatLab sonst lieber komplexwertige Gleitkommawerte in die Hand nimmt.
Mit zeros legt man denn auch eine Matrix mit solchen Werten an.
Bei einer statisch typisierten Sprache
würde die Zuweisung y(i,j) = x(i,j); abgelehnt werden,
und zwar noch bevor das Programm gestartet werden kann.
Dann würde sich der Benutzer des Problems bewusst werden
und müsste eine Konvertierung einbauen.
MatLab denkt sich aber,
dass es doch nun keinen großen Unterschied
zwischen einer Gleitkommazahl und einer ganzen Zahl gibt
und konvertiert die Zahl eigenmächtig.
Die Image-Toolbox unterscheidet aber sehr wohl
zwischen Ganzzahl- und Gleitkommamatrizen.
Für erstere werden die Grauabstufungen mit dem Bereich 0-255 kodiert,
für zweitere gilt der Bereich 0-1.
Wird etwa der ganzzahlige Wert 111 eingelesen,
so wird die abzuspeichernde Gleitkomma-111
als zu groß geratene Zahl betrachtet,
die eigentlich höchstens 1 sein dürfte.
Also wird sie stillschweigend auf 1 zurückgesetzt
und das bedeutet weiß.
Eine "Lösung" in MatLab-Manier für das Problem besteht übrigens darin,
das Bild vor weitergehenden Berechnungen mit im2double
in Gleitkommazahlen zu konvertieren.
Nach dieser langen Auflistung von Problemen
muss man als Leser den Eindruck haben,
dass MatLab rundherum schlecht
und sein Geld nicht wert sei.
(... und MatLab kostet viel Geld!
Mit Toolboxen und allem drum und dran
erleichtert MathWorks den Käufer gerne um mehrere tausend Euro.)
Dieser Eindruck stimmt nicht,
denn ich habe natürlich vor allem die Sachen zusammengetragen,
die mich immer wieder genervt haben.
Zum Schluss will ich daher wenigstens eine gute Eigenschaft von MatLab hervorheben:
Wenn man an einer Einrichtung arbeitet,
die über MatLab-Lizenzen verfügt,
so kann es leicht passieren, dass alle vorhandenen Lizenzen
für MatLab oder eine seiner Toolboxen
leider schon von den Kollegen blockiert sind.
Die Chancen stehen also gut,
dass man sich aufgrund von Mangel an Lizenzen
gar nicht mit MatLab herumzuärgern braucht!
Erstellt: | 2002.05 Henning Thielemann |
Mit HSC verarbeitet seit: | 2002-05-29 |
Zuletzt geändert: | 2017-05-29, 11:22 |
Meinung des HTML-Prüfers: |
|
Seitenstatistiken: |
|
|
|
Überwachungszustand: | |
|
|
Hier noch ein paar schwachsinnige E-Mail-Adressen zur Fütterung von SPAM-Adresssammlern.
Die Adressen sind mit Hilfe von Markov-Ketten zufällig aus realen Namen
zusammengewürfelt
und existieren mit sehr großer Wahrscheinlichkeit nicht.
|