日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

Rust で try! マクロが上手く動かなかった

Rust 絶賛学習中なんだけども、try! を 使用したサンプルが動かなかったのでメモとして残す。 原因はちゃんと追ってないが、状況と回避方法だけ。 (と言っても try! マクロ使わないようにするってだけだけど)

2016-05-08 追記

原因分かった。未読だったエラーハンドリング を読んでtry! の API docでのサンプル見てやっと理解した。

Result を返す関数の中で記述を簡略化する為のものなのね。 Rust の main は戻り値が無い(()を返す) 所に try!マクロ内の処理が失敗すると return Err が返るようになってるからコンパイルエラーになると。

けどそれならそれでサンプルは何がしかの Result を返す関数内で使うサンプルにすべきじゃないのかなあ。 まだまだ勉強が足りんかな。

と言うわけで単にtry!を使わないようにすると言うのはプロトタイピングレベルじゃなければやらないようにすべきなので 冒頭のは完全に消しはしないけど打ち消し線つけさせておいてもらいます。

環境

> rustc --version
rustc 1.8.0 (db2939409 2016-04-11)

状況

ファイル IO をやってみたくて API doc のサンプルコンパイルするとコンパイルエラー発生。

fn main() {
    use std::io::prelude::*;
    use std::fs::File;
    
    let mut buffer = try!(File::create("foo.txt"));
    
    try!(buffer.write(b"some bytes"));
}
<std macros>:5:8: 6:42 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
<std macros>:5 return $ crate:: result:: Result:: Err (
<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )
main.rs:37:22: 37:51 note: in this expansion of try! (defined in <std macros>)
<std macros>:5:8: 6:42 help: run `rustc --explain E0308` to see a detailed explanation
<std macros>:5:8: 6:42 error: mismatched types:
 expected `()`,
    found `core::result::Result<_, _>`
(expected (),
    found enum `core::result::Result`) [E0308]
<std macros>:5 return $ crate:: result:: Result:: Err (
<std macros>:6 $ crate:: convert:: From:: from ( err ) ) } } )
main.rs:39:5: 39:39 note: in this expansion of try! (defined in <std macros>)
<std macros>:5:8: 6:42 help: run `rustc --explain E0308` to see a detailed explanation
error: aborting due to 2 previous errors

エラーメッセージ見るとマクロ展開時に期待する型じゃないとか言われた。 けど API doc じゃ File::createは Result 返すし try! マクロ も Result を返すもので使うみ たいな事書いてあるのに、エラーメッセージはどうもそうなってない。

とりあえず try! を使用しないようにすればファイル書き込みはいけた。

fn main() {
    use std::io::prelude::*;
    use std::fs::File;

    let mut buffer = File::create("foo.txt").expect("cannot create file.");

    buffer.write(b"some bytes");
}

マクロのコードも追っかけてみたいけどまだ Rust でのマクロの作り方は全く学習してないので保留。

Rust 開発環境構築 - Eclipse で構築してみた

LifeTime や Trait、関数型プログラミング的な事も可能らしいシステムプログラミング言語辺りの ワードが気になって Rust を始めてみようと思いたった。

で、まずは環境構築からと言う事で探してみた。


開発環境選定

タイトルにも入れてるが今回は Eclipseプラグインで提供されている RustDT を選ぶことにした。 デバッグ機能が頭一つ抜けていると思ったため。

定義ジャンプ・コード補完・色付け辺りは大体どの環境も備えてて、gdbコマンドラインでガシガ シ使える人ならどれを選んでもそんなに機能差は無いと思う。

以下各環境を少し調べた雑感。

Emacs

  • 個人的には手を切りたいエディターというのはさておき
  • 例によって色々モードはあり設定も今はさほど面倒でもない
  • GDB とのデバッグ連携もかなり GUI っぽくなってるんだが 見た目がもはや他の IDE よりも煩雑になってる感じなのでスルー

