Haskell 文字列変換入門

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

## TL;DR

* [convertString](https://hackage.haskell.org/package/string-conversions-0.4.0.1/docs/Data-String-Conversions.html#v: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` よりもメモリ効率がよくヒープの断片化を防ぐ一方[^fragmentation]、`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.Lazy.fromStrict`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy.html#v:fromStrict)

* [`Data.ByteStrng.Lazy.toStrict`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy.html#v:toStrict)

* [`Data.ByteString.Lazy.Char8.fromStrict`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy-Char8.html#v:fromStrict)

* [`Data.ByteString.Lazy.Char8.toStrict`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy-Char8.html#v:toStrict)

* [`Data.Text.Lazy.fromStrict`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy.html#v:fromStrict)

* [`Data.Text.Lazy.toStrict`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy.html#v:toStrict)

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

### String との相互変換

#### Data.ByteString.Char8 との相互変換

* [`Data.ByteString.Char8.pack`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Char8.html#v:pack)
* [`Data.ByteString.Char8.unpack`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Char8.html#v:pack)
* [`Data.ByteString.Lazy.Char8.pack`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy-Char8.html#v:pack)
* [`Data.ByteString.Lazy.Char8.unpack`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Lazy-Char8.html#v:unpack)

`pack` と `unpack` で相互変換します。

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

```haskell
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"
```

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

```haskell
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`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:encodeString) で UCS4 から UTF-8 に変換して 6 文字の `String` にしておく必要があるのです。

また `unpack` で得られた UTF-8 の `String` は、そのままでは `String` として正しく扱えないため [`decodeString`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:decodeString) で UCS4 に戻す必要があります。

#### Data.Text との相互変換

* [`Data.Text.pack`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text.html#v:pack)
* [`Data.Text.unpack`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text.html#v:unpack)
* [`Data.Text.Lazy.pack`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy.html#v:pack)
* [`Data.Text.Lazy.unpack`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy.html#v:unpack)

同様に `pack` と `unpack` で相互変換します。

#### Data.ByteString との相互変換

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

まずは `Codec.Binary.UTF8.String` の [`encode`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:encode) と [`decode`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:decode) を用いて `[Word8]` を経由して変換する方法です。

```haskell
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` 経由で変換を行う方法です。

```haskell
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`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy-Encoding.html#v:encodeUtf8)、[`decodeUtf8`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy-Encoding.html#v:decodeUtf8) の代わりに [`encodeLatin1`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy-Encoding.html#v:encodeLatin1)、[`decodeLatin1`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Lazy-Encoding.html#v:decodeLatin1) を使用します。

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

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

例えば `String` が Latin-1 なら、[`stringUtf8`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Builder.html#v:stringUtf8) の代わりに  [`string8`](http://hackage.haskell.org/package/bytestring-0.10.6.0/docs/Data-ByteString-Builder.html#v:string8) を使用します。このモジュールは Builder なので `String` には戻せません。

#### Data.ByteString.UTF8 との相互変換

* [`Data.ByteString.UTF8.fromString`](https://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Data-ByteString-UTF8.html#v:fromString)
* [`Data.ByteString.UTF8.toString`](https://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Data-ByteString-UTF8.html#v:toString)
* [`Data.ByteString.Lazy.UTF8.fromString`](https://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Data-ByteString-Lazy-UTF8.html#v:fromString)
* [`Data.ByteString.Lazy.UTF8.toString`](https://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Data-ByteString-Lazy-UTF8.html#v:toString)

`fromString` と `toString` で相互変換します。

`fromString` と `toString` は内部的に [`encodeString`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:encodeString)、[`decodeString`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:decodeString) が実行されるため、いわゆる文字化けが発生することはありません。

```haskell
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]` 経由で行います。

```haskell
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`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:encodeString)、[`decodeString`](http://hackage.haskell.org/package/utf8-string-1.0.1.1/docs/Codec-Binary-UTF8-String.html#v:decodeString) が便利です。

あらためて使い方です。

```haskell
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`](https://hackage.haskell.org/package/iconv-0.4.1.3/docs/Codec-Text-IConv.html) の [`convert`](https://hackage.haskell.org/package/iconv-0.4.1.3/docs/Codec-Text-IConv.html#v:convert) を使用します。その名の通り、内部的には iconv が使用されています。

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

```haskell
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`](https://hackage.haskell.org/package/text-icu-0.7.0.1/docs/Data-Text-ICU-Convert.html) は任意のエンコーディングから Unicode に相互変換するモジュールです。内部的に [ICU](http://site.icu-project.org/) が利用されているため、変換能力としては最強クラスです。[Converter Explorer](http://demo.icu-project.org/icu-bin/convexp) を見ればその強力な変換能力がわかります。`Data.ByteString` を `Data.Text` に変換、または `Data.Text` を `Data.ByteString` に変換可能です。

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

```haskell
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`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Encoding.html) でも可能ですが、いったん `Data.Text` を経由するので、内容を編集する必要がないのであれば [`Codec.Text.IConv`](https://hackage.haskell.org/package/iconv-0.4.1.3/docs/Codec-Text-IConv.html) の方がおすすめです。

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

```haskell
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`](http://hackage.haskell.org/package/base-4.8.2.0/docs/GHC-IO-Encoding.html) というモジュールがありますが、これは Latin-1 と Unicode しか扱えないため、能力的には [`Data.Text.Encoding`](http://hackage.haskell.org/package/text-1.2.2.0/docs/Data-Text-Encoding.html) と同等です。

[GHC.IO.Encoding を使って文字コード変換](http://qiita.com/mr_konn/items/e2e3bae32b4e2220ddd9)というエントリが参考になります。

## 文字コード判定

### Codec.Text.Detect

[`Codec.Text.Detect`](http://hackage.haskell.org/package/charsetdetect-1.0/docs/Codec-Text-Detect.html) では以下のエンコーディングを判定可能です。

> 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`](http://hackage.haskell.org/package/charsetdetect-1.0/docs/Codec-Text-Detect.html#v:detectEncodingName) または [`detectEncoding`](http://hackage.haskell.org/package/charsetdetect-1.0/docs/Codec-Text-Detect.html#v:detectEncoding) で判定できます。

```haskell
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（欧文）と判定されてしまいました。

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

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

* [Haskellでの文字コードの扱い方が分かってきた](http://d.hatena.ne.jp/n314/20100624/1277366395)
* [Haskell Tips (文字列編)](http://bicycle1885.hatenablog.com/entry/2012/12/24/234707)

[^fragmentation]: GHC では `ByteString` は固定メモリが使用され、このメモリ領域は GC の対象にはなりません。大きな文字列を `ByteString` として確保するのは問題ないですが、小さな文字列を `ByteString` で確保するとヒープの断片化が発生します。