Primitive Datentypen in Haskell

Werbung
Primitive Datentypen in Haskell
Der Datentyp Bool
Der Datentyp Bool dient zur Beschreibung von Wahrheitswerten und zum Rechnen mit
solchen Werten. Bool hat nur zwei mögliche Werte True und False. Als Operationen
sind die Negation not sowie Konjunktion && und Disjunktion || vordefiniert, wobei
letztere als echte zweistellige Operationen in Infix-Notation anzuwenden sind oder alternativ als Funktionen (||) , (&&) :: Bool -> Bool -> Bool verwendet werden
können.
Wie in der Aussagenlogik bindet die Konjunktion stärker als die Disjunktion, d.h. im
Ausdruck (x && y) || z kann man auf die Klammern verzichten aber der Ausdruck
(x || y) && z würde ohne Klammern eine andere Bedeutung erhalten.
Der Datentyp Bool tritt bei der Auswertung von Bedingungen auf, vor allem als Rückgabetyp von Vergleichsoperationen wie z.B. i <= n oder k == n auf.
Der Datentyp Int
Der Datentyp Int ist der Standardtyp zur Darstellung von (positiven und negativen)
ganzen Zahlen. Darstellbar sind alle Zahlen aus dem Bereich [−231 , 231 −1]. Für Anwendungen, bei denen dieser Bereich überschritten werden könnte, sollte man den Datentyp
Integer verwenden. Da einige Effekte, die bei der Verwendung von Int auftreten, nur
durch genauere Kenntnisse der internen Darstellung von ganzen Zahlen zu verstehen
sind, stellen wir dieses Thema an den Anfang unserer Betrachtungen.
Darstellung negativer Zahlen
Wie wir bereits wissen, kann man mit n Bits jede natürliche Zahl aus dem Bereich
von 0 bis 2n − 1 darstellen. Zur Darstellung negativer Zahlen wird ein zusätzliches Bit
verwendet, das man üblicherweise wie ein Minuszeichen an den Anfang der Darstellung
stellt. Bei insgesamt n-Bits bleiben also noch n − 1 Bits für die Darstellung positiver
Zahlen womit der Bereich von 0 bis 2n−1 −1 abgedeckt werden kann. Für die Darstellung
einer negativen Zahl z kann man die folgenden Ideen verwenden:
• Naiver Ansatz: Mit einer 1 im Vorzeichenbit wird ein Minus codiert, die verbleibenden Bits dienen zur Codierung des Betrags von z.
• 1–Komplement: Man codiert zuerst die positive Zahl −z = |z| mit einer Null
im Vorzeichenbit und bildet dann das Komplement dieser Darstellung, d.h. alle
Bits b werden durch ihr Gegenteil (Komplement) b ersetzt, insbesondere wird das
Vorzeichenbit zur 1.
• 2–Komplement: Das Vorzeichenbit steht für −2n−1 und alle anderen Bits haben
die übliche Bedeutung wie bei der Darstellung einer positiven Zahl, d.h. eine Folge
von n Bits bn−1 bn−2 . . . b1 b0 ist die Darstellung der Zahl
z = −bn−1 2n−1 + bn−2 2n−2 + . . . + b1 2 + b0
Obwohl die dritte Variante zunächst als ein unnötig komplizierter Ansatz erscheint,
wird sie sehr häufig verwendet, insbesondere liefert sie die Darstellung für den HaskellDatentyp Int. Genauer gesagt werden in Haskell Zahlen des Typs Int mit 32 Bits
dargestellt und daraus ergibt sich [−231 , 231 − 1] als darstellbarer Bereich. Bei der Suche
nach Gründen, die gegen die ersten zwei Varianten sprechen, fällt zuerst auf, dass die
Zahl 0 keine eindeutige Darstellung hat. Das 2–Komplement hat aber einen weiteren
Vorteil, der erst bei genauerer Betrachtung der Addition zu Tage tritt.
Berechnung der 2–Komplement–Darstellung von negativen Zahlen
Wir beschäftigen uns zuerst mit der Frage, wie man aus der Darstellung einer positiven
Zahl z die Darstellung von −z im 2–Komplement bestimmen kann. Man verfährt dazu
nach der folgenden Regel, die auch gleichzeitig eine Erklärung dafür gibt, warum diese
Darstellung als Komplement bezeichnet wird:
Bilde zuerst das 1–Komplement von z und addiere dazu eine 1.
Zuerst kann man sich leicht davon überzeugen, dass −0 nach dieser Regel die gleiche
Darstellung wie die 0 hat, denn das Komplement der Darstellung von 0 ist 11 . . . 11 und
durch Addition von 1 ergibt sich eigentlich 100 . . . 00, aber die führende 1 ist nur ein
Übertrag auf eine nicht vorhandene Stelle, d.h. sie fällt aus der Darstellung heraus und
übrig bleibt 00 . . . 00.
Die Dartellung von −1 ergibt sich aus 11 . . . 110 plus 1, also 11 . . . 111, und man kann
leicht nachrechen, dass das mit der Definition des 2–Komplements übereinstimmt.
Zur Überprüfung des allgemeinen Falles z ≥ 1 ignorieren wir zunächst das Vorzeichenbit
b31 sehen wir uns die Bitfolge b30 b29 . . . b1 b0 aus der Darstellung von z an. Sei u die durch
b30 b29 . . . b1 b0
dargestellte, positive Zahl. Da die Addition dieser beiden Bitfolgen die Folge 11 . . . 111
ergibt, gilt offensichtlich z + u = 231 − 1 und damit u + 1 = 231 − z. Addiert man also
nach obiger Regel zum 1–Komplement von z noch eine 1, wird mit den hinteren Stellen
die positive Zahl u + 1 repräsentiert und das Vorzeichnenbit b31 ist 1. Der Wert dieser
Darstellung im 2-Komplement ist somit
−231 + (u + 1) = −231 + (231 − z) = −z.
Addition von Zahlen in 2–Komplement–Darstellung
Der Vorteil des 2–Komplements liegt darin, dass man die übliche Additionsmethode auch
zur Addition eines positiven und eines negativen Summanden verwenden kann, während
man bei den anderen Darsetllungen Fallunterscheidungen für die auszuführenden Operationen machen muss. Beim 2–Komplement reicht eine Fallunterscheidung in der
Beweisfürung aus.
Fall 1: Angenommen a ≥ b ≥ 0 sind ganze Zahlen und es ist die Summe von a und −b
im 2–Komplement zu bestimmen. Dann ist in der Darstellung a31 a30 . . . a1 a0 von a das
Bit a31 = 0 und die restlichen Bits stellen die positive Zahl a dar. In der Darstellung
c31 c30 . . . c1 c0 von −b ist das Bit c31 = 1 und die restlichen Bits stellen die positive Zahl
c = 231 − b dar. Bei der Addition der hinteren 31 Bits entsteht wegen
a + c = a + (231 − b) = 231 + (a − b) ≥ 231
die Darstellung von a − b und eine 1 im Überlaufbit, das zu a31 + c31 addiert wird und
damit eine 0 im Vorzeichenbit erzeugt. Der Überlauf aus der Addition der Vorzeichenbits
wird ignoriert und das Ergebnis ist wie beabsichtigt die Zahl a − b.
Fall 2: Angenommen b > a ≥ 0 sind ganze Zahlen und es ist die Summe von a und −b
im 2–Komplement zu bestimmen. Dann ist in der Darstellung a31 a30 . . . a1 a0 von a das
Bit a31 = 0 und die restlichen Bits stellen die positive Zahl a dar. In der Darstellung
c31 c30 . . . c1 c0 von −b ist das Bit c31 = 1 und die restlichen Bits stellen die positive Zahl
231 − b dar. Bei der Addition der hinteren 31 Bits entsteht wegen
a + c = a + (231 − b) = 231 + (a − b) < 231
die Darstellung von 231 + (a − b) und eine 0 im Überlaufbit, das zu a31 + c31 addiert
wird und damit eine 1 im Vorzeichenbit erzeugt. Der Wert des Ergebnisses ist wie
beabsichtigt die Zahl −231 + (231 + (a − b)) = a − b.
Durch weitere Fallunterscheidungen kann man sich auch davon überzeugen, das Additionen von zwei positiven bzw. von zwei negativen Zahlen korrekt ausgeführt werden,
solange das Ergebnis im darstellbaren Bereich liegt.
Achtung: Liegt das Ergebnis einer Additionen von zwei positiven Zahlen nicht mehr
im darstellbaren Bereich, so entsteht eine 1 im Vorzeichenbit und damit ein negatives
Ergebnis. Ähnlich führt die Addition von zwei negativen Zahlen bei Überschreitung des
darstellbaren Bereichs zu einem positiven Ergebnis. Wenn man diese unerwünschten
Effekte vermeiden will, muss man zum Datentyp Integer wechseln, bezahlt das aber
durch Laufzeitverluste.
Die Funktionen div und mod
Für die ganzzahlige Division sind die Funktionen div, mod :: Int -> Int -> Int
vordefiniert. Im Gegensatz zur Programmiersprache Java stimmt die Haskell-Implementierung dieser Funktionen mit der mathematischen Definition überein, d.h. sie realisieren
die Ausaage des folgenden Satzes.
Satz: Für beliebige Zahlen n, d ∈ Z mit d > 0 existieren eindeutig bestimmte Zahlen
q, r ∈ Z, so dass folgende Bedingungen erfüllt sind
n=q·d+r
und
r ∈ {0, 1, . . . , d − 1}
Man nennt q den ganzzahligen Quotienten und r den Rest aus n und d. Der Aufruf
div n d berechnet q und der Aufruf mod n d berechnet r. In mathematischen Texten
verwendet man b nd c für q und n mod d für r.
Welchen Wert mod für negative Werte von n liefern sollte, kann man sich am Beispiel
n = −17 und d = 5 überlegen. Man beginnt mit dem positiven Fall |n| = 17, also
17 = 3·5+2 und multipliziert beide Seiten mit −1. In der Gleichung −17 = (−3)·5+(−2)
ist der Rest aber nicht aus der zuläsigen Menge {0, 1, 2, 3, 4}. Man korrigiert das durch
Addition und Subtraktion einer 5, wobei die subtrahierte 5 dem Produkt zugeschlagen
wird:
−17 = (−3) · 5 + (−2) + 5 − 5 = (−4) · 5 + 3 folglich ist q = −4 und r = 3
.
Der Datentyp Float
Der Datentyp Float ist der Standardtyp zur Darstellung von rellen Zahlen. Natürlich
kann man mit einer beschränktem Anzahl von Bits nicht alle reellen Zahlen darstellen,
man kann nur eine beschränkte Genauigkeit erreichen und auch nicht beliebig große
Zahlen darstellen. Häufig gibt man die die Zahlen in der einfachen Notation mit einem
Dezimalpunkt an, wie z.B.:
0.432
-332.675
0.00967
Liegt der Dezimalpunkt aber sehr weit rechts (bei sehr großen Zahlen) oder stehen am
Anfang sehr viele Nullen (bei Zahlen mit sehr kleinem Betrag), bietet sich auch die
sogennante wissenschaftliche Notation an:
45.675e5 = 45.675 · 105 = 4 567 500
-1.564e6 = −1.564 · 106 = −1 564 000
23.67e-6 = 23.67 · 10−6 = 0.000 023 67
Die wichtigsten Standardfunktionen wie Quadratwurzel sqrt, Exponential- und Logarithmusfunktion exp, log, Winkelfunktionen sin, cos,tan und die Betragsfunktion
abs sind als Funktionen von Float nach Float implementiert. Dazu kommen die Funktionen floor, ceiling :: Float -> Int, die für einen gegeben Float-Wert r die
größte bzw. kleinste ganze Zahl ausgeben die ≤ r bzw. ≥ r ist.
Da die Operationen +, − und ∗ sowohl für den Int- als auch für den Float-Typ definiert
sind, muss der Interpreter durch Typanalyse feststellen, welcher Operationstyp zur
Ausführung kommt. Deshalb ist es auch nicht möglich, einen Int- und einen FloatWert zu addieren. Um dieses Problem zu umgehen, kann man vorher die Funktion
fromIntegral zur Typangleichung verwenden. Es ist zu beachten, dass die Eingabe
einer ganzen Zahl ohne Dezimalpunkt wie z.B. 2543 bei Bedarf auch als Float-Wert
interpretiert wird.
Beispiel:
3.5 + 4
3.5 + floor 4.5
3.5 + fromIntegral (floor 4.5)
7.5
ERROR - Unresolved overloading
7.5
Der Datentyp Char
Mit dem Datentyp Char werden Zeichen beschrieben, die man auf der Tastatur finden
kann oder die in einer normalen Textzeile angezeigt werden können. Haskell verwendet
zur internen Darstellung von Zeichen den Unicode, einen 16-Bit-Code. Wir werden
uns in der Regel aber mit einem sehr kleinen Teil dieses Codes begnügen, den ASCIIZeichen.
American Standard Code for Information Interchange (ASCII)
Dieser Code ist schon relativ alt und wurde in den frühen Zeiten der Datenfernübertragung
entwickelt. Es ist ein 7-Bit-Code bei dem ein achtes Bit als Prüfbit (Paritätsbit) verwendet wurde. Da die 128 darstellbaren Zeichen nicht ausreichten, um auch Umlaute oder
andere Sonderzeichen aus dem skandinavischen Sprachraum aufzunehmen, wurde später
das achte Bit zur Erweiterung auf 256 Zeichen verwendet, unter anderem entstand so
der Code Latin1. Da aber auch diese Erweiterungen nicht einmal für alle europäischen
Sprachen und schon gar nicht für chinesische und japanische Schriftzeichen ausreichten,
wurde mit dem sogenannte Unicode eine wesentlich größere Erweiterung entwickelt.
Unicode
Der Unicode wurde ursprünglich als 16-Bit-Code konzipiert mit dem man 65536 Zeichen
darstellen kann, aber bald zeigte sich, dass diese Zahl nicht für alle Schriftzeichen der
Welt verbreiteten Sprachen ausreicht. Deshalb entschloss man sich, diesen Bereich um
16 weitere Bereiche der gleichen Göße aufzustocken.
Da der ASCII-Standard schon weltweit verbreitet war, wurden die ersten 128 Zeichen
des Unicodes für die ASCII-Zeichen reserviert. Für die Arbeit mit dem Datentyp Char in
Haskell spielt die Dualität zwischen Zeichen und Zahl eine entscheidende Rolle: Jedes 16Bit-Tupel kann sowohl als Zeichen als auch als Binärzahl interpretiert werden. Hier sind
die wichtigsten Fakten, die man zu diesem Thema wissen sollte. Um die im Folgenden
besprochenen Funktionen verwenden zu können, muss (für alle neueren Hugs-Versionen)
das Modul Char.hs geladen werden. Das geschieht durch die Anweisung import Char
in der .hs–Datei, die diese Funktionen verwendet oder bei einfachen Testbeispielen auf
der Hugs–Kommandozeile durch :l Char.
1. Zur Übersetzung von Zeichen in Zahlen und von Zahlen in Zeichen gibt es die
Fuktionen
ord :: Char -> Int
chr ::
Int -> Char
2. Dadurch wird auch eine Ordnung auf Char definiert, die mit der lexikalischen Ordnung unseres Alphabet übereinstimmt, insbesondere bilden die Großbuchstaben
einen zusammenhängenden Abschnitt von 65 bis 90, ebenso wie die Kleinbuchstaben von 97 bis 122 und die Ziffern von 48 bis 57.
3. Um Zeichen, also Elemente von Char, von Namen für Variable oder Funktionen zu
unterscheiden, werden sie in Hochkommata eingefasst, wie z.B. ’a’, ’X’ oder ’3’.
Man kann Zeichen aber auch mit einem backslash und ihrer Nummer angeben, wie
z.B. ’\51’ für ’3’.
4. Man kann diese Umrechnungen auch nutzen, um einige der in Char.hs vordefinierten
Funktionen oder Abwandlungen davon selbst zu implementieren. Als Beispiel betrachten wir die Funktion toUpper :: Char -> Char mit der Kleinbuchstaben
in die entsprechenden Großbuchstaben umgewandelt werden:
offset :: Int -- Verschiebeabstand der Bloecke, ist ein Int-Wert
offset = ord ’A’ - ord ’a’ -- Wert kann jetzt verwendet werden
toUpper ch = chr (ord ch + offset)
5. In Char.hs sind auch schon die Ordnungsrelationen <, <=, >, >= vordefiniert,
man muss also nicht den Umweg über die Funktion ord gehen, um Zeichen vergleichen zu können.
Zum Beispiel kann man eine Funktion isCapital :: Char -> Bool für den
Test, ob ein Zeichen ein Großbuchstabe ist, wie folgt definieren:
isCapital ch = (’A’ <= ch) && (ch <= ’Z’)
Das ist doch etwas kürzer und einfacher als:
isCapital ch = (ord ’A’ <= ord ch) && (ord ch <= ord ’Z’)
Herunterladen