IntelliJ

  • こちらもプラグインがあるのだが軽く情報を探った感じデバッガ連携が見当たらなかったので見送り
  • サイトを見ただけで試してはいない

VisualStudio

  • Visual Rust というプラグインがある
  • が、補完が何故か予約語しか効かないので見送った
  • デバッガ連携可能かどうかは未確認

Eclipse

  • RustDT というプラグインがあり、gdb との連携も可能らしい
  • 他の環境が備えている機能は大体持っている
  • 今回はこれを試した

他にもいくつか見かけたが未確認。

事前環境

  • MinGW-w64
    • RustDT で gdb を使うのに必要
    • 自分は Git for Windows SDK 1.0.3 経由で入れた
      • 時間はややかかるが必要なの一式ほぼ勝手に入れてくれるので楽
  • Rust 1.8.0 GNU ABI
    • Windows だと MSVC ABI もあるのだが、GNU ABI でないと RustDT が連携している GDBデバッグ情報を認識してくれない為
  • racer
    • コード補完を受け持つ外部ツール
    • 一番楽なのは Rust 入れた後 cargo install racer とコマンド打つだけ
  • Rust のソースを DL、環境変数 RUST_SRC_PATH にソースを展開したパスを指定してある事
    • racer で必要

結構事前準備が必要なんだけどどれもさほど面倒でもないのと記事を書いた時点では 既に導入してしまっていたので割愛。

Eclipse, RustDT インストール

とは言っても、既に ブログ記事 があったので基本はそこに従った。

ただ、事前環境でも書いたが、Rust は GNU ABI の方を入れる事。

デバッグ動作確認

こんなコードを書いた。*1

fn main() {
    let a = 10;
    let b = 20;
    let c = "こんにちわ";
    
    println!("{}, {} 年後の世界!", c, add(a, b));
}

fn add(x: i32, y: i32) -> i32 {
    x + y
}

んで2行目にブレークポイント設定してデバッグ実行。ブレークポイント設定は行表示している辺りを ダブルクリックすれば設定される。

んでブレークした所にも止まるし、ステップ実行も可能なんだけど変数を見る時 数値はいいんだが文字列は C/C++ 同様パッと見た感じさっぱり分からん。

ここ に書かれていた事を頼りに gdb の Rust pretty-printer を導入してみたが 文字列は対応してないっぽい。

文字列の簡単なデバッグ方法が今後の課題だけどとりあえずは しばらく RustDT を使いつつ勉強してみよう。

*1:何も調べず書いたけどはてなブログって既に Rust のシンタックスハイライトも対応してたのね

Dungeon Crawl Stone Soup 0.17.1(英語版)をビルドしてみた

ちょっと思う所があって PC フリゲのローグライクの一つである Dungeon Crawl Stone Soupをソースからビルドして上手く行ったので その時の環境をメモしておく。

  • OS は Win7 Pro 32bit
  • ソースは 公式に置かれてる Full source with dependencies のリンクから DL
    • 試したバージョンは 0.17.1。
  • ビルド方法は MinGW。手順は INSTALL.txt に書かれている事そのまま実行するだけ。

ソースビルド上手く行ったよって記事は賞味期限めちゃくちゃ短い気がするけどまあ。 英語だけどソースDLすれば本当にちゃんと手順が書かれてるので細かい事はここでは書きません。 繰り返しだけどソースビルド関係は特に腐るの早そうだし。

cider での TDD っぽいワークフロー

ワークフロー

  1. REPL 起動(C-c M-j) ※ 1 回のみ
  2. 何か関数書く
  3. REPL 上でテスト
  4. ok なら保存・リロード(C-x C-s, C-c C-n, C-c C-k)
  5. REPL 上で実行したコードと結果を = とした is フォームにコピペし、 適当な名前を付けて保存・リロード(C-x C-s, C-c C-n, C-c C-k)
  6. C-c ,でテスト

2-6 を繰り返す。

