Hatena::ブログ(Diary)

DebugIto’s diary このページをアンテナに追加 RSSフィード

2017-09-03

慢性上咽頭炎の近況 (6)


前回の近況から約1年が経過した。これまであったことを記す。

前回はまだたまにめまいなどの症状があると書いていたが、幸いここ1年はそうした症状はほとんどなく、あっても半日程度で治っていた。直接的な症状はほぼ完治したと思う。

ただし、体調を崩した際に回復するのに時間がかかるようになった気がする。

2017年1月頭に風邪を引いた。咳が出て、喉が痛み、緑色の濃い鼻水が出た。熱は比較的早く下がり動けるようになったのだが、喉と鼻の症状は結局2週間くらい続いた。

次に風邪を引いたのは7月下旬だった。エアコンをつけて寝るようになって体が冷えたのだろうか。鼻が乾燥し、片方の鼻の奥が詰まる感覚があり、後鼻漏も少しあった。この時は微熱が10日間ほど続いた。

熱が下がり、体調はほぼ回復したのだが、1ヶ月経った今も鼻が乾燥する感覚と後鼻漏はわずかながら続いている。良くなったり悪くなったりを微妙に繰り返しながらダラダラ続いている感じだ。

Bスポット治療をしていただいている耳鼻科の先生によると、風邪などをキッカケにして慢性上咽頭炎が再発することもあるようだ。自分もそのケースなのかもしれない。ただ、症状がそれほど強くないので通院はしないでいる。鼻呼吸、鼻うがいを心がけ、以前教わったお灸をまたやり始めた。これで治ってくれればいいが。

2017-01-01

.cabalファイルのパース


先日、staversionの新しいバージョンをリリースした。

staversionはStackageやHackageに上がっているパッケージバージョンを調べるコマンドラインツール。今回のバージョンでは、.cabalファイルを読みこんでそのbuild-dependsにあるパッケージのバージョンを根こそぎ調べる機能を追加した。

$ staversion --hackage staversion.cabal 
------ latest in hackage
-- staversion.cabal - library
base ==4.9.0.0,
unordered-containers ==0.2.7.2,
aeson ==1.0.2.1,
text ==1.2.2.1,
bytestring ==0.10.8.1,
yaml ==0.8.21.1,
filepath ==1.4.1.1,
directory ==1.3.0.0,
optparse-applicative ==0.13.0.0,
containers ==0.5.9.1,
http-client ==0.5.5,
http-client-tls ==0.3.3,
http-types ==0.9.1,
transformers ==0.5.2.0,
transformers-compat ==0.5.1.4,
megaparsec ==5.1.2

(以下省略)

問題は.cabalファイルをどうやってパースするかだ。

.cabalファイルではわりとクセの強い独自フォーマットが採用されている。そのパーサツールはCabalパッケージのDistribution.PackageDescription.Parseモジュールで提供されているが、お世辞にも使いやすいものとは言えない。また、このパーサツールではGenericPackageDescription型を結果として返してくるが、このデータ型では.cabalファイル内でのフィールドの出現順などの情報は失われてしまう。staversionではこの点がイヤだったので、仕方なくmegaparsecで簡単なパーサを実装した。

で、今回はもうちょっとCabalのコードを読んでパーサがどうなっているかを調べてみた(Cabal-1.24.2.0)。

トップレベルのパーサはParseモジュールのparsePackageDescription関数である。この関数は2段階で.cabalファイルをパースする。まずはStringから[Field]を作り、[Field]からGenericPackageDescriptionを作る。

Fieldなど、パーサで使う内部データ構造や関数の多くはDistribution.ParseUtilsモジュールで定義されている。Fieldは.cabalファイルの基本構造を木構造で表現するデータ構造である。Stringから[Field]を作るのはreadFields関数(ParseUtilsモジュール)であり、.cabalのややこしいブロックフォーマットをパースする。ただし、ブロックの中身はまだStringのまま保持する。

Field内に保持されているStringのパースの仕方はフィールド種別によって異なる。パースの仕方を保持するデータ構造がFieldDescr(ParseUtilsモジュール)である。FieldDescrのfieldSetメンバは、「Stringをパースし、その結果を型aにセットする」という一連の処理を行う。

なお、基本的なデータ構造に対するパース処理はTextクラスのparseメソッド(Distribution.Textモジュール)として定義されている。一部のFieldDescrはこのparseメソッドを使って定義される。

