Learning Haskell: Unicode

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:

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.