TDD と言うよりは REPL で試した結果を残すようにすると言った方が正しいかも。

改善したい部分や気になる所

On Lisp -> Clojure へ移植: 14.1~14.4

14.1 関数の構築

関数合成をマクロ化する意味が結局良く分からない。 funcall を暗に呼んでいて省略できるのがメリットくらい?それにしてもまだ呼び方が

(fn (compose list 1+ truncate))

と、Clojure の comp を知っていると使う側が fn を改めて呼ぶ必要があんのか と言う気もしてマクロ版作る気にならない。

ただ comp を作ってみるのは面白そうなので関数版で見ないで作ってみた。 4Clojure であったようななかったような、忘れた。

(defn compose
  ([] identity)
  ([f] f)
  ([f g] (fn [& args] (f (apply g args))))
  ([f g h] (fn [& args] (f (g (apply h args)))))
  ([f1 f2 f3 & fs]
     (reduce (fn [g f] (comp f g))
             (concat (reverse fs) [f3 f2 f1]))))

では大元の comp はどうなんだろと見てみたらちょっと長めだったので (ソースのリンク)https://github.com/clojure/clojure/blob/028af0e0b271aa558ea44780e5d951f4932c7842/src/clj/clojure/core.clj#L2391 だけ載せる。

関数返す所で設定してる仮引数も引数多重定義してる他はそう外れた作り方じゃない感じだったかなと。

この後見ると、On Lisp で作ってる fn マクロはもう少し柔軟な合成の仕方ができるみたいだけど やっぱりマクロ版はあんま作る気にならん。comp だってそうしょっちゅう使ってるわけじゃないしなあ。

14.2 Cdr 部での再帰

14.3 部分ツリーでの再帰

すんません、これらは自分にゃ難し過ぎたのでギブ。 一つ敢えて言うとするなら、ちょっと抽象化しすぎじゃね?って気が。

14.4 遅延評価

そういや Clojure にも標準で delay/force あったなあ。 とりあえず図 82 にあるものを移植してみる。

(defrecord Delay [forced closure])