[Field]のパース処理はparseFields関数(Parseモジュール)が行う。入力Fieldを、名前のマッチするFieldDescrで順次パースしていき、その結果を型aへ畳み込む。ちなみにaccumFields関数(ParseUtilsモジュール)もほぼ同じことをやるが、parseFieldsのほうはエラーハンドリングをより丁寧にやっているように見える。

parseFields関数を用いて実際に"library"などのセクションをパースするのはparsePackageDescription関数の内部関数getBodyである。が、これはStTモナド変換子(StateTと同等)で入力[Field]を更新しながら再帰呼び出しを行うかなり複雑な関数である。なんとなく、.cabalファイル中のif節の処理が大変そうに見える。

なお、GenericPackageDescriptionは.cabalファイルの条件分岐も情報として含んでいる。それにフラグなどの各種設定を与えてPackageDescriptionを作るのがfinalizePackageDescription関数(Distribution.PackageDescription.Configurationモジュール)である。その際、build-dependsフィールドのdependencyリストは一度Map型(Data.Mapモジュール)にまとめられ、複数ある場合はマージされる。そのため、finalizePackageDescription関数にはbuild-dependsリストをソートするという副作用がある。

.cabalファイルのパーサは、様々な歴史的事情もあり、かなり複雑な作りになっているように見える。また、現状ではFieldなどの中間データ構造は(exposeされているものの)基本的にCabal内部に使用が制限されており、外からは活用できない。その点に関して、以下のissueが出されている。

よりよいCabalの中間データ構造(CabalAst)を導入するのはどうかという提案。また、パーサをParsecで実装し直すという話もあるようだ。

2016-12-29

AttoparsecとMegaparsecについて


先日、HaskellのParserについていろいろ調べた。

きっかけは、attoparsecだとやっぱりエラーメッセージが不親切すぎてデバッグがキツい、ということ。

パーサは所詮純粋関数なので、部品を細かく分けてそれごとに単体テストを書いていけばいいという話もあるが、end-to-endテストがいくつかあれば十分な状況で、デバッグのためにいちいちそれをやるのはキツい。

デバッグ用の情報を得ようにも、attoparsecのParserはmonad transformerではないので、自前で動作ログを残すためのモナドを中に仕込むことはできない。WriterTなどで外から包むことはできるが、そうすると今度はパーサコードに大量のliftが必要になる。

比較的新しいパーサライブラリ。どちらかというとParsecをお手本に、Parsecのイケてないところを徹底的になんとかした、という感じ。

下記、parsersと非互換なのはやむを得ない事情から、らしい。

ReadP, parsec, attoparsecを共通APIで扱えるパーサコンビネータークラス。

エラーメッセージに注力したパーサ。色付きでエラーを出せる、らしい。

Parsecのtryと<|>演算子の使い方について。

Parsecで"try a <|> b"と書くと、aがfailしたらそのエラー情報は丸ごと消えてなくなる。これにより、bもfailした場合に直感に反するエラーメッセージが出たりする。

そもそも問題は、aのパース過程で既にbもfailすると分かっているのに(パース分岐がaに入ると確定しているのに)bを実行することである。そこで、tryをaの中に入れ込むよう、スコープを絞ってしまうのがいい、ということを論じている。Parsecの<|>は左辺が入力を消費せずにfailした場合のみ右辺を実行するので、aが何かしらの入力を消費すればbは実行されない。

ちなみにattoparsecは全てのパーサアクションにtryがついていると同等である(常にbacktrackする)ので、わりと上記のような厄介な状況に出くわすと思う。大きめのParserアクションで<|>を使うと途端にデバッグが難しくなる。どこでエラーになったのか分からないからだ。

attoparsecはエラーの場所に関する情報を出してくれないので、attoparsec-conduitパッケージ(現conduit-extraパッケージ)側でlineとcolumnを数えて場所情報を出せるようにした、という話。

ParsecからMegaparsecへの乗り換えガイド。

Megaparsec 4と5の新機能について。

incremental parseはversion 4でできるようになったが、Megaparsecはincremental parsingをきちんとサポートするものではないとのこと。考えてみれば、incremental parse(というかinfinite stream)をパースしようとしたらpositionとかあんまりキープできないよな。

2016-10-16

stackageの特定のresolverに上がっているパッケージバージョンを調べるツール


指定したHaskellパッケージのバージョン番号が指定したstackage resolverでいくつなのかを調べるコマンドラインツールを作った。

