Rustのmoveとimmutabilityについてのルール

Rustを勉強しながら書いていて驚いた点のメモ。

immutable変数で持っていたオブジェクトをmutable変数へmoveできる

Rustではletで宣言した変数はimmutableとなるが、immutable変数で持っていたオブジェクトであってもlet mutで宣言したmutable変数にmoveさせることができる。

例えば、以下のコードはコンパイルが通る。

let foo = "foo".to_string();
let mut bar = foo;
bar.push_str("_bar");
println!("foo = {}", bar);

どうもRustのimmutabilityはオブジェクトではなくて変数に紐づくようだ。

というより、代入演算子=はあくまでオブジェクトの複製を作る、という考え方なのだろうか。複製なので、右辺と左辺のmutabilityは独立になる。ただ、Copy traitを持たない場合は"たまたま"move semanticsになってしまう、ということなのか。

また、Rustには"interior mutability pattern"というのがある。つまり、CellRefCellといった型を使うと、immutable変数を通じて中身を書き換えられる。だとするとimmutableとは何だったのかという気分にもなる。なんとなく、Rustにとってのmutabilityというのはmemory safetyを確保できれば(つまりborrow checkerをパスできれば)そんなに厳密に守らなくてもいい概念なのかもしれない。

immutable変数はmoveするけどconstはmoveしない

C言語でプログラミングする際には、自分は定数をconst変数として宣言・定義することがよくある(これは悪い作法かもしれないが)。

一方、Rustではletで宣言するimmutable変数とconstで宣言する定数には違いがある。immutable変数はmoveするけど、定数はmoveしない。

つまり、以下のコードはコンパイルが通るが、

#[derive(Debug)]
enum Foo {
    Bar,
    Buzz,
}

fn consume_foo(f: Foo) {
    println!("Foo: {:?}", f);
}

fn main() {
    const F: Foo = Foo::Bar;
    consume_foo(F);
    consume_foo(F);
}

以下は通らない。

fn main() {
    let f = Foo::Bar;
    consume_foo(f);
    consume_foo(f);
}

fは最初のconsume_fooでmoveして無効になっているので、2回目のconsume_fooに渡すことはできない。

structのフィールドのmove

structのあるフィールドを外にmoveさせると、そのフィールドだけではなく基本的にstruct全体が無効化する。

よって、以下のコードはコンパイルが通らない。

#[derive(Debug)]
struct Foo {
    bar: String,
    buzz: String,
}

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    println!("foo: {:?}", f);
}

barへの代入の時点でfが無効化しているのでfprintln!することができない。

一方、別のフィールドへのアクセスはできる。なので以下のコードはコンパイルが通る。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    println!("buzz: {:?}", f.buzz);
}

ただし、クロージャが絡むと以下のような一見問題なさそうなコードがエラーになる場合がある。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    let c = || { println!("buzz: {:?}", f.buzz); };
    c();
}

どうもクロージャを作るときにfを丸ごとborrowしようとするらしく、f.barのmoveによってfが無効化しているのでエラーになるようだ。

この場合は以下のように書き直すとコンパイルが通る。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    let buzz = f.buzz;
    let c = || { println!("buzz: {:?}", buzz); };
    c();
}

配列要素のmove

structと異なり、配列やVecの要素を外にmoveさせることはそもそもできないらしい。以下のコードはエラーになる。

fn consume_string(s: String) {
    println!("S: {:?}", s);
}

fn main() {
    let ss: [String; 3] = ["zero".to_string(), "one".to_string(), "two".to_string()];
    consume_string(ss[0]);
}

なお、配列の中の型がCopy traitを実装している場合はmoveにならないので上記のような要素のアクセスは許される。

Haskellで書いたプログラムをRaspberry Piで動かす方法

Haskellで書いたプログラムをRaspberry Piで動かす方法に関して少し調べた。

Raspberry Pi上でHaskellプログラムをビルドする方法

LLVM関連パッケージとスワップサイズを増やすことに気をつければ、何とかなりそうな印象。stack/cabal/ghcによるビルドではとにかくメモリを食うのでスワップが必要になるようだ。

ただでさえRaspberry Pi上でのビルドになる上、スワップも必要になるので相当時間がかかりそうではある。

ARM仮想マシン内でHaskellプログラムをビルドする方法

QEMUのARMエミュレーション機能を使えば、x86_64 アーキテクチャマシン上でARM仮想マシンを立てることができる。上記のqemu-rpi-kernelを使えば、ARM仮想マシン上でRaspberry Pi(を模した)カーネルを動かし、Raspbianを動かせるようだ。

ただ、QEMUではメモリサイズが256MBに制限されているらしく、Haskellプログラムのビルドにはどのみちスワップ領域の設定が必要になる。また、QEMUのARMエミュレーションはめちゃくちゃ重いのでどのみちビルド性能はいまいちだと思われる。

