Haskell 文字列変換入門

  • 36
    いいね
  • 3
    コメント

文字列処理といえば、プログラミング言語を学ぶ上で必須と言っても過言ではないでしょう。この記事では、Haskell のちょっとわかりづらい文字列変換について説明していこうと思います。

Haskell の文字列

Haskell には文字列がたくさんあります。

まずは String です。これは [Char] のシノニムであることはあまりにも有名です。リストなので扱いやすいのですが、リストであるがゆえパフォーマンスが出ないんです。

次は ByteString です。ここが混乱しやすいところです。ByteString は全く性質の異なる二種類があります。Data.ByteStringData.ByteString.Char8 です。誤解を恐れずに言ってしまうと、Data.ByteStringバイナリ で、Data.ByteString.Char8文字列 です。はい、String という名前がついていますが片方はバイナリなんです。でもライブラリによってはそのバイナリが文字列を表していたりすることがあるのでややこしいんです。さらに正格評価の Data.ByteString に対し、遅延評価の Data.ByteString.Lazy が、正格評価の Data.ByteString.Char8 に対し遅延評価の Data.ByteString.Lazy.Char8 が存在します。

そして Data.Text です。これは StringData.ByteString.Char8 のそれぞれの良さ、パフォーマンスと文字の扱いやすさを兼ね備えています。Data.Text も遅延評価を行う Data.Text.Lazy が存在します。もしあなたのプログラムで文字列操作をがっつり行う必要がある場合、この型に変換して操作すると良いです。

まだあります。

Data.ByteString.UTF8 とその遅延評価の Data.ByteString.Lazy.UTF8 です。これらの型はバイト列で効率よく UTF-8 を表現するための実装で、UTF-8 を適切に扱える Data.ByteString.Char8 のサブセットと考えて良いと思います。内部的には Data.ByteString を使用しています。

それぞれ用途があるので致し方ないのですが、それにしても多すぎますね! 特に ByteString という型は、Data.ByteStringData.ByteString.Char8Data.ByteString.LazyData.ByteString.Lazy.Char8Data.ByteString.UTF8Data.ByteString.Lazy.UTF8 のいずれかの可能性があります。

それぞれの文字列の特徴

それぞれの特徴は以下のようになります。

文字列 文字の扱い パフォーマンス 遅延評価版 備考
String Char(UCS4) 遅い - リスト
Data.ByteString Word8(Latin-1) すごく速い Data.ByteString.Lazy バイナリ(または Latin-1 だったり UTF-8 だったり)
Data.ByteString.Char8 Char8(UTF-8) すごく速い Data.ByteString.Lazy.Char8 マルチバイト文字が適切に扱えない
Data.Text Char(UCS4) 速い Data.Text.Lazy マルチバイト文字が適切に扱える
Data.ByteString.UTF8 Word8(UTF-8) 速い Data.ByteString.Lazy.UTF8 UTF-8 が適切に扱える

※ この記事内の UCS4 は、文脈によっては UTF-32 を表していることがあります。

変換方法

正格評価 -> 遅延評価、遅延評価 -> 正格評価

Data.ByteString.UTF8Data.ByteString.Lazy.UTF8 の相互変換を行う関数はありません。それ以外はすべて fromStricttoStrict で統一されています。

String との相互変換

Data.ByteString.Char8 との相互変換

packunpack で相互変換します。

ただし、このままだと日本語などのマルチバイト文字でいわゆる 文字化け してしまいます。マルチバイト文字を相互変換する場合は、UCS4 から UTF-8 へ、または UTF-8 から UCS4 に事前/事後に変換する必要があります。

Prelude> :m + Data.ByteString.Char8
Prelude Data.ByteString.Char8> :m + Codec.Binary.UTF8.String
Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> pack "ほげ"  -- ダメなパターン
"{R"
Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> pack $ encodeString "ほげ"  -- 良いなパターン
"\227\129\187\227\129\146"
Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> unpack $ pack $ encodeString "ほげ"  -- ダメなパターン
"\227\129\187\227\129\146"
Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> decodeString $ unpack $ pack $ encodeString "ほげ"  -- 良いなパターン
"\12411\12370"

何故このような変換が必要なのでしょうか。以下を見るとわかりやすいです。

Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> encodeString "ほげ"
"\227\129\187\227\129\146"
Prelude Data.ByteString.Char8 Codec.Binary.UTF8.String> decodeString $ encodeString "ほげ"
"\12411\12370"

String[Char] であり、Char は 4 バイトの UCS4 です。一方で Data.ByteString.Char8Stringpackunpack する際、1 文字を 1 バイトとして変換します。つまり pack "ほげ" なら本来 UTF-8 で 6 バイトになるはずなのに、2 バイトに変換されてしまいます。そのため、事前に encodeString で UCS4 から UTF-8 に変換して 6 文字の String にしておく必要があるのです。

また unpack で得られた UTF-8 の String は、そのままでは String として正しく扱えないため decodeString で UCS4 に戻す必要があります。

Data.Text との相互変換

同様に packunpack で相互変換します。

Data.ByteString との相互変換

ここが鬼門です。String から Data.ByteStringData.ByteString から String への相互変換を行う関数は存在しません。ただし、他の型を経由することで変換することは可能です。

まずは Codec.Binary.UTF8.Stringencodedecode を用いて [Word8] を経由して変換する方法です。

Prelude> :m + Codec.Binary.UTF8.String
Prelude Codec.Binary.UTF8.String> :m + Data.ByteString
Prelude Codec.Binary.UTF8.String Data.ByteString> pack $ encode "ほげ"
"\227\129\187\227\129\146"
Prelude Codec.Binary.UTF8.String Data.ByteString> decode $ unpack $ pack $ encode "ほげ"
"\12411\12370"