例えばこのように使う。

$ staversion --resolver lts-4.2 --resolver lts-7.0 conduit base
------ lts-4.2
conduit ==1.2.6.1,
base ==4.8.2.0

------ lts-7.0
conduit ==1.2.7,
base ==4.9.0.0

自分はHaskellパッケージを作る際、travis-CIを使ってresolverはいくつか変えながらテストを回し、その結果を依存先パッケージのバージョン範囲(.cabalファイルのbuild-dependsセクション)に反映させている。その場合、テストした各resolver(特にそのうちの最新と最古のもの)における依存先パッケージのバージョン番号を知る必要がある。stackage.orgのWebサイトでは、最新のLTSやnightlyにおけるパッケージバージョン番号は分かっても、古いresolverでのバージョン番号をまとめて表示することができない。

staversionを使えば、こういったバージョン番号調査をまとめて実施できる。現状では、stackがローカルにダウンロードして置いているビルドプランファイルを読み込んでバージョン番号を調べる。のちのち、自らネットワークを通じてビルドプランファイルをダウンロードできるようにする予定。

ところで、そもそも.cabalの依存先パッケージのバージョン番号範囲はどれだけマジメに書くべきなのだろうか。バージョン上限を入れるとやはりいろいろ面倒くさい。この間hspecのメジャーバージョンアップがあったが、コードの対応作業は全く必要ないにもかかわらず、ビルドを検証して.cabalを書き直す作業だけでそれなりに大変だった。

Zipper関連のパッケージ


先日コーディングしていてZipperが必要になる場面に出くわした。それは単純なList zipperだったので自前で実装して済ませたが、ふと、汎用的なZipperライブラリがあるかどうかが気になったので、少し調べた。

syz

traverse-with-class

uniplate

comonad-extras

zippo

zippers

  • zippers: Traversal based zippers
  • lensパッケージに依存。
  • LensやIndexedLensを使ってZipperのフォーカスを動かす。
  • (:>)といった型コンビネータ(?)を使い、非均一なデータ構造を辿る場合でも経路の型情報を全て保持する。


使い勝手がよさそうなのはsyzであるが、generic programmingを使う以上、型安全性が少しだけ弱いように思える。実際に使う場合はMaybeがたくさん出てきそう。

zippersはかなり型安全に見えるが、lensへの依存はそれなりに重い。また、Zipperの型を自分で正しく記述するのは至難の業なようにも思える。全て型推論に任せられるならいいが。

2016-09-22

Haskellでテンキーに動的なキーバインドを設定できるやつを作った


テンキーに対して動的なキーバインドを設定するためのHaskellライブラリ"WildBind"をリリースした。

現状はX11デスクトップ環境のみサポートしている。要はxbindkeysみたいなものだが、その実体は普通のHaskellモジュールである。以下のようにして使う。

import WildBind.Task.X11
import System.Process (spawnCommand)

main = wildNumPad myBinding

myBinding = binds $ do
  on NumCenter `run` putStrLn "Hello, world!"
  on NumPageUp `run` spawnCommand "firefox"

「動的なキーバインド」というのは、ここでは2通りの意味で使っている。

第1に、WildBindではアクティブなウィンドウに応じてキーバインドを変化させることができる。例えば、Firefoxが開いているとき、動画プレーヤー(VLCとか)が開いているとき、PDFビューワ(evinceとか)が開いているとき、それぞれに対してキーバインドを設定すれば、それらが自動的に切り替わる。

第2に、WildBindではキーバインド自体が状態を持ち、自身の状態によってキーバインドを変化させることができる。そのため、例えばEmacsのようにキーシーケンスに対してアクションを割り当てることができる。

なお、現状ではWildBindはテンキー上のキーにのみ、キーバインドを設定できる。これは、全てのキーを対象とするように作るのが面倒だったのと、全てのキーを対象とした場合は動的なキーバインドなどほとんど必要ないと思ったからである。テンキーという限られたスペースに可能な限り機能を詰め込むのがWildBindの基本的な考え方である。

詳しい説明やコード例はGitHub上のREADMEを参照されたい。



ところで余談だが、WildBindは数年前に作ったNumpaarというものが元になっている。

NumpaarはPerlとCで書かれていて、やたら複雑なマルチプロセス構成になっており、使いづらい上に不安定だった。そこで今回、全てをHaskellで書き直すことにしたのだった。