ARM向けにHaskellプログラムをクロスコンパイルする方法

状況はかなり難しいようだ。

ARMをターゲットにしたクロスコンパイラとしてGHCをビルドすることはできそう。これによりごくシンプルなHaskellプログラムをビルドすることはできる。

ただ、もう少し複雑なプログラムを扱おうとすると問題が出てくるようだ。特にTemplateHaskellが厄介そう。

TemplateHaskellはコンパイル中にソースコードに埋め込まれたHaskellコード(splice)を実行する仕組みだが、クロスコンパイルではこのspliceがターゲットアーキテクチャの形式で出力されるため、ホストでは実行できない。

なお、ghcjsは本質的にJavaScriptへのクロスコンパイラなわけだが、ghcjsではTemplateHaskell spliceのコンパイル結果をnode.jsに流し込んで評価しているらしい。

上記記事では、spliceのコンパイル結果をネットワーク越しにRaspberry Piに流して評価してもらう構成をとっている。そこまですればTemplateHaskellの問題はクリアできるようだが、、かなりのdirty hackに見える。

TemplateHaskellを使うパッケージを一切使わなければ問題はないはずだが、aesonがTemplateHaskell使ってるんだよなあ。

TemplateHaskellの問題もあるが、クロスコンパイルではFFI(Foreign Function Interface)も問題を起こすことがあるようだ。

上記を鑑みると、Haskell(GHC)のRaspberry Pi(ARM)へのクロスコンパイルはあまり現実的ではないように思う。golangやRustはクロスコンパイルのサポートが充実していると聞くので、羨ましい限りだ。

Gitでmasterの開発内容を開発ブランチに反映させるとき、mergeか、rebaseか

参考:

あるあるのシナリオだが、masterブランチからフォークした開発ブランチでコミットを進めている間にmasterでコミットが進む場合。

master -o--o--o--o--o
           |
           v
dev        +--o--o--o

この場合も多くの場合では特に何も考えずにmasterにマージしていいはずである。

master -o--o--o--o--o--o
           |           ^
           v           |
dev        +--o--o--o--+

マージ時にconflictがある場合、ローカルのgitでマージをしているのならマージ元(master)でconflictを解消することもできるが、GitHubのpull-requestではマージがリモートサーバで発生するので少々ややこしい (conflict解消できるのか??)

また、masterブランチの新しいコミットを開発ブランチで使いたい場合も少々厄介だ。

こういうとき、自分は極力rebaseをするようにしている。

$ git checkout dev
$ git rebase master

これにより、コミットグラフは以下のようになる。

master -o--o--o--o--o
                    |
                    v
dev                 +--o--o--o

そのため、devでmasterの新しいコミットを使うことができる。rebase時にconflictが発生する場合があるので、その場合は自力で解決する。

注意点としては、devを既にどこかのリモートブランチにpushしていた場合、rebase後のローカルdevをそのままではリモートにpushできない。push --forceで強制的にリモートブランチを書き換えることはできるが、リモートdevを複数人で共有している場合は混乱を招くので注意が必要だ。

他のやり方としては、一度masterをdevにマージするやり方がある。

$ git checkout dev
$ git merge master

これにより、グラフは

master -o--o--o--o--o--+
           |           |
           v           v
dev        +--o--o--o--o

こうなる。

個人的にはこのやり方は好きではない。理由は、masterからdevへのマージリンクがコミットグラフに生じることで、コミットグラフの構造が複雑になるからだ。masterからdevへはフォーク(branch)だけが起こり、devからmasterへはmergeだけが起こる、というルールになっている方がコミットグラフが分かりやすくなる。と思う。

ということで、masterから任意の開発ブランチへのmergeを禁止するような設定がGitでできないか少し調べた。Git Hookを使えばなんとかできそうではあるが、ちょっと微妙な作りのスクリプトを使うことになりそうだ。

上記スクリプトでは、マージコミットのコミットメッセージをスクレイピングしてマージ元のブランチ名を取得している。うーん、もう少しエレガントにできないものか。

ところで、調べていると以下のQ&Aでもう一つの方法が言及されていた。

$ git checkout master
$ git branch dev2
$ git checkout dev2
$ git merge dev

要は、こういうコミットグラフを作る。

master -o--o--o--o--o
           |        |
           |        v
dev2       |        +--o
           |           ^
           v           |
dev        +--o--o--o--+

で、devは放置してdev2で開発を進める。

これだと確かに、masterから開発ブランチへ直接マージすることはない。devからdev2へのマージは、要はmasterへのマージのconflict解消作業を手元のdev2でやるということだと考えられる。とはいえ、なんだか余計にコミットグラフが複雑になったような印象は否めない。

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

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

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

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

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

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

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

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

.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で実装し直すという話もあるようだ。

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とかあんまりキープできないよな。

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を書き直す作業だけでそれなりに大変だった。