(defmacro delay [& expr]
  (let [self (gensym)]
    `(let [~self (Delay. :unforced (fn [] ~@expr))]
       ~self)))
  
(defn force [x]
  (if (= (type x) Delay)
    (if (= (:forced x) :unforced)
      ((:closure x))
      (:forced x))
    x))

defconst は (def ^:const unforced (gensym)) としても良かったが、 Clojure だと Keyword を定数のように扱えるので特に設けなかった。

defstruct は defrecord で。まんま defstruct もあるんだが、 使ってるサンプルあんま見かけないし Clojure の方では試した事もない。 ちなみに defrecord で定義した名前は Clojure 標準も Delay と同じ名前。 標準の方は defrecord で作ったものではないみたいだが。

えーと、項目的に豪快にすっ飛ばしたけど、

第 14 章 関数を返すマクロ

はここまで。

On Lisp -> Clojure へ移植: 13.1~13.3

13.1 アナフォリックな変種オペレータ

p116

aif を初めとした図 72 ひと通り。

(defmacro aif [test-form then-form & [else-form]]
  `(let [~'it ~test-form]
     (if ~'it ~then-form ~else-form)))

(defmacro awhen [test-form & body]
  `(aif ~test-form
     (do ~@body)))

(defmacro awhile [expr & body]
  `(loop [~'it ~expr]
     (if (not ~'it)
       ~@body
       (recur ~expr))))

(defmacro aand [& args]
  (cond
   (empty? args) true
   (empty? (next args)) (first args)
   :default `(aif ~(first args) (aand ~@(next args)))))

(defmacro acond [& clauses]
  (if (empty? clauses)
    nil
    (let [cl1 (subvec clauses 0 2)
          sym (gensym)]
      `(let [~sym ~(cl1 0)]
         (if ~sym
           (let [~'it ~sym] ~@(cl1 1))
           (acond ~@(subvec clauses 2)))))))

aand はややこしいので本書の擬似コードを使って展開形を見てみる。

(pprint
 (clojure.walk/macroexpand-all 
  '(aand (owner x) (address it) (town it))))
;=> (let* [it (owner x)]
;     (if it (let* [it (address it)]
;              (if it (town it) nil))
;         nil))

it の束縛関係がよく分からんので、展開形しか確認できない本書の擬似コードでなく 実際に動くコードで確かめてみる。

(aand (inc x) (- it 10) (- it 100))
;=> 99

it は直近の式に束縛されるみたい。

acond は一応 Clojure の cond に近くなるよう subvec で展開するように書いてみたが これの動作は未確認。

続いて図 74 にあるやつ。alambda は Clojure に合わせて afn と言う名前にした。 block は Clojure で言う名前が付けられる do みたいなもんだが、標準では用意されてない。 ここでは名前指定する tag は省略した。

(defmacro afn [parms & body]
  `(letfn [(~'self ~parms ~@body)]
     ~'self))

(defmacro ablock [& args]
  `(do
    ~((afn [args]
        (case (count args)
          0 nil
          1 (first args)
          `(let [~'it ~(first args)]
             ~(self (next args)))))
      args)))

afn はよりそれらしい再帰は確かに書けるが、

((afn [x] (if (= x 0) 1 (* x (self (dec x))))) 10)
;=> 3628800

ここでも JVM の制限で recur を使わないとすぐ StackOverflowError になる。

((afn [x] (if (= x 0) 1 (* x (self (dec x))))) 100M)
;=> StackOverflowError   clojure.lang.PersistentHashMap$BitmapIndexedNode.index (PersistentHashMap.java:580)

と言うか、Clojure だと標準の fn で名前を指定する事もできる為、 afn のようなマクロを特別に用意する必要もなかったりする。 依然 StackOverflowError はつきまとうがそれはまた別の話。

((fn fact [x] (if (= x 0) 1 (* x (fact (dec x))))) 10)

なので本文に出ている count-instances は Clojure だと afn 無しにこう書ける。

(defn count-instances [obj lists]
  (map (fn self [list]
         (if list
           (+ (if (= (first list) obj) 1 0)
              (self (next list)))
           0))
       lists))

ablock は、、、名前が付けられず、return-from も使えないので Clojure では定義する意味がまるでないかな。一応動くが。

13.2 失敗

p120

いきなり相違点。Clojure では nil = 空リストではない。移植でさっきから使ってる cdr と似たような next を使ってみると、一見同じじゃないかと思える。

(next '(a))
;=> nil

となるが、それは next が nil を返してるだけで、比較してみれば分かる。

(= () nil)
;=> false

Clojure には next とほぼ似たような rest もあるが、こちらは () が返る。

(rest '(a))
;=> ()

この辺りは Clojure 界隈では falsy なんちゃらと言われたりしてて、Joy of Clojure でも確か見た気がする。

正直使い分け方はよー分からん。自分は falsy になる nil が欲しい事が多いので もっぱら next を使うけどあんまり明確な基準はない。

また、() は falsy ではなく、次を実行してみればその事が分かる。

(if () 'a)
;=> a

nil の他に真偽値も別にあり、他言語と同様に true/false となる。

(= 1 0)
;=> false

nil と false を分けたのはおそらく nil と言う値が欲しい場合と 偽値としての nil が欲しい場合を分けたくてこうしたのかな? On Lisp でもちょうどここでこの件について語られてるが 解決法は Clojure とは異なる。詳しくは本書でどうぞ。

p122

図 74 は 図 72 にあるアナフォリックマクロの多値版があるが、前にも書いたように 個人的に多値にあまり意味を見出だせないので飛ばす。

図 75 も特段何が便利になるのか今ひとつピンと来ないので飛ばす。

13.3 参照の透明性

ここはほぼ読むだけでサンプルは無し。

第 13 章 アナフォリックマクロ

はここまで。