この記事は、Haskell (その4) Advent Calendar 201714日目の記事です。
枠が空いていたので埋めるために登録しました。
長くかかった割には実験自体は失敗気味な、昨日のこちらの記事よりは有用な情報じゃないかと思います。
ほかの言語でもありそうな話ですしね。
すごく簡潔にまとめるとこの間の下記のツイートに収まるのですが、もう少し丁寧に補足するために書きます。
学んだことをまとめると
— Yuji Yamamoto: 山本悠滋 (@igrep) 2017年12月5日
- Invalid characterと言われたらchcp 65001しよう
- Permission Deniedと言われたらビルドし直そう
- 日本語のパスが混ざらないよう気をつけよう
- Cのライブラリーはものによる
ですか。多分 #haskell 以外でも有益な話。
Link to
hereInvalid characterと言われたらchcp 65001しよう
恐らく一番高確率で遭遇する & 知らないと回避できないのがこれ。
あ、ほらまたhakyllでビルドしたら起きた!
> stack exec -- site rebuild
...
[ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence)
GHCがファイルを読み書きする時に使うHandle
というオブジェクトには、文字コードの情報が含まれています。
これはRubyのIO
やPerlのファイルハンドラーにあるような仕組みと大体似ていて、Handle
といったデータの「入り口」を表すオブジェクトに文字コードを紐付けることで、外から入ってくる文字列の文字コードを確実に内部の統一された文字コードに変換してくれます。
HaskellのChar
型の場合はUTF-32(この場合その言い方でよかったっけ?)のはずです。
このHandle
に紐付ける文字コード、当然のごとくデフォルトではOSのロケール設定に従って設定されるようになってまして、日本語版のWindowsではそう、Windows-31J(またの名をCP932)ですね。
でも今はもうすぐ2018年。あなたが「メモ帳」でプログラムを書く人でもない限り、新しく作るファイルの大半はUTF-8でしょう。
UTF-8とWindows-31Jは全然違う体系の文字コードなので、UTF-8なファイルをWindows-31Jのファイルとして読もうとしてもうまくいかないわけです。
冒頭にあげたinvalid byte sequence
というエラーはまさにそうした場合に起こるエラーです。
ファイルの読み書きだけでなく標準入出力でもしばしば発生するので覚えておいてください。
Link to
here対策
Link to
hereユーザーとして出くわした場合
多くの場合、このエラーは以下のコマンドをあらかじめ実行しておけば回避できます。
> chcp 65001
> stack exec -- site rebuild
... 動くはず!
これは、現在開いているコマンドプロンプトで一時的に文字コードを切り替えるコマンドです。
65001
という数字がUTF-8を指しているようです。
もとに戻したい場合はchcp 932
と実行しましょう。
> chcp 932
どうやら「CP932」の「932」はここで出てくる「932」と同じものを指しているようですね!
どういう仕様なのか分かりませんが、このコマンド、MSYS2のbashでも使用できます。
ただしchcp
コマンドはC:\Windows\System32\
という、MSYS2ユーザーにとってはあまりPATH
に入れたくない場所に入っています。
このディレクトリーには、find.exe
など、Unixな方が好んで使うコマンドと同じ名前の非互換なコマンドがゴロゴロ転がっているのです!
なので私はMSYS2を使う時はC:\Windows\System32\
はPATH
から抜いています。
私と同じような方は下記のようにフルパスで実行しましょう。
/c/Windows/System32/chcp.com 932
Link to
hereそれでもダメな場合、あるいはライブラリーなどの開発者として出くわした場合
残念ながら、chcp 65001
してもこのエラーが消えないことはあります1。
私の推測なんですが、どうもchcp 65001
はchcp 65001
したコマンドプロンプト(とかbash)の孫プロセス(つまり、あなたが入力したコマンドの子プロセス)には届かないことがあるようです。
そんなときは、実際にエラーが起きているコマンドの開発元にバグ報告するか、自分で直してみましょう。
バグ報告する場合は、「chcp 932
してから実行してみて」とお願いすると、バグ報告を受けた開発者も再現しやすくて助かるかも知れません(残念ながら私はやったことがありません)。
自分で直す場合、いろいろ方法はありますが、対象のHandle
オブジェクトの文字コードを変えることで対処するのが、一番直接的で確実でしょう。
この問題はHandle
に設定された文字コードと実際にやりとりされる文字列の文字コードに食い違いが発生しているため起こるものなのですから、適切な文字コードに変えてしまえばいいのです。
状況にもよりますがエラーが起きたHandle
が普通のUTF-8なファイルを読み書きするものである場合、下記のようにすれば、問題は回避できるはずです。
import System.IO (hSetEncoding)
import GHC.IO.Encoding (utf8)
hSetEncoding handle utf8
それから、実際に私がhaddockのバグを直した時を例に標準出力(または標準エラー出力)でこのエラーが発生した時の対応も紹介しておきます。
コードだけ貼り付けると、下記のようにすれば少なくともエラーが起こらないようにすることはできます(このコミットとほぼ同じ内容です)。
{-# LANGUAGE CPP #-}
import System.IO (hSetEncoding, stdout)
#if defined(mingw32_HOST_OS)
import GHC.IO.Encoding.CodePage (mkLocaleEncoding)
import GHC.IO.Encoding.Failure (CodingFailureMode(TransliterateCodingFailure))
#endif
...
#if defined(mingw32_HOST_OS)
$ hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
liftIO #endif
Windowsでしか使用できないモジュールをimport
している関係上、CPPのマクロが混ざって読みにくいですが、重要な部分だけ切り出すと、
hSetEncoding stdout $ mkLocaleEncoding TransliterateCodingFailure
とすればよいのです。
一つ一つ解説しましょう。
まずhSetEncoding
は先ほども触れたとおり指定したHandle
の文字コードを変更する関数です。
そしてstdout
は名前の通り標準出力を表すHandle
です。
最後のmkLocaleEncoding TransliterateCodingFailure
ですが、これはWindowsで設定された文字コード(chcp
された文字コードと同じ)を作って、「もし(Unicodeから、あるいはUnicodeに)変換できない文字があった場合、エラーにせず、それっぽい文字に変換する」という設定で返す、という意味です。
結果、chcp 932
な状態でGHCのエラーメッセージにも使われる
↓この文字
• No instance for (Transformation Nagisa CardCommune_Mepple)
↑
が、
? No instance for (Transformation Nagisa CardCommune_Mepple)
のように、クエスチョンマークに変換されるようになります。そう、日本語のWindowsでGHCをお使いの方は一度は目にした「?」ではないでしょうか😅
つまりGHCはデフォルトでhSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailure
しているものと推測されます。
いずれにせよ、エラーでプログラムが異常終了しないだけマシですね。
更に補足すると、GHCの文字コードについてより詳しい情報は、GHC.IO.Encodingのドキュメントをご覧ください。
Link to
herePermission Deniedと言われたらビルドし直そう
雑なまとめと言いつつ最初の一つ目が長くなってしまいましたが、ここからは簡単に言います。
Windowsでstack build
なりghc
なりelm-make
なりとにかくいろいろ動かしていると、「Permission Denied」と言ったエラー(あるいはこれと似たようなメッセージのエラー)に出遭います。
正直に言って私は原因はサッパリ分かってないのですが、このエラーは大抵の場合何度も同じコマンドを実行すれば再現しませんでした。
一度や二度ではめげず、繰り返すのがポイントです 😅
問題が起きているディレクトリーをウィルス対策ソフトのスキャン対象から外してみるとか、Dropboxの同期を一時的に止めてみる、といったこともやってみるといいかもしれません。
あ、あと、「Directory not empty」みたいなのもあったかな。これは同類のはずです。
Link to
hereCのライブラリーは… まぁ、頑張れ。
Pure Haskellなライブラリーであれば大体OKなんですが、残念ながらCのライブラリー(lib***
みたいな名前でよくOSのパッケージマネージャーに登録されているやつですね)に依存したライブラリーは、Windowsでインストールするのは結構トラブることが多いです。
まぁ、これはHaskellに限った話ではないでしょう。
対応方法は私が知る限り完全にケースバイケースなので、ここでは知っている対応例をいくつか挙げておきましょう。
以上です!
それでは2018年もHaskell on Windows 10でHappy Hacking!! WSLなんて知らないぜ!🏁🏁🏁