Haskellの文字列型：変換時の心構えと変換方法まとめ

[先の記事](https://qiita.com/mod_poppo/items/740659702f31216fdade)では、Haskellエコシステムに色々ある文字列型を分類しました。この記事では、色々ある文字列型を変換する方法を説明します。

# パッケージの依存関係：変換関数を探すにはどこを見れば良いか

各種文字列型や変換関数を提供するパッケージ間の依存関係は次のようになっています（依存元パッケージから依存先パッケージに矢印を伸ばしています）：

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185919/5a0b7750-92db-7b7e-0e9c-1e3abe675f70.png)

当たり前ですが、**変換関数が用意されているとしたら変換元の型と変換先の型の両方を参照できるパッケージです**。つまり、 `Text` と `ByteString` の変換関数を探したかったら bytestring パッケージではなく text パッケージを探すべきです。なぜなら、 bytestring パッケージの関数は `Text` 型には言及できないからです。

同様の理由で、 `ByteString` と `CStringLen` の変換関数を探したかったら bytestring パッケージを探すべきです。また、 `Text` を ByteString `Builder` に書き出す関数を探したかったら text パッケージを探すべきです。そして、 `ShortText` を他の型と変換する関数は全て text-short パッケージにあるはずです。

例外として、 `String` と `ByteString` を**UTF-8で**変換する関数は utf8-string パッケージにあります。

# Unicode文字列同士の変換

Unicode文字列同士の変換の場合は、エンコーディングや変換失敗時の挙動はあまり考える必要はありません。厳密に言えば `String` から `Text` 系への変換時はサロゲートコードポイントの扱いを考える必要がありますが、デフォルトの挙動で問題ないことが多いでしょう[^text-builder-fromString]。

[^text-builder-fromString]: と言いたかったのですが、この記事を書いている最中に `TB.fromString` の挙動を確認したところ、サロゲートコードポイントの扱いが不適切で `Text` の不変条件（正当なUTF-16である）を崩せてしまうことがわかりました。要バグ報告です。

ここでは、以下の5つの型（Unicode文字列型、あるいは文字列ビルダー型）の変換方法を図でまとめておきます。

* `String` (= `[Char]`)
* strict `Text`
* lazy `Text`
* `ShortText`
* Text `Builder`

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185919/e7a9adf4-27d1-9999-2d70-1bfc5f9b3841.png)

図の中でモジュール名は以下のように略しています。`Data.Text.Short` 以外は text パッケージで提供されています。

