Als Angehöriger eines kleinen europäischen Volkes mit eigenwilliger Sprache frage ich mich bei jeder neuen Programmiersprache instinktiv, wie sie mit den ganzen ü, ö, ä und ß klarkommt. Python 2.x war in dieser Hinsicht keine Erleuchtung, aber Haskell funktioniert glücklicherweise wie Python 3: Strings werden intern als Unicode-Zeichenketten gespeichert.
Prelude> let a = "Übelkrähe" Prelude> a "\220belkr\228he" Prelude> putStrLn a Übelkrähe
Wunderbar, die Zeichen \220 und \228 werden bei der Ausgabe durch die Funktion putStrLn
automatisch in UTF-8 kodiert. Alles könnte so schön sein, wenn es sich bei Haskell-Strings nicht um linked lists von 32bit-Zeichen handelte. Diese Kombination hat zwei Nachteile: Strings benötigen mit 4 Bytes pro Zeichen relativ viel Platz (weshalb UTF-32 auch nur selten die beste Wahl ist), und linked lists sind bei einigen Listenoperationen viel langsamer als dynamic arrays.
Das ist den Haskell-Leuten auch schon aufgefallen. Die Lösung sind ausgerechnet die vermaledeiten Bytestrings:
Normal Haskell
String
types are linked lists of 32-bit characters. This has a number of useful properties like coverage of the Unicode space and laziness, however when it comes to dealing with bytewise data,String
involves a space-inflation of about 24x and a large reduction in speed. Bytestrings are packed arrays of bytes or 8-bit chars.
Packed arrays of bytes
klingt schon mal sehr viel schneller als 32bit-Zeichenketten. Trotzdem sind wir noch nicht ganz am Ziel. Das Modul Data.ByteString.Char8
erlaubt zwar, Zeichenoperationen an Bytestrings vorzunehmen, aber... sehen Sie selbst:
Prelude> import qualified Data.ByteString.Char8 as B Prelude Data.ByteString.Char8> let a = B.pack "Übelkrähe" Prelude Data.ByteString.Char8> a "\220belkr\228he" Prelude Data.ByteString.Char8> B.putStrLn a ?belkr?he
Anders als in Python 2.x enthält ein Wert vom Typ ByteString
nicht einen kodierten String (in dem \220 für den Buchstaben Ü stünde), sondern nur eine Kette von Bytes, deren Zeichenhaftigkeit dem Modul reichlich wurscht ist. Entsprechend kommt Data.ByteString.Char8.putStrLn
nicht auf die Idee, irgendetwas zu kodieren. Glücklicherweise gibt es ein zusätzliches Paket, utf8-string
, mit dessen Hilfe reguläre Strings in UTF-8-kodierte Bytestrings umgewandelt werden können:
Prelude> import qualified Data.ByteString.UTF8 as BU Prelude Data.ByteString.UTF8> import qualified Data.ByteString.Char8 as B Prelude Data.ByteString.UTF8 Data.ByteString.Char8> let a = BU.fromString "Übelkrähe" Prelude Data.ByteString.UTF8 Data.ByteString.Char8> a "\195\156belkr\195\164he" Prelude Data.ByteString.UTF8 Data.ByteString.Char8> B.putStrLn a Übelkrähe
Hier sehen wir auch zum ersten Mal, dass das Zeichen \220 (aus dem Zeichenbereich 000080 – 00009F) in UTF-8 mit zwei Bytes (\195\156) repräsentiert wird. Da der Bytestring nun bereits kodiert ist, liefert auch die unveränderte Ausgabe durch B.putStrLn
das gewünschte Ergebnis.
Trotzdem ist das noch nicht ganz befriedigend. Es müsste doch möglich sein, kodierte Bytestrings mit einem einzigen Modul zu verarbeiten – ohne den Zwischenschritt über die langsamen Haskell-Strings. Das ist es auch. Die Kombination der Spracherweiterung OverloadedStrings
mit dem Modul Data.Text
erlaubt folgendes Vorgehen:
Prelude> :set -XOverloadedStrings Prelude> import qualified Data.Text as T Prelude Data.Text> import qualified Data.Text.IO as T Prelude Data.Text Data.Text.IO> let a = "Übelkrähe"::T.Text Prelude Data.Text Data.Text.IO> a "\220belkr\228he" Prelude Data.Text Data.Text.IO> T.putStrLn a Übelkrähe Prelude Data.Text Data.Text.IO> T.putStrLn "Übelkrähe" Übelkrähe
Dank OverloadedStrings
können Zeichenketten innerhalb eines Programms explizit oder über type inference auf den Typ Text
bezogen werden, und T.putStrLn
übernimmt die UTF-8-Kodierung bei der Ausgabe. Die Behandlung eines Text
-Wertes unterscheidet sich dadurch auf den ersten Blick nicht von der eines regulären String
-Wertes. Intern allerdings handelt es sich nur bei ersteren um eine time and space-efficient implementation of Unicode text
, konkret: Um UTF-16-kodierte Bytestrings.
Einen exzellenten Überblick über das Verhältnis der Haskell-Typen zu den verschiedenen Unicode-Kodierungen und der Dichotomie linked list/packed array bietet Edward Z. Yang:
- ASCII or 8-bit:
- Packed and lazy:
Data.ByteString.Lazy.Char8
- Packed and strict:
Data.ByteString.Char8
,Data.CompactString.ASCII
orData.CompactString
with Latin1- Unicode:
- UTF-32, unpacked and lazy:
[Char]
(not a library, but if it was, this is where it would fall)- UTF-16:
- Packed and lazy:
Data.Text.Lazy
- Packed and strict:
Data.Text
orData.CompactString.UTF16
- UTF-8:
- Unpacked and lazy:
Codec.Binary.UTF8.Generic
contains generic operations that can be used to process [Word8].- Packed and lazy:
Data.ByteString.Lazy.UTF8
- Packed and strict:
Data.CompactString.UTF8
orData.ByteString.UTF8
- Compact (UTF-8-like), packed and strict:
Data.CompactString
with Compact
Theoretisch ist die Verarbeitung von ByteString
-Werten noch etwas effizienter als die von Text
-Werten – solange die verwendeten Zeichen sich größtenteils im ASCII-Bereich befinden. Praktisch ist der Geschwindigkeitsunterschied zu vernachlässigen, und konzeptuell ist Text eindeutig die adäquatere Lösung.