WindowsでHaskellを扱う時によく遭遇するエラーと対処法

雑なまとめ

Posted by Yuji Yamamoto(@igrep) on December 25, 2017

この記事は、Haskell (その4) Advent Calendar 201714日目の記事です。
枠が空いていたので埋めるために登録しました。
長くかかった割には実験自体は失敗気味な、昨日のこちらの記事よりは有用な情報じゃないかと思います。
ほかの言語でもありそうな話ですしね。

すごく簡潔にまとめるとこの間の下記のツイートに収まるのですが、もう少し丁寧に補足するために書きます。

Link to
here
Invalid characterと言われたらchcp 65001しよう

恐らく一番高確率で遭遇する & 知らないと回避できないのがこれ。
あ、ほらまたhakyllでビルドしたら起きた!

> stack exec -- site rebuild
...
  [ERROR] preprocessed-site\posts/2017/01-first.md: hGetContents: invalid argument (invalid byte sequence)

GHCがファイルを読み書きする時に使うHandleというオブジェクトには、文字コードの情報が含まれています。

これはRubyIOPerlのファイルハンドラーにあるような仕組みと大体似ていて、Handleといったデータの「入り口」を表すオブジェクトに文字コードを紐付けることで、外から入ってくる文字列の文字コードを確実に内部の統一された文字コードに変換してくれます。
HaskellChar型の場合はUTF-32(この場合その言い方でよかったっけ?)のはずです。

このHandleに紐付ける文字コード、当然のごとくデフォルトではOSのロケール設定に従って設定されるようになってまして、日本語版のWindowsではそう、Windows-31J(またの名をCP932)ですね。
でも今はもうすぐ2018年。あなたが「メモ帳」でプログラムを書く人でもない限り、新しく作るファイルの大半はUTF-8でしょう。
UTF-8Windows-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」と同じものを指しているようですね!

どういう仕様なのか分かりませんが、このコマンド、MSYS2bashでも使用できます。
ただし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 65001chcp 65001したコマンドプロンプト(とかbash)の孫プロセス(つまり、あなたが入力したコマンドの子プロセス)には届かないことがあるようです。

そんなときは、実際にエラーが起きているコマンドの開発元にバグ報告するか、自分で直してみましょう。
バグ報告する場合は、「chcp 932してから実行してみて」とお願いすると、バグ報告を受けた開発者も再現しやすくて助かるかも知れません(残念ながら私はやったことがありません)。
自分で直す場合、いろいろ方法はありますが、対象のHandleオブジェクトの文字コードを変えることで対処するのが、一番直接的で確実でしょう。

この問題はHandleに設定された文字コードと実際にやりとりされる文字列の文字コードに食い違いが発生しているため起こるものなのですから、適切な文字コードに変えてしまえばいいのです。
状況にもよりますがエラーが起きたHandleが普通のUTF-8なファイルを読み書きするものである場合、下記のようにすれば、問題は回避できるはずです。

それから、実際に私がhaddockのバグを直した時を例に標準出力(または標準エラー出力)でこのエラーが発生した時の対応も紹介しておきます。
コードだけ貼り付けると、下記のようにすれば少なくともエラーが起こらないようにすることはできます(このコミットとほぼ同じ内容です)。

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)

のように、クエスチョンマークに変換されるようになります。そう、日本語のWindowsGHCをお使いの方は一度は目にした「?」ではないでしょうか😅
つまりGHCはデフォルトでhSetEncoding stderr $ mkLocaleEncoding TransliterateCodingFailureしているものと推測されます。
いずれにせよ、エラーでプログラムが異常終了しないだけマシですね。

更に補足すると、GHCの文字コードについてより詳しい情報は、GHC.IO.Encodingのドキュメントをご覧ください。

Link to
here
Permission Deniedと言われたらビルドし直そう

雑なまとめと言いつつ最初の一つ目が長くなってしまいましたが、ここからは簡単に言います。
Windowsstack buildなりghcなりelm-makeなりとにかくいろいろ動かしていると、「Permission Denied」と言ったエラー(あるいはこれと似たようなメッセージのエラー)に出遭います。
正直に言って私は原因はサッパリ分かってないのですが、このエラーは大抵の場合何度も同じコマンドを実行すれば再現しませんでした。
一度や二度ではめげず、繰り返すのがポイントです 😅
問題が起きているディレクトリーをウィルス対策ソフトのスキャン対象から外してみるとか、Dropboxの同期を一時的に止めてみる、といったこともやってみるといいかもしれません。

あ、あと、「Directory not empty」みたいなのもあったかな。これは同類のはずです。

Link to
here
Cのライブラリーは… まぁ、頑張れ。

Pure Haskellなライブラリーであれば大体OKなんですが、残念ながらCのライブラリー(lib***みたいな名前でよくOSのパッケージマネージャーに登録されているやつですね)に依存したライブラリーは、Windowsでインストールするのは結構トラブることが多いです。
まぁ、これはHaskellに限った話ではないでしょう。

対応方法は私が知る限り完全にケースバイケースなので、ここでは知っている対応例をいくつか挙げておきましょう。

以上です!
それでは2018年もHaskell on Windows 10Happy Hacking!! WSLなんて知らないぜ!🏁🏁🏁


  1. 敢えて脚注に書きますが、Etaのコンパイラーをビルドしている時(のはず)、chcp 65001でもダメでchcp 20127ならうまくいったことがあります。
    chcp 20127US-ASCIIに切り替えるためのコマンドですが、やっぱりEtaの開発者の手元(?)ではそうなっているからなのでしょうか…?