エンコーディングが UTF-8 で良いならこれが一番簡単です。

次に Data.Text.Encoding を使用して Data.Text 経由で変換を行う方法です。

Prelude> :m + Data.Text.Encoding
Prelude Data.Text.Encoding> :m + Data.Text
Prelude Data.Text.Encoding Data.Text> encodeUtf8 $ pack "ほげ"
"\227\129\187\227\129\146"
Prelude Data.Text.Encoding Data.Text> unpack $ decodeUtf8 $ encodeUtf8 $ pack "ほげ"
"\12411\12370"

例えば String が Latin-1 なら、encodeUtf8decodeUtf8 の代わりに encodeLatin1decodeLatin1 を使用します。

そして Data.ByteString.Builder を使用して変換を行う方法です。

Prelude> :m + Data.ByteString.Builder
Prelude Data.ByteString.Builder>toLazyByteString $ stringUtf8 "ほげ"
"\227\129\187\227\129\146"

例えば String が Latin-1 なら、stringUtf8 の代わりに string8 を使用します。このモジュールは Builder なので String には戻せません。

Data.ByteString.UTF8 との相互変換

fromStringtoString で相互変換します。

fromStringtoString は内部的に encodeStringdecodeString が実行されるため、いわゆる文字化けが発生することはありません。

Prelude> :m + Data.ByteString.UTF8
Prelude Data.ByteString.UTF8> fromString "ほげ"
"\227\129\187\227\129\146"
Prelude Data.ByteString.UTF8> toString $ fromString "ほげ"
"\12411\12370"

エンコーディング変換

エンコーディング変換も複雑です。

Codec.Binary.UTF8.String

既に紹介しましたが、String の UCS4 と UTF-8 の相互変換は、 encodeStringdecodeString が便利です。

あらためて使い方です。

Prelude> :m + Codec.Binary.UTF8.String
Prelude Codec.Binary.UTF8.String> encodeString "ほげ"
"\227\129\187\227\129\146"
Prelude Codec.Binary.UTF8.String> decodeString $ encodeString "ほげ"
"\12411\12370"

Codec.Text.IConv

Data.ByteString のエンコーディングを変換する場合は、Codec.Text.IConvconvert を使用します。その名の通り、内部的には iconv が使用されています。

使い方はこんな感じです。

Prelude> :m + Codec.Text.IConv
Prelude Codec.Text.IConv> :m + Data.ByteString.Builder
Prelude Codec.Text.IConv Data.ByteString.Builder> convert "UTF-8" "Shift_JIS" (toLazyByteString $ stringUtf8 "ほげ")
"\130\217\130\176"

Data.Text.ICU.Convert

Data.Text.ICU.Convert は任意のエンコーディングから Unicode に相互変換するモジュールです。内部的に ICU が利用されているため、変換能力としては最強クラスです。Converter Explorer を見ればその強力な変換能力がわかります。Data.ByteStringData.Text に変換、または Data.TextData.ByteString に変換可能です。

使い方はこんな感じです。

Prelude> :set -XOverloadedStrings
Prelude> :m + Data.Text.ICU.Convert
Prelude Data.Text.ICU.Convert> conv <- open "Shift_JIS" Nothing
Prelude Data.Text.ICU.Convert> fromUnicode conv "ほげ"
"\130\217\130\176"

Data.Text.Encoding

変換対象のエンコーディングが Latin-1、Unicode のいずれかであれば Data.Text.Encoding でも可能ですが、いったん Data.Text を経由するので、内容を編集する必要がないのであれば Codec.Text.IConv の方がおすすめです。

使い方はこんな感じです。

Prelude> :set -XOverloadedStrings
Prelude> :m + Data.Text.Encoding
Prelude Data.Text.Encoding> encodeUtf8 "ほげ"
"\227\129\187\227\129\146"
Prelude Data.Text.Encoding> decodeUtf8 $ encodeUtf8 "ほげ"
"\12411\12370"

GHC.IO.Encoding

GHC には GHC.IO.Encoding というモジュールがありますが、これは Latin-1 と Unicode しか扱えないため、能力的には Data.Text.Encoding と同等です。

GHC.IO.Encoding を使って文字コード変換というエントリが参考になります。

文字コード判定

Codec.Text.Detect

Codec.Text.Detect では以下のエンコーディングを判定可能です。

Big5
EUC-JP
EUC-KR
GB18030
gb18030
HZ-GB-2312
IBM855
IBM866
ISO-2022-CN
ISO-2022-JP
ISO-2022-KR
ISO-8859-2
ISO-8859-5
ISO-8859-7
ISO-8859-8
KOI8-R
Shift_JIS
TIS-620
UTF-8
UTF-16BE
UTF-16LE
UTF-32BE
UTF-32LE
windows-1250
windows-1251
windows-1252
windows-1253
windows-1255
x-euc-tw
X-ISO-10646-UCS-4-2143
X-ISO-10646-UCS-4-3412
x-mac-cyrillic

特徴は使い方が簡単であることです。detectEncodingName または detectEncoding で判定できます。

Prelude> :m + Codec.Text.Detect
Prelude Codec.Text.Detect> :m + Data.ByteString.Lazy
Prelude Codec.Text.Detect Data.ByteString.Lazy> detectEncoding $ pack [130, 217, 130, 176]
Just windows-1252

上記サンプルの結果で期待するエンコーディング名は Shift_JIS だったのですが、文字列が短すぎる("ほげ" の Shift_JIS バイト)からか windows-1252(欧文)と判定されてしまいました。

参考にさせてもらったサイト

どちらも大変参考になる良記事です。