文字列処理といえば、プログラミング言語を学ぶ上で必須と言っても過言ではないでしょう。この記事では、Haskell のちょっとわかりづらい文字列変換について説明していこうと思います。
TL;DR
- convertString という関数でかんたんに変換できます
- 型変換に関してはこの関数で大体カバーできます
- この関数でカバーされないケースや文字エンコーディングの変換が必要な場合は以下を参照ください
Haskell の文字列
Haskell には文字列がたくさんあります。
まずは String
です。これは [Char]
のシノニムであることはあまりにも有名です。リストなので扱いやすいのですが、リストであるがゆえパフォーマンスが出ないんです。
次は ByteString
です。ここが混乱しやすいところです。ByteString
は全く性質の異なる二種類があります。Data.ByteString
と Data.ByteString.Char8
です。誤解を恐れずに言ってしまうと、Data.ByteString
は バイナリ で、Data.ByteString.Char8
は 文字列 です。はい、String という名前がついていますが片方はバイナリなんです。でもライブラリによってはそのバイナリが文字列を表していたりすることがあるのでややこしいんです。さらに正格評価の Data.ByteString
に対し、遅延評価の Data.ByteString.Lazy
が、正格評価の Data.ByteString.Char8
に対し遅延評価の Data.ByteString.Lazy.Char8
が存在します。
そして Data.Text
です。これは String
と Data.ByteString.Char8
のそれぞれの良さ、パフォーマンスと文字の扱いやすさを兼ね備えています。Data.Text
も遅延評価を行う Data.Text.Lazy
が存在します。もしあなたのプログラムで文字列操作をがっつり行う必要がある場合、この型に変換して操作すると良いです。
まだあります。
Data.ByteString.UTF8
とその遅延評価の Data.ByteString.Lazy.UTF8
です。これらの型はバイト列で効率よく UTF-8 を表現するための実装で、UTF-8 を適切に扱える Data.ByteString.Char8
のサブセットと考えて良いと思います。内部的には Data.ByteString
を使用しています。
Data.ByteString.Short
というのもあります。これは Data.ByteString
よりもメモリ効率がよくヒープの断片化を防ぐ一方1、Data.ByteString
が提供するほとんどの関数を提供しません。また Data.ByteString
への変換はコピーを要し、その処理量は O(n) です。プログラム内部で多くの文字列を保持する目的で使用するのに適していますが、データのやり取りに使用するには適していません。
それぞれ用途があるので致し方ないのですが、それにしても多すぎますね! 特に ByteString
という型は、Data.ByteString
、Data.ByteString.Char8
、Data.ByteString.Lazy
、Data.ByteString.Lazy.Char8
、Data.ByteString.UTF8
、Data.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 が適切に扱える |
Data.ByteString.Short |
Word8 (Latin-1) |
N/A | N/A | 短い文字列保持用 |
※ この記事内の UCS4 は、文脈によっては UTF-32 を表していることがあります。
変換方法
正格評価 -> 遅延評価、遅延評価 -> 正格評価
Data.ByteString.UTF8
と Data.ByteString.Lazy.UTF8
の相互変換を行う関数はありません。それ以外はすべて fromStrict
と toStrict
で統一されています。
String との相互変換
Data.ByteString.Char8 との相互変換
Data.ByteString.Char8.pack
Data.ByteString.Char8.unpack
Data.ByteString.Lazy.Char8.pack
Data.ByteString.Lazy.Char8.unpack
pack
と unpack
で相互変換します。
ただし、このままだと日本語などのマルチバイト文字でいわゆる 文字化け してしまいます。マルチバイト文字を相互変換する場合は、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.Char8
は String
を pack
、unpack
する際、1 文字を 1 バイトとして変換します。つまり pack "ほげ"
なら本来 UTF-8 で 6 バイトになるはずなのに、2 バイトに変換されてしまいます。そのため、事前に encodeString
で UCS4 から UTF-8 に変換して 6 文字の String
にしておく必要があるのです。
また unpack
で得られた UTF-8 の String
は、そのままでは String
として正しく扱えないため decodeString
で UCS4 に戻す必要があります。
Data.Text との相互変換
同様に pack
と unpack
で相互変換します。
Data.ByteString との相互変換
ここが鬼門です。String
から Data.ByteString
、Data.ByteString
から String
への相互変換を行う関数は存在しません。ただし、他の型を経由することで変換することは可能です。
まずは Codec.Binary.UTF8.String
の encode
と decode
を用いて [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 なら、encodeUtf8
、decodeUtf8
の代わりに encodeLatin1
、decodeLatin1
を使用します。
そして 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 との相互変換
Data.ByteString.UTF8.fromString
Data.ByteString.UTF8.toString
Data.ByteString.Lazy.UTF8.fromString
Data.ByteString.Lazy.UTF8.toString
fromString
と toString
で相互変換します。
fromString
と toString
は内部的に encodeString
、decodeString
が実行されるため、いわゆる文字化けが発生することはありません。
Prelude> :m + Data.ByteString.UTF8
Prelude Data.ByteString.UTF8> fromString "ほげ"
"\227\129\187\227\129\146"
Prelude Data.ByteString.UTF8> toString $ fromString "ほげ"
"\12411\12370"
Data.ByteString.Short との相互変換
通常は行いませんが、Data.ByteString
または [Word8]
経由で可能です。前述のとおり Data.ByteString
は面倒なのでここでは [Word8]
経由で行います。
Prelude> :m + Codec.Binary.UTF8.String
Prelude Codec.Binary.UTF8.String> :m + Data.ByteString.Short
Prelude Codec.Binary.UTF8.String Data.ByteString.Short> pack $ encode "ほげ"
"\227\129\187\227\129\146"
Prelude Codec.Binary.UTF8.String Data.ByteString.Short> decode $ unpack $ pack $ encode "ほげ"
"\12411\12370"
エンコーディング変換
エンコーディング変換も複雑です。
Codec.Binary.UTF8.String
既に紹介しましたが、String
の UCS4 と UTF-8 の相互変換は、 encodeString
、decodeString
が便利です。
あらためて使い方です。
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.IConv
の convert
を使用します。その名の通り、内部的には 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.ByteString
を Data.Text
に変換、または Data.Text
を Data.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(欧文)と判定されてしまいました。
参考にさせてもらったサイト
どちらも大変参考になる良記事です。
-
GHC では
ByteString
は固定メモリが使用され、このメモリ領域は GC の対象にはなりません。大きな文字列をByteString
として確保するのは問題ないですが、小さな文字列をByteString
で確保するとヒープの断片化が発生します。 ↩