* `T`: [`Data.Text`](http://hackage.haskell.org/package/text/docs/Data-Text.html)
* `TL`: [`Data.Text.Lazy`](http://hackage.haskell.org/package/text/docs/Data-Text-Lazy.html)
* `TB`: [`Data.Text.Lazy.Builder`](http://hackage.haskell.org/package/text/docs/Data-Text-Lazy-Builder.html)
* `TS`: [`Data.Text.Short` (text-short パッケージ)](https://hackage.haskell.org/package/text-short/docs/Data-Text-Short.html)
    * `ShortText` はtextとは別のパッケージで定義されている関係で、 `ShortText` が絡む変換は全て `Data.Text.Short` で定義されています。

lazy `Text` は strict `Text` のリスト `[T.Text]` に近い構造で定義されているので、strict `Text` から lazy `Text` への変換（`TL.fromStrict`）は低コストで行えます。文字列型を `Builder` に埋め込む関数も、その段階ではコピーは行われないので低コストです。他の変換関数は多くの場合データのコピーを伴うので、長さに比例した時間がかかります。

Text `Builder` から直接出力できる文字列型は、 lazy `Text` のみです。strict `Text` が欲しい場合は、一旦 lazy `Text` を出力してから `toStrict` する必要があります。

`ShortText` と lazy `Text` を直接変換する関数は text-short パッケージからは提供されていないようです。

# バイト文字列同士の変換

バイト文字列同士の変換も、エンコーディングや失敗時の挙動は考える必要はありません。

ここでは、以下の5つの型の変換方法を図示します。

* `[Word8]`
* strict `ByteString`
* lazy `ByteString`
* `ShortByteString`
* ByteString `Builder`

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185919/c13d43b0-8518-b9a5-aa14-b8787aee7186.png)

図の中でモジュール名は以下のように略しています。いずれも bytestring パッケージで提供されています。

* `BS`: [`Data.ByteString`](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString.html)
* `BSL`: [`Data.ByteString.Lazy`](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Lazy.html)
* `BSB`: [`Data.ByteString.Builder`](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Builder.html)
* `BSS`: [`Data.ByteString.Short`](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Short.html)

lazy `ByteString` は strict `ByteString` のリスト `[BS.ByteString]` に近い構造で定義されているので、strict `ByteString` から lazy `ByteString` への変換（`BSL.fromStrict`）は低コストで行えます。文字列型を `Builder` に埋め込む関数も、その段階ではコピーは行われないので低コストです。他の変換関数はデータのコピーが伴うので、長さに比例した時間がかかります。

ByteString `Builder` から直接出力できる文字列型は、 lazy `ByteString` のみです。strict `ByteString` が欲しい場合は、一旦 lazy `ByteString` を出力してから `toStrict` する必要があります。ビルダーの代替物の中には直接 strict `ByteString` を出力できるものもあるようです（fast-builder 等）。

`ShortByteString` と lazy `ByteString` を直接変換する関数は bytestring パッケージからは提供されていないようです。

# Unicode文字列とバイト文字列の変換

Unicode文字列同士の変換、バイト文字列同士の変換は、効率を度外視すればどういう経路で変換してもほぼ同じ結果になります。

これに対し、Unicode文字列とバイト文字列を変換する場合はそうはいきません。「エンコーディング」と「エラー処理」を意識する必要があります。

## ASCIIまたは8ビット文字 (Latin-1)

まず、バイト文字列の文字コードをASCIIまたはLatin-1として扱う場合を考えます。この場合、バイト文字列型からUnicode文字列型への変換時にはエラーは起こりません。

一方で、Unicode文字列型からバイト文字列型に変換する際には、エラーまたは文字化けが起こる可能性があります。以下の図では、そういう「危険な」変換は破線で描きました。実際の挙動はドキュメントを参照して確かめてください。

![conv-char8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185919/618ae83f-24d5-af69-1bd6-1652ba3ccfab.png)

モジュール名の略称は以下の通りです。

* `BS.Char8`: [`Data.ByteString.Char8` (bytestring パッケージ)](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Char8.html)
* `BSL.Char8`: [`Data.ByteString.Lazy.Char8` (bytestring パッケージ)](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Lazy-Char8.html)
* `BSB`: [`Data.ByteString.Builder` (bytestring パッケージ)](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Builder.html)
* `TE`: [`Data.Text.Encoding` (text パッケージ)](http://hackage.haskell.org/package/text/docs/Data-Text-Encoding.html)
* `TLE`: [`Data.Text.Lazy.Encoding` (text パッケージ)](http://hackage.haskell.org/package/text/docs/Data-Text-Lazy-Encoding.html)

## UTF-8による変換

多くの方が興味のあるのは、UTF-8による変換でしょう。UTF-8によってUnicode文字列からバイト文字列へ変換する際は基本的にはエラーは発生しません[^surrogate-code-point-utf8]。

一方で、バイト文字列をUTF-8として解釈してUnicode文字列を得る場合、不正なバイト列をどうするかという問題があります。エラーを投げるのかU+FFFDに置換するのかはたまた `Maybe` で返すのか、実際の挙動はドキュメントを読んで確かめてください。以下の図ではそういう「失敗しうる」変換は破線で描きました。

[^surrogate-code-point-utf8]: 変換元が `String` の場合はサロゲートコードポイントをどうするかという問題があります。`BSB.stringUtf8` や utf8-string のソースを読んだ感じでは、サロゲートコードポイントの検査は行わずに、UTF-8としては不正なバイト列を出力するようになっているようです。

![conv-utf8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185919/55110eb6-55a1-a45c-a5bc-44ac0d1dfa33.png)

モジュール名の略称は以下の通りです。

* `BSB`: [`Data.ByteString.Builder` (bytestring パッケージ)](https://hackage.haskell.org/package/bytestring/docs/Data-ByteString-Builder.html)
* `BS.UTF8`: [`Data.ByteString.UTF8` (utf8-string パッケージ)](http://hackage.haskell.org/package/utf8-string/docs/Data-ByteString-UTF8.html)
* `BSL.UTF8`: [`Data.ByteString.Lazy.UTF8` (utf8-string パッケージ)](http://hackage.haskell.org/package/utf8-string/docs/Data-ByteString-Lazy-UTF8.html)
* `TE`: [`Data.Text.Encoding` (text パッケージ)](http://hackage.haskell.org/package/text/docs/Data-Text-Encoding.html)
* `TLE`: [`Data.Text.Lazy.Encoding` (text パッケージ)](http://hackage.haskell.org/package/text/docs/Data-Text-Lazy-Encoding.html)
* `TS`: [`Data.Text.Short` (text-short パッケージ)](https://hackage.haskell.org/package/text-short/docs/Data-Text-Short.html)
    * `ShortText` が関係する関数は、例によってここで提供されています。

内部的には `ShortText` は `ShortByteString` のnewtypeであり、Unicode文字列をUTF-8で保持しているので、 `ShortText` から `ShortByteString` への変換はゼロコストで行えます。逆の変換の際にはUTF-8としての正当性を検査する必要があるので、長さに比例した時間がかかります[^shortbytestring-to-shorttext]。

[^shortbytestring-to-shorttext]: 変換元が正当なUTF-8であるとわかっているのであれば検査を飛ばして `ShortByteString` から `ShortText` へゼロコストで変換することができます。詳しくは `Data.Text.Short.Unsafe` を参照してください。

# C文字列との変換

CとのFFIで使う、 `CStringLen` 等の変換についても触れておきます。先に、簡単なまとめを書いておきます。

* `CString`(`Len`) &harr; `String`
    * システムデフォルトのエンコーディングを使用：`Foreign.C.String.`(`peek`|`new`|`with`)`CString`(`Len`)
    * ASCII/8ビット文字専用：`Foreign.C.String.`(`peek`|`new`|`with`)`CAString`(`Len`)
    * 任意の `TextEncoding` を指定する版：`GHC.Foreign.`(`peek`|`new`|`with`)`CString`(`Len`)
* `CStringLen` &harr; `Text`
    * UTF-8：`Data.Text.Foreign.`(`peek`|`with`)`CStringLen`
* `CString`(`Len`) &harr; `ByteString`
    * バイト文字列同士：`Data.ByteString.`(`pack`|`useAs`)`CString`(`Len`)
* `CWString`(`Len`) &harr; `String`
    * Unicode文字列同士（UTF-16 or UTF-32）：`Foreign.C.String.`(`peek`|`new`|`with`)`CWString`(`Len`)

## ロケール依存な変換方法

[`Foreign.C.String`](https://hackage.haskell.org/package/base/docs/Foreign-C-String.html) には `String` と `CString`(`Len`) を変換するための関数が用意されています。

```haskell:Foreign.C.String
-- String から CStringLen
newCStringLen :: String -> IO CStringLen
withCStringLen :: String -> (CStringLen -> IO a) -> IO a

-- CStringLen から String
peekCStringLen :: CStringLen -> IO String

-- CString に対する（C文字列がNUL終端だと仮定する）変換関数もある。ドキュメント参照
```

これらの関数では、 `GHC.IO.Encoding` の `getForeignEncoding`/`setForeignEncoding` で指定されるエンコーディングが使われます。この初期値もシステム依存です。

エンコーディングを明示的に指定して変換したい場合は、 [`GHC.Foreign`](https://hackage.haskell.org/package/base/docs/GHC-Foreign.html) にある関数を使えば良いでしょう。これらの関数は `TextEncoding` 型でエンコーディングを指定できます。

```haskell:GHC.Foreign
-- String から CStringLen
newCStringLen :: TextEncoding -> String -> IO CStringLen
withCStringLen :: TextEncoding -> String -> (CStringLen -> IO a) -> IO a

-- CStringLen から String
peekCStringLen :: TextEncoding -> CStringLen -> IO String

-- CString に対する（C文字列がNUL終端だと仮定する）変換関数もある。ドキュメント参照
```

これらの関数に渡すべき `TextEncoding` 型の値は [`GHC.IO.Encoding`](https://hackage.haskell.org/package/base/docs/GHC-IO-Encoding.html) にいくつか用意されています（`utf8` など）。他のエンコーディングを使いたい場合は、 `mkTextEncoding` を使うことで、Windowsならコードページを、Unix系ならiconvを使用した変換が可能です。

## ASCIIまたは8ビット文字列

`ByteString` と `String` を変換する際にコードポイントの下位8ビットを切り出す変換方法（`Data.ByteString.Char8`）があったのと同じように、 `String` のコードポイントの下位8ビットを切り出して `CString`(`Len`) と変換する関数もあります。

```haskell:Foreign.C.String
-- String から CStringLen
newCAStringLen :: String -> IO CStringLen
withCAStringLen :: String -> (CStringLen -> IO a) -> IO a

-- CStringLen から String
peekCAStringLen :: CStringLen -> IO String

-- CString に対する（C文字列がNUL終端だと仮定する）変換関数もある。ドキュメント参照
```

## UTF-8

既に述べたように、 `String` と `CStringLen` をUTF-8で変換する場合には、 `GHC.Foreign` の関数に `utf8` を渡せば良いでしょう。

`Text` の場合は、 [`Data.Text.Foreign`](http://hackage.haskell.org/package/text/docs/Data-Text-Foreign.html) に `Text` と `CStringLen` を変換する関数が用意されています。

```haskell:Data.Text.Foreign
peekCStringLen :: CStringLen -> IO Text
withCStringLen :: Text -> (CStringLen -> IO a) -> IO a
```

見た感じでは `newCStringLen` や、 `CString` 用の関数は用意されてなさそうです。必要なら自作しましょう。

## ByteString と CString(Len)

`ByteString` も `CString(Len)` もバイト列なので、エンコーディングや失敗時の挙動を考えなくても変換できます。

`Data.ByteString` に `packCString(Len)` と `useAsCString(Len)` という関数があります。

```haskell:Data.ByteString
packStringLen :: CStringLen -> IO ByteString
useAsCStringLen :: ByteString -> (CStringLen -> IO a) -> a
```

`ByteString` のデータはピン留されていますが、 `Data.ByteString` の関数は一旦データのコピーを作るようになっています。これは、本来イミュータブルであるはずの `ByteString` の内容がポインターを経由して書き換えられてしまうのを防ぐためです。コピーを作成せずに `ByteString` の内容をCに渡したい場合は、 `Data.ByteString.Unsafe` にある関数を使うと良いでしょう。

## ワイド文字列

ワイド文字列（`CWString`, `CWStringLen`）は `Foreign.C.String` に `String` との変換関数が用意されています。GHCが想定するワイド文字列はUnicode系なので、エンコーディングの指定はありません。ただ、不正なUTF-16や不正なUTF-32はあまりきちんとチェックされていないようです。

# まとめ

異なる文字列型同士を変換する際には

* 失敗しうる変換なのか
* どの程度のコストがかかる変換なのか（大抵の変換は長さに比例した時間がかかりますが、 `fromStrict` のような一部の関数は定数時間でできる場合があります）
* バイト文字列とUnicode文字列との間の変換の場合、使用されるエンコーディングはどれなのか

の3点を意識しましょう。

**Unicode文字列とバイト文字列を相互変換するための、万人が納得する標準的な方法はありません。** なので、「Unicode文字列とバイト文字列をひっくるめていい感じに変換してくれる関数・型クラス」みたいなやつには要注意です。実際、 `IsString` クラスはバイト文字列とUnicode文字列をひっくるめて扱おうとしたために失敗しています。

Unicode文字列とバイト文字列の際は、多少面倒でも、（UTF-8で変換されて欲しいのなら）UTF-8で変換してくれる関数を使う、という風にプログラマーが明示するようにしましょう。