Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

ライプニッツのモナド (1)

関数型プログラミング言語を触っていると,「モナド」という言葉が登場します.関数型プログラミングの文脈でのモナドは,圏論という数学の一分野が元ネタです.しかし,モナドにはさらに元ネタがいます.高校の倫理をやった方は,聞き覚えがあるかもしれません.そうです.ライプニッツです*1

私は圏論の論文を一切読んだことがなく,Wikipedia 情報でしか知りませんが,どうやらソンダース・マックレーンという人がライプニッツモナドを借用して命名したのがこの,圏論モナドなのだそうです.

当初は圏論の勉強をしてからライプニッツモナドの記事を書いてみたいなと思っていました.しかし,案の定数学の素養が足りず,学習に難儀したので断念しました.またの機会にします.

なお私はそもそもライプニッツが専門だったわけではありませんので,解釈の間違いなどありましたら教えていただけますと幸いです.

哲学を学習するにあたって少しだけ大切なこと

ある哲学者の思想を理解するにあたって,私が個人的に大切だと思うことが2つあります.

ひとつは,その哲学者の生きた時代背景を理解するということです.「哲学は「普遍」や「真理」を探求する学問なのでは?」なので,「時代背景も何もないんじゃないの?」と思うかもしれません.もちろんそういった一面もあるでしょう.

が,私がこれまで哲学を勉強してきた限りでは,ヘーゲルという人が『法の哲学』という著書の冒頭で書いた次の一節が,非常に哲学の学問上の特性を表していると思います.

ミネルヴァの梟は迫りくる黄昏に飛び立つ

ヘーゲルいわく,「哲学はもともと、いつも来方が遅すぎるのである。哲学は…現実がその形成過程を完了して、おのれを仕上げたあとで初めて、哲学は時間のなかに現れる」.

つまりどういうことかというと,哲学というのは,哲学者がその時代の人々が考えていたことをまとめあげたものだということです.哲学とは時代を総括したものなのだ,というのがこのミネルヴァの梟という有名な一節の言わんとしていることです.

もうひとつは,哲学は概念の集まりだということです.哲学の別の側面として,「概念 (concept) を創り上げること」があげられるはずです.哲学者はその時代の人々が考えていたことを的確に文章としてまとめあげつつ,そこに新しい「用語」を投入してきました.ライプニッツの「モナド」もそのうちの一つでしょう.「用語」というのはすなわちそのまま「概念」です.

「私的言語の限界こそ世界の限界である」とウィトゲンシュタインも言いました.少し都合よくこの言葉を解釈してみましょう.逆に捉えます.私たちは,自身の抱えているモヤモヤに対してつねに「言葉」を与え,「私的言語」を拡大させることで,新しい世界を創造してきたとも言えるはずです.新しい世界を獲得するための哲学でもあった,ということです.

したがって,ある哲学者の思想を勉強するにあたって必要なことは,

  1. その哲学者が生きた時代背景 (戦争や宗教が多くの場合契機となります)
  2. その時代ではどういったことが問題となっていたか
  3. その問題に対してどういう概念を使ってどう応えたか

を,整理しつつ学ぶと,哲学というものが少しだけわかってくることでしょう*2

ではモナドは,いったい時代のどのような問題に対して,どのような解決策の概念装置として使われたのでしょうか.まずは,ライプニッツが生きた時代から整理していきましょう.

法の哲学〈1〉 (中公クラシックス)

法の哲学〈1〉 (中公クラシックス)

法の哲学〈2〉 (中公クラシックス)

法の哲学〈2〉 (中公クラシックス)

論理哲学論考 (岩波文庫)

論理哲学論考 (岩波文庫)

哲学探究

哲学探究

哲学とは何か (河出文庫)

哲学とは何か (河出文庫)

ライプニッツと時代

ライプニッツは1646年生まれ,1716年没の人です.17世紀中盤〜18世紀初頭の人です.神聖ローマ帝国で生まれ,同じく神聖ローマ帝国で亡くなりました.神聖ローマ帝国は,今でいうドイツ全土,チェコやイタリアなどにまたがったひとつの大きな帝国でした.ライプニッツはその中でも今でいうところのドイツの領土に住む人でした.

三十年戦争

この時代の大きな出来事は,やはり三十年戦争でしょう.三十年戦争は1618年〜1648年で起こった戦争でした.きっかけは,神聖ローマ帝国内での宗教派閥争いでした.しかしそこに,フランスのブルボン家とスペインやオーストリアなどのハプスブルク家が介入したことで,ヨーロッパ全体を巻き込む戦争に様変わりました.

当時の神聖ローマ帝国は,300もの領邦によってできあがっていました.1555年に決まったアウクスブルクの和議により,各領主は,自身が信仰するキリスト教の派閥を自由に決めていました.ただ,民衆の信仰の自由はその和議では認められておらず,領主vs民衆という構図ができあがる領邦もありました.神聖ローマ皇帝が介入できるほどの権力をもっており,それを修正できればよかったのですが,皇帝は権力が弱く皇帝vs領邦の構図にもなってしまいました.それが,三十年戦争のきっかけでした.

戦争を止めきれなかった神聖ローマ皇帝は,スペインに軍事介入を要請しました.スペインはカトリック側の味方に付きました.スペインはハプスブルク家でした.当時,フランスのブルボン家が徐々に勢力を拡大してきていました.なので,フランスは覇権争いをするために,カトリックの国でありながらもプロテスタント側の味方につきます.これがまた事態をややこしくし,ヨーロッパ全体を巻き込む戦争になりました.

その後に結ばれたウェストファリア条約により,はじめて世の中に主権国家というものが誕生しました.が,その条約で同時に神聖ローマ帝国の領土は見事にバラバラになります.この条約が,神聖ローマ帝国の死亡証明書などと呼ばれるゆえんです.神聖ローマ帝国そのものは残りましたが,皇帝は実質名ばかりの存在となりました.

この戦争は本当に悲惨でした.ドイツ全土は荒廃し,多くの人々が亡くなりました.『人間の記憶のなかの戦争』という本に,当時の戦争の悲惨さを版画にした作品が多く載っています.版画はこのページに載っています.

人間の記憶のなかの戦争―カロ/ゴヤ/ドーミエ

人間の記憶のなかの戦争―カロ/ゴヤ/ドーミエ

ライプニッツの生涯

上からわかりとおり,ライプニッツはそういった荒廃しきったドイツに生まれ,亡くなりました.宗教起因の戦争がいかに悲惨な結果をもたらすかについて,間近で感じてきた人でした.三十年戦争の悲惨さを目の当たりにしてきたので,教会の統一を目標として人生を重ねた人だったようです.ちなみにライプニッツ自身はプロテスタントの人でした.

ライプニッツは外交官をやっていた時期があったということは忘れてはいけないでしょう.教会の統一のために実際に自分自身も実践者とし活躍していました.外交官をやっていたため,世俗的関心と宗教的関心と,各国の政治的関心のはざまで戦った人生だったと思います.ライプニッツは,思想を抽象的な概念で終わらせようとはしない,社会的な実現を望む実践者でした.

ライプニッツは他にもさまざまな分野で活躍した人でした.ニュートンとは違う手法の微分法を発見し,ニュートンと論争を繰り広げたこともありました.あるいは歴史学や法学の方面でも大活躍したマルチな人でした.時代を代表する人々と多くの書簡を交わしたことでも有名です.

ライプニッツの時代の人々が考えていたこと

この時代の人々が考えていたことは,「分裂した教会をなんとか統一したい」ということだったと言えます.宗教の対立がもたらす悲惨な戦争を経験してしまったためです.そもそも対立しない統一理論があればと思ってしまうことは,人間であれば致し方のないことです.

同時代の人にスピノザというこれもまた偉大な哲学者がいますが,彼も同様に「カトリック」「プロテスタント」の枠にとらわれない,独自の神学の世界観を構築した人でした.ちなみにライプニッツスピノザと面会しており,少なからず影響を受けました.

そんな中でのモナドです.「モナド」という概念から「予定調和」という概念を導きました.ライプニッツは実践者でしたので,本気で調和を実現しようと思っていたことでしょう.では,次の記事でモナドの中身について簡単に紹介したいと思います.

モナドロジー・形而上学叙説 (中公クラシックス)

モナドロジー・形而上学叙説 (中公クラシックス)

神学・政治論(下) (光文社古典新訳文庫)

神学・政治論(下) (光文社古典新訳文庫)

神学・政治論(上) (光文社古典新訳文庫)

神学・政治論(上) (光文社古典新訳文庫)

*1:厳密に言うと完全な元ネタではなくて,本当に元ネタなのはギリシャ語の μονάς (monas) です.これは「個」あるいは「単一」を意味します.

*2:もっとも,これは「学問上の」哲学を学ぶ上で重要なのであって,個々人が哲学する際に何が重要かということを決めるのはとても難しいことです.「哲学」と「哲学すること」の間にはかなりの差があります.今回のライプニッツに関するこの記事では,あくまで「哲学」であるということを忘れないでください.

Rust LT#2 で話をしました & その話の詳細な解説

Rust LT#2 で話をしました.「インタプリタを作ってまなぶ Rust らしい書き方」という話です.内容は実は,Ruby のコードを Rust のコードに置き換えてみようという内容でした.Ruby 製のインタプリタを Rust に置き換えるセッションです.

speakerdeck.com

しかし,5分では無理がありました.とくにインタプリタを2分以内で説明し切るのは完全に無理で,その実装を3分で説明し切るには時間が少なすぎました.なので,今回インタプリタを自作してみようという方向けに,どのような背景知識が必要となってくるのかについて簡単に書き残しておこうと思います.また,今回上げたソースコードの解説もこの記事の後半で加えておきます.

インタプリタ側の話

コンパイラという単語についてはすでに聞いたことがあるという前提で進めます.コンパイラそのものの解説.あくまでイメージを掴んでもらうことを目的としていますので,厳密さには欠きます.

コンパイラ

コンパイラの中身について軽く説明します.コンパイラは実は,いくつかのフェーズに分かれます.

  • 字句解析
  • 構文解析
  • 意味解析
  • 中間コード生成
  • コード最適化
  • コード生成

字句解析

プログラムの書かれた文字列を受け取って,トークと呼ばれるものを最終的に生成します.具体的には,

position = initial + rate * 60

というプログラム文字列があったとき,これを次のようなくくりで分解します.

〈position〉 〈=〉 〈initial〉 〈+〉 〈rate〉 〈*〉 〈60〉

〈〉 で囲まれた1つ1つの塊をトークと呼び,コンパイラはこのトークンからすべてがはじまります.このとき多くの言語ではホワイトスペースやタブは消失します.

ここで,=Eq+Add*Mul というトークンとして管理するとします.さらに,60Num, value というトークンとして管理するものとします.すると,

〈position〉 〈Eq〉 〈initial〉 〈Add〉 〈rate〉 〈Mul〉 〈Num, 60〉

というふうにトークンを整理できます.さらに,変数は記号表と呼ばれるものに保存することが多いです.positioninitialrate などの変数を,Var, id というトークンで管理し,記号表に内容を保存するものとしましょう.

〈Var, 1〉 〈Eq〉 〈Var, 2〉 〈Add〉 〈Var, 3〉 〈Mul〉 〈Num, 60〉

というトークン列に分解することができました.記号表は

id name value
1 position ...
2 initial ...
3 rate ...

というような構成になります.

構文解析

字句解析を行った結果,トークン列を受け取ります.そしてトークン列が持つ文法構造を明らかにしていくのが,構文解析です.最終的には構文木(あるいは抽象構文木: Abstract Syntax Tree; AST)と呼ばれる中間表現物を結果として得ます.

先ほど得たトークン列

〈Var, 1〉 〈Eq〉 〈Var, 2〉 〈Add〉 〈Var, 3〉 〈Mult〉 〈Num, 60〉

は,たとえば次のような構文木を作ります.

f:id:yuk1tyd:20180803184604p:plain

構文木は手順を表します.なので,掛け算は足し算よりも優先度が高いので足し算の木よりも処理があとに来るように構文木を構築します.

また,足し算引き算以外にもifwhilefor などの制御構文を構文木に直したりします.

この木構造でできているというのがポイントで,木構造だからこそ再帰的な処理で効率よく文章をたどっていくことができるのです.

意味解析

このフェーズではいくつかやることがあります.

  • 変数とか関数とかの使い方が言語定義に沿ったものになっているかチェック
  • スコープの決定
  • 型情報を収集して型検査
  • etc

このようにプログラムの意味の正しさについてチェックするフェーズが意味解析です.今回作成したインタプリタでは意味解析はほぼやっていません.

中間コード生成

〈Var, 1〉 〈Eq〉 〈Var, 2〉 〈Add〉 〈Var, 3〉 〈Mult〉 〈Num, 60〉

先程のこのトークン列からコンパイラが解析しやすい形にさらにトークンを変換します.たとえば今回のコンパイラでは,足し算と掛け算を一気に扱うと計算順序の一貫性を後続処理まで担保し続けることが難しいので両者を切り離したいと考えたとします.このとき,次のような中間コードを生成します.var1 = position, var2 = initial, var3 = rate だと思ってください.

m1 = 60
m2 = var3 * m1
m3 = var2 + m2
var1 = m3

このように切り分けた後,次の最適化フェーズでさらに中間生成物などの無駄を省いてコードを最適化します.

コード最適化

コード最適化フェーズでは要するに中間コードの無駄を省きます.

具体的には,上の中間コードでは, m1 は2度出てきていますし,m3 も2度出てきてしまっています.なので,

m1 = var3 * 60
var1 = var2 + m1

というように最適化できるので,このフェーズでそれをやってしまいます.

コード生成

最適化された中間コードからアセンブリなどが生成されます.

代表的なこれらのフェーズで一通りコンパイラの中で何が起きているか理解できたかと思います.

詳しいところは下記の本などが有名なのでそちらをご覧ください.

コンパイラ―原理・技法・ツール (Information & Computing)

コンパイラ―原理・技法・ツール (Information & Computing)

補足知識

さらに補足知識としてよく言語処理系の教科書で出てくる単語を簡単に抑えておきましょう.

パーサー

今回作成したインタプリタでは,構文木を人力で渡すとしていました.しかし通常プログラミング言語コンパイラでは,構文解析フェーズ (抽象構文木を作るところ) を自動的に解決させます.具体的にはトークンの優先度や前後関係,何を子にもつかなどを別ファイルに定義し,それに従ってプログラムにトークンを木構造に再整理させます.そのような機能をもつものをパーサーと呼びます.

インタプリタ

今回作成したインタプリタは,要するに意味解析くらいまでをやって,そこからそのままコードを評価してしまいます.実行ファイルを生成して,その実行ファイルに対して実行コマンドをかけるわけではないということです.

抽象機械

プログラムをどのように実行するかという規則を定義する装置です.これを抽象機械 (ちょっと具体化したものを仮想機械; Virtual Machine) と呼びます.

ラムダ計算

プログラミング言語の基礎理論として学ぶものです.今回登場させた簡約という概念も,じつはラムダ計算の中に登場してきます.ちなみに今回の発表したコードはラムダ計算で言うところのスモールステップ意味論操作的意味論などの単語が概念的には該当します.

さすがに書くと長くなるので参考資料をあげさせて代用とさせてください: [pdf] ラムダ計算入門

ここまで準備できたところで,ようやく今回作成したコードを読み解くための基礎知識を得たことになります.長くて申し訳ないですが,上記までを軽く理解していただいてからコードを読んでいただくとすんなり入ってくるのではないかと思っています.

Rust のサンプルコード側の話

さて,今回作成した Rust のコードの話にフォーカスして解説していきます.ソースコードは下記のリポジトリにあります.

github.com

構成

トークンを表現する enum Token と,VM を表現する struct Machine によって成り立っています.

enum Token

トークンそのものは enum で表現します.

#[derive(Clone)]
pub enum Token {
    Number(i32),
    BoolValue(bool),
    Var(String),
    Add(Box<Token>, Box<Token>),
    Multiply(Box<Token>, Box<Token>),
    LessThan(Box<Token>, Box<Token>),
}

Rust において enum代数的データ型になっています.パターンマッチすることができます.代数的データ型は数学的には直和の表現のようです.

たとえば,各トークンの情報をコンソール上に文字列で出力したいとします.Rust では Display トレイトを実装することで,Java で言うところの toString を定義できます.その定義の際に self に対してパターンマッチを行うことで,各トークンの出力方法を定義することができます.

impl fmt::Display for Token {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use Token::*;
        match self {
            &Number(v) => write!(f, "{}", v),
            &BoolValue(b) => write!(f, "{}", b),
            &Var(ref n) => write!(f, "{}", n),
            &Add(ref blv, ref brv) => write!(f, "{} + {}", blv.to_string(), brv.to_string()),
            &Multiply(ref blv, ref brv) => write!(f, "{} * {}", blv.to_string(), brv.to_string()),
            &LessThan(ref blv, ref brv) => write!(f, "{} < {}", blv.to_string(), brv.to_string()),
        }
    }
}

Token 1つ1つの簡約定義

さて各 Token には簡約定義をつけます.後々再帰的にトークンの簡約処理を回して結果を取得するためです.

そのためにまず,「これ以上簡約可能か?」を定義します.たとえば AddMultiply は評価を走らせさえすればもっと式を単純化できますが,NumberBoolValue はこれ以上評価のしようがありませんよね.

    pub fn is_reducible(&self) -> bool {
        use Token::*;
        match *self {
            Number(_) => false,
            BoolValue(_) => false,
            Var(_) => true,
            Add(_, _) => true,
            Multiply(_, _) => true,
            LessThan(_, _) => true,
        }
    }

で,fn is_reducible が true だった場合には,fn reduce が走ります.

    pub fn reduce(&self, env: &HashMap<String, Token>) -> Token {
        use Token::*;
        match self {
            &Number(_) => panic!("Number token couldn't reduce!"),
            &BoolValue(_) => panic!("BoolValue token couldn't reduce!"),
            &Var(ref name) => env.get(name).expect("Variable couldn't get!").clone(),
            &Add(ref blv, ref brv) if blv.is_reducible() => {
                Add(Box::new(blv.reduce(env)), brv.clone())
            }
            &Add(ref blv, ref brv) if brv.is_reducible() => {
                Add(blv.clone(), Box::new(brv.reduce(env)))
            }
            &Add(ref blv, ref brv) => match **blv {
                Number(left_value) => match **brv {
                    Number(right_value) => Number(left_value + right_value),
                    _ => panic!("Unexpected error in Add!"),
                },
                _ => panic!("Unexpected error in Add!"),
            },
            &Multiply(ref blv, ref brv) if blv.is_reducible() => {
                Multiply(Box::new(blv.reduce(env)), brv.clone())
            }
            &Multiply(ref blv, ref brv) if brv.is_reducible() => {
                Multiply(blv.clone(), Box::new(brv.reduce(env)))
            }
            &Multiply(ref blv, ref brv) => match **blv {
                Number(left_value) => match **brv {
                    Number(right_value) => Number(left_value * right_value),
                    _ => panic!("Unexpected error in Multiply!"),
                },
                _ => panic!("Unexpected error in Multiply!"),
            },
            &LessThan(ref blv, ref brv) if blv.is_reducible() => {
                LessThan(Box::new(blv.reduce(env)), brv.clone())
            }
            &LessThan(ref blv, ref brv) if brv.is_reducible() => {
                LessThan(blv.clone(), Box::new(brv.reduce(env)))
            }
            &LessThan(ref blv, ref brv) => match **blv {
                Number(left_value) => match **brv {
                    Number(right_value) => BoolValue(left_value < right_value),
                    _ => panic!("Unexpected error in LessThan!"),
                },
                _ => panic!("Unexpected error in LessThan!"),
            },
        }
    }

fn reduce では何をやっているかというと,「親自身が簡約可能なのであれば,左右それぞれの子どもの簡約可能性を見て,さらに簡約可能性探索処理を走らせる.これ以上無理なら簡約する.」ということをやっているだけです.再帰の力を存分に使っています.美しいですね.ちなみに簡約できなかった子の方を clone() しているのはなんとなく無駄かなと思っています.

**blv とか **brv みたいな変数の頭についている ** については,参照外しを2回行っているということです.わかりにくいですが,ref blv というのは要は ref Box<T> になっていて,参照が2つくっついているん (Box も参照の一種) ですね.素の値を欲しいがために ** としています.

エラーメッセージについては,本来はきちんと何行目のどこで出力されたのかを蓄積して持ち回る必要があります.failure クレートなどを使って書くべきではあるんですが,今回は panic で済ませました.

struct Machine

スライドの中でまったくこちらに触れられなかったので解説します.Machine は VM を表現した構造体で,「現在のトークン列の解析状況」と先ほど出てきた「記号表」を保持しています.

pub struct Machine {
    expression: RefCell<Token>, // 現在のトークン列の解析状況
    environment: HashMap<String, Token>, // 記号表
}

実行.

impl Machine {
    pub fn new(expression: Token) -> Self {
        Machine {
            expression: RefCell::new(expression),
            environment: HashMap::new(),
        }
    }

    pub fn run(&self) {
        let environment = HashMap::new();

        while self.expression.borrow().is_reducible() {
            println!("{}", self.expression.borrow());
            self.step(&environment);
        }

        println!("{}", self.expression.borrow());
    }

    fn step(&self, environment: &HashMap<String, Token>) {
        self.expression
            .replace(self.expression.clone().into_inner().reduce(environment));
    }
}

fn new(...) というのはよくやる手で,これを作っておくと構造体の生成が楽になります.こんなふうに.

Machine::new(actual).run();

実行そのもの (fn run) は何をやっているかというと,

  1. 記号表を HashMap で生成.
  2. Token が簡約可能かチェック
  3. 簡約可能ならば,簡約用の関数 fn step を走らせる.
  4. fn step の中で簡約を起こし,途中経過を expression に保存する.

というようなことをやっています.重要なのは reduce()再帰処理を繰り返しているということで,これはインタプリタを作る上ではよく使う手です.

これで実行できるようになりました.テストコードもそれなりに書いてあるので,よかったらデバッグしてみてください.

作ってみた感想とか

  • どこで所有権を奪ってとか,どれの所有権を持ち回るかみたいなところがまだまだ難しい: 困ったら clone しちゃうんですよね.してもいい場合とかもちろんあるんですけど,余計なメモリ上のコピーなどはできるだけ少なくしたいなと常々思いつつなかなかうまくできません.設計の問題なのかもしれません.
  • 型推論強い: たとえば fn run の中の let environment = HashMap::new(); なんかは,Scala だと型パラメータをつけないと (今回の場合だと HashMap[String, Token]() みたいにね) Nothing になってしまってダメなんですが,Rust は後ろの fn step の仮引数の型パラメータから推論してくれるのか,HashMap::new() で済んでしまうのがすごいですね.Hindley/Milner やっぱすごいな.

最後に

元ネタの引用を忘れていたので元ネタ載せておきます.一応元ネタがあります.Ruby コードで載っていて,Rust で書き直すちょうどいい練習になったので今回発表に使わせてもらいました.

いい本です.コンピュータサイエンスの教科書を読み解くと,どうしても数式の羅列だったりしてそもそもそういった数学教育を受けていないと解読が難しかったりします.しかしこの本はコードでコンピュータサイエンスの諸概念を解説してくれるので,コードさえ読めればどんな難しい概念でも理解できるすばらしい一冊です.よかったらどうぞ.

関数を呼び出すまではアセンブリに直されない? (C++ と Rust を見比べた)

もしかすると一般的な話なのかもしれませんが,おもしろかったのでメモ書き程度に残しておきます *1ソースコードはすべてアセンブリに直されているものだとばかり思っていましたが,そうではないんですね.

使ったツールは,Compiler Explorer というサイトです.

ちなみに,Rust のゼロコスト抽象化 (zero cost abstraction / zero overhead principle) について,アセンブラではどのような処理がなされているのかを調査していた最中に見つけました (ですが,今回はゼロコスト抽象化は関係のない話です.これはまた別途記事にしようと思います.).

C++

C++ で,次のようなコードをコンパイルさせて,アセンブリがどのように生成されるのかを見ていました.初めて見たんですが.

class A {
    private:
    int value;

    public:
    A(int a) : value(a) {}

    int get() {
        return value;
    }

    void set(int a) {
        value = a;
    }
};

int main() {
    A a_stack_cpp(5);
}

すると,つぎのようなアセンブリが生成されるはずです (以降,すべて x86-64, gcc 8.1 です).

A::A(int):
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -8(%rbp)
        movl    %esi, -12(%rbp)
        movq    -8(%rbp), %rax
        movl    -12(%rbp), %edx
        movl    %edx, (%rax)
        nop
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        leaq    -4(%rbp), %rax
        movl    $5, %esi
        movq    %rax, %rdi
        call    A::A(int)
        movl    $0, %eax
        leave
        ret

クラス用のコンストラクタと,main 関数に関するアセンブリが生成されているようですね.ここで,「setgetアセンブリはじゃあどうなるんだろう?」という点が気になりました.C++アセンブラの出力内容を見るのは初めてだったので,あまり想像がつかなかったのでやってみました.

ということで, get を使う記述を追加します.

class A {
    private:
    int value;

    public:
    A(int a) : value(a) {}

    int get() {
        return value;
    }

    void set(int a) {
        value = a;
    }
};

int main() {
    A a_stack_cpp(5);
    a_stack_cpp.get(); // 追加した
}

すると,アセンブリはつぎのように出力されました.

A::A(int):
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -8(%rbp)
        movl    %esi, -12(%rbp)
        movq    -8(%rbp), %rax
        movl    -12(%rbp), %edx
        movl    %edx, (%rax)
        nop
        popq    %rbp
        ret
A::get():
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -8(%rbp)
        movq    -8(%rbp), %rax
        movl    (%rax), %eax
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        leaq    -4(%rbp), %rax
        movl    $5, %esi
        movq    %rax, %rdi
        call    A::A(int)
        leaq    -4(%rbp), %rax
        movq    %rax, %rdi
        call    A::get()
        movl    $0, %eax
        leave
        ret

これは興味深いですね.get メソッドを呼び出す記述を追加すると,同時にアセンブラにも get メソッドに関するアセンブリが追加されました.要するに,メソッドを使用するタイミングになってはじめて,必要な分だけアセンブリを生成するようになっているという感じでしょうか.逆に,set に関するアセンブリは一切生成されていません.無駄がなくていいですね.

Rust

ここで普段使っている Rust がどうなっているのかも知りたくなってきますね.ということで,似たようなコードを書いてどういう動きをするのかを見てみましょう.(以下,rustc 1.27.1 です)

struct A {
    value: i32,
}

impl A {
    fn new(value: i32) -> A {
        A { value }
    }

    fn get(self) -> i32 {
        self.value
    }

   // set はちょっとめんどうだったので省略しました…
}

pub fn main() {
    let a = A::new(5);
}

アセンブリに直してみましょう.

example::A::new:
  pushq %rbp
  movq %rsp, %rbp
  subq $4, %rsp
  movl %edi, -4(%rbp)
  movl -4(%rbp), %eax
  addq $4, %rsp
  popq %rbp
  retq

example::main:
  pushq %rbp
  movq %rsp, %rbp
  subq $16, %rsp
  movl $5, %edi
  callq example::A::new
  movl %eax, -4(%rbp)
  addq $16, %rsp
  popq %rbp
  retq

C++ と似たような匂いがしてきました.main 関数の中では A の生成しか呼び出していないので,A の生成部分しかアセンブリが生成されていません.こちらも無駄がなさそうです.

代わりに get を一度だけ呼び出してみます.普段はこういう書き方しないけど.

struct A {
    value: i32,
}

impl A {
    fn new(value: i32) -> A {
        A { value }
    }

    fn get(self) -> i32 {
        self.value
    }
}

pub fn main() {
    let a = A::new(5);
    a.get();
}

すると

example::A::new:
  pushq %rbp
  movq %rsp, %rbp
  subq $4, %rsp
  movl %edi, -4(%rbp)
  movl -4(%rbp), %eax
  addq $4, %rsp
  popq %rbp
  retq

example::A::get:
  pushq %rbp
  movq %rsp, %rbp
  movl %edi, %eax
  popq %rbp
  retq

example::main:
  pushq %rbp
  movq %rsp, %rbp
  subq $16, %rsp
  movl $5, %edi
  callq example::A::new
  movl %eax, -4(%rbp)
  movl -4(%rbp), %edi
  callq example::A::get
  movl %eax, -8(%rbp)
  addq $16, %rsp
  popq %rbp
  retq

無事,get 関数に関するアセンブリが追加されたことがわかりました *2

まとめ

  • C++ や Rust においては,メソッド (あるいは関数) に関するアセンブリは,1回以上呼び出されたもののみ生成される.
  • 言語にもよりますが通常,関数やメソッドは意味解析の段階でコンパイラ側(あるいはインタプリタ側の)の仮想のスタックに積まれて,そのスタック内で順繰りに評価が走って実行されます.積まれた順に各言語のVM用の命令に直されます.そもそも関数呼び出しが起きなければスタックに積まれずそこに対する処理が走らないため,結果的にアセンブリが生成されなかったという話なんですかね.gcc も rustc もまったく詳しくないのでそうなってるかはわかりませんが.
  • ちなみに C++ 側で,変数を volatile にしてみても結果は変わらなかったので,最適化とは関係なさそうというところまではわかっています.その先がよくわからないので,上の解釈が正しいかは微妙なところですね….

*1:あと,アセンブラアセンブリアセンブルというややこしい3つの用法があってるか自信がないので,雰囲気で感じ取っていただけますと嬉しいです.

*2:example というのは,多分 crate 名が Compiler Explorer 上では example になっているからだと思います.

Rust LT #1 で話をしました

先日開催された Rust LT #1 で話をしました.話は HTTP サーバーを自作してみるというものです.Rust に最近入門された方や,今回の LT ではじめて Rust に触れる方向けに,なにかお話できたらなと思い LT をしました.

実は,去年の末のアドベントカレンダーのネタとして使おうかなと思って裏で作っていました.が,いい感じにコードが完成せず,とりあえず別の話をと思ってそのときにはお蔵入りさせていました.今回いい機会だと思ってお話しました.

直前に Rust OSS のツイートでなぜかあのリポジトリが捕捉されていて,「あ,そいえばこういうの作ったな.よし話そう」と思ったのは内緒です.

リポジトリは下記です.もしコードを読んでいておかしい箇所がありましたら,ぜひ遠慮なくご指摘ください.

github.com

今後作ろうかなと思っているものとして,tokio を用いたノンブロッキングな HTTP サーバーがあります.こちらもチャレンジングで今回のリポジトリを少し改変すれば実装可能なはずなので,興味のある方はぜひ一緒にチャレンジしましょう!

次回は OS 関係の話か,最近作っている MinCaml の Rust 版の話でもしようかなと思います.Rocket と Vue.js で Todo アプリを作ったこともあるので,その話でもいいかもしれませんね.業務利用のチャンスはしばらくなさそうですが.

お酒を片手にそんなお話をしました.楽しかったです.次はライプニッツの話書きますね.私も最近関数型プログラミングをしていてそこに登場してくる「圏論モナド」と,「ライプニッツモナド」とがどう違うかは改めて考え直したいなとちょうど思っていたところでした.

簡易シェル LSH を Rust で実装してみた

シェルをフルスクラッチしてみようと思い,まずは手始めに簡易的なシェルとして紹介されていた LSH を実装してみました.

実装したソースコードはこちらにあります.

github.com

LSH に実装されている機能

LSH には次のコマンドが実装されています.

  • cd
  • help
  • exit
  • 指定したプログラムの実行

これだけのコマンドを簡単に実装するだけなので,システムプログラミングのいい入り口になるかなと思います.なので,興味のある方はぜひチャレンジしてみてください.

使用したライブラリ

  • nix: これは以前にも紹介したことがありますが,今回はシステムコールの呼び出し部分をすべてこのライブラリで実装しています.Rust の unsafe な処理を safe になるようにラップしたライブラリで,unsafe ブロックを呼び出さずに済むメリットがあります.ただ,実装されていないコマンドも多く,今後どうなるかは動向が気になります.

全体の構成とシステムコールを使用した箇所に関する解説

元の実装は C 言語によるもので,若干 C 言語特有のワークアラウンドを Rust 向けに書き直すという作業が必要になります.また,型クラスや代数データを今回行った実装よりもハードめに使えば,もう少しスッキリした実装にできたかなとは思いますが,C 言語側の実装に関数名や流れを合わせるという意図から今回はそうしませんでした.

簡易的なシェルの作り方ですが,これはとてもシンプルです.文字列をパースしてパターンマッチし,マッチされた結果に該当する関数を呼び出すという処理を行うだけで実装可能です.

コマンドそのものの実装については,cd コマンドのようにシステムコールを1つ使用すれば実装できてしまうものもあれば,cat や ls コマンドのように,いくつかのシステムコールを組み合わせて実装するコマンドもあります.

cd コマンド

cd コマンドの実装は,拍子抜けかもしれませんがシステムコールchdir を使用していましたので,その通りに使用しました.

fn lsh_cd(dir: &str) -> Result<Status, LshError> {
    if dir.is_empty() {
        Err(LshError::new("lsh: expected argument to cd\n"))
    } else {
        chdir(Path::new(&dir))
            .map(|_| Status::Success)
            .map_err(|err| LshError::new(&err.to_string()))
    }
}

指定したプログラムの実行

よくあるプログラム実行です.プロセスを fork し,子プロセスにプログラムそのものの実行は任せ,親プロセスは子プロセスの結果を待つという流れです.各システムコールに関する詳しい解説については以前書いた記事にまとめております

fn lsh_launch(args: Vec<String>) -> Result<Status, LshError> {
    let pid = fork().map_err(|_| LshError::new("fork failed"))?;
    match pid {
        ForkResult::Parent { child } => {
            let wait_pid_result =
                waitpid(child, None).map_err(|err| LshError::new(&format!("{}", err)));
            match wait_pid_result {
                Ok(WaitStatus::Exited(_, _)) => Ok(Status::Success),
                Ok(WaitStatus::Signaled(_, _, _)) => Ok(Status::Success),
                Err(err) => Err(LshError::new(&err.message)),
                _ => Ok(Status::Success),
            }
        }
        ForkResult::Child => {
            let path = CString::new(args[0].to_string()).unwrap();
            let args = if args.len() > 1 {
                CString::new(args[1].to_string()).unwrap()
            } else {
                CString::new("").unwrap()
            };
            execv(&path, &[path.clone(), args])
                .map(|_| Status::Success)
                .map_err(|_| LshError::new("Child Process failed"))
        }
    }
}

さて,実行についてですが,example/ 配下にある C プログラムをコンパイルして,lsh シェルを実行し,次のようなコマンドを打つとうまくいくはずです.

C のコンパイルについてはお決まりの,

gcc -o main main.c

そして,lsh シェルを起動し,

cd ./example
./main

と実行するだけです.Hello, World! とコンソールに出力されたら成功です.シェルを終了するには,

exit

と入力します.

まとめ

  • LSH は簡易シェル作成のいい練習になるのでオススメです.
  • nix を使うとシステムコールを容易に呼び出せる上に,後続処理を Rust らしい書き方にできるのでいいと思います.
  • 時間があったら,ls コマンドや cat コマンドも実装してみたいですね.とりあえず ls コマンドがないのは不便なので一番最初はそれかなと思います.

Junction Tokyo 2018 で優勝しました――技術的な話

3/23-2/25 で,Junction Tokyo というハッカソンに参加してきました.そして,Logistics 部門において見事,優勝をすることができました.3日間本当にお疲れ様でした.スタッフの皆さんもどうもありがとうございました.

Junction Tokyo は,毎年5月頃に開催されているハッカソンで,私も去年出場しました.もともとはフィンランド発のハッカソンらしく,海外からの出場者も半数以上いるような国際色豊かなハッカソンです.アナウンスなどはすべて英語で行われますし,チーム内でも普通に英語で会話します.

Junction Tokyo Hackathon

私たちのチームはいわば「宅配版Uber」のようなアイディアで,車で通勤する社会人向けのサービスを考案し発表しました.車のトランクや屋根といった空きスペースを使って荷物を運び,その運んだ状況を IoT デバイスで記録して,安全な運転を行うドライバーはより高い評価を得られるというアイディアも盛り込みました.

チームにプレゼンの神様のような方がおり,その方がまず非常によくまとまったプレゼン資料を作ってくれました.またデザイナーの方は,とてもきれいな UI のプロトタイプを作ってくれました.これら2つによって優勝することができたと言っても過言ではないです.ビジネスプランとしてはベストなものが発表できたと思います.

さて一方で,プレゼンではほとんどサーバーサイド側の話をしている余裕がありませんでした.あくまでハッカソンはアイディア勝負であり,デモも行うといえば行うのですが,あくまで注目されるのはフロントエンドです.サーバーサイドの話を盛り込むことができませんでした [*1].

しかし,一応サーバーサイドエンジニア枠で出場したので,ブログでは思いっきりサーバーサイド寄りの話をここに書いておきたいと思います.

何を作ったのか,そして中身について

上記のサービスを利用する際に使用するアプリの画面と,IoT デバイスで振動や傾きを検知するプログラムと,それらを REST を使ってやりとりするサーバーを作りました.IoT デバイスの結果を一度 Python プログラムに渡して独自のアルゴリズムで評価し,それをサーバーに送ります.サーバーはそれを受け取ると,今度は画面側のドライバーの評価用にまたオブジェクトを返す,という流れです.その他,ドライバー一覧の表示やドライバーの登録を DB に行うといったこともできるようにしました.

f:id:yuk1tyd:20180326231002j:plain
全体の構成のスケッチ

サーバーには全面的に Scala を採用しました.今回は Scala の生産性の高さに非常に助けられたように思います.ハッカソンは短い時間でプロトタイプを出す必要があるので,使い慣れていて生産性の高い言語やツールを使えるとベストだと思います *2.また,今回 Scala を使ったからか技術点を結構もらうことができ,結果的に優勝につながったという点でも Scala を採用してよかったかなと思いました.

Web フレームワークFinagleFinch を採用しました.これも第一に私が使い慣れているためです.

Finch は非常に処理を書きやすく,関数型プログラミングに特化されており,コンビネータを用いた洗練された設計になっています.処理を多少雑に書いても予想外の挙動が発生しにくいように感じます.さらに twitter-util や finagle-XXX のような強力なエコシステムをもっており,それを利用するだけで非常にすばやくアプリケーションを構築できます.DB には MySQL を採用しましたが,finagle-mysql を使って簡単に接続することができました.O/R マッパーもシンプルに実装できました.Finch の生産性の高さにもまた,とても助けられました.

JSON のパースには circe を使用しました.仕事でも使っていて大変高評価だったのですが,改めて circe は楽でした.ほとんど自動的に JSON のパースをしてくれて本当に助かりました.

github.com

画面側では Vue.js を使用しました.これは別のインドネシアの子が作ってくれました.とても優秀なエンジニアで,こちらが返す (めちゃくちゃな) JSON をきれいに画面に反映してくれました *3インドネシアの方は英語もできるし頭もきれるし,これから彼らと戦わないといけないのか…と思うと正直ゾッとしましたね (笑).

IoT デバイス側は,アメリカ出身の方が作ってくれました.BOSCH の XDK を使って,主に傾きと揺れを検知し,ドライバーがどの程度安全に運転しているかの指標にしようとしました.検出はとてもうまくいき,一応適当なアルゴリズムを考えてドライバーの評価が行われ,その評価が画面上で「★」の数で表示されるというところまで作りました.

ハッカソンということで時間がなく,正直 REST のレスポンスや保存時の処理などはドライバー一覧の取得以外わりとめちゃくちゃになっています.Python 側 (IoT 側) でステータスから成功か失敗か判断する実装をつけ忘れていたことに直前に気づき,デモの直前で「保存されると音がなる」という実装をサーバー側に実装して公開するなどのやっつけ仕事もしました.が,これが意外にチームに好評でデモもスムーズにいったので,美しく実装することがすべてではないのだなとも思いました.

ハッカソンで優勝して学んだこと

今回のハッカソンで優勝して学んだことは次です.

  • 高速でプロトタイプを作る必要がある際には,技術的にこだわってはいられないということ.
  • 変更に強いアーキテクチャであること.
  • プレゼンテーションめちゃくちゃ大事.

1つ1つ見ていきましょう.

高速でプロトタイプを作る必要がある際には,技術的にこだわってはいられないということ

ハッカソンでもっとも大きく評価に影響するのはプロトタイプのできであり,アイディアを具体的なプロトタイプにして示せなかったとしたらハッカソンの意味がありません.そして,ハッカソンは時間がありません.もっとも避けなければならないのは「プロトタイプができあがらない」ことなはずです.

エンジニアである以上,そしてハッカソンという日常業務から離れられる場である以上,どうしても「これまで使ってみたいと思っていた技術を使って作ってみよう」と思うのはわからなくはないのですが,しかしハッカソンで本気で優勝を狙うのであれば,むしろ「使い慣れた技術で」「精度の高い」プロトタイプを作ることが必要不可欠かなと思います.したがって,使い慣れた技術かつやり慣れた手順でプロトタイプ制作を行うことを強くおすすめします.

私も,正直 Rust でサーバーサイドを作ってもよかったです.が,Rust の Web フレームワークはそこまで使い慣れているということはなく,「時間が余計にかかってしまってやりたいことが実現できないかもしれない」と,仕事で使い慣れている Scala を選びました.

変更に強いアーキテクチャであること

ハッカソンで工夫できることはむしろこちらなのかなと思います.とにかく時間がないので,やることの大枠が決まった時点で手を動かし始める必要があります.そして,あとからやってくる仕様変更の波に耐えきることのできるような設計を行う必要があります.ある程度抽象的な実装を行っておいて拡張しやすいようにしておく必要があるなど,エンジニアとしての熟練度を求められます.

プレゼンテーションめちゃくちゃ大事

今回,優勝できた一番の要因はこれかなと思います.プレゼンテーションはとても重要です.とくに,自分たちが何を問題と置き,世の中では現状どのようになっていて,それを解決するためのプロダクトとして何を作ったかというストーリーを,わかりやすく伝える必要があります.

また,本番の前にたくさん練習を積むことも重要だなと改めて思いました.今回はプレゼンの神様が会場のスタッフやら参加者やらいろんな人を連れてきて,合計で10回くらいリハーサルも含めて練習しました.その結果,本番で想定される質問の洗い出しや,プロダクトの実演時の問題点などをたくさん洗い出して,高速で PDCA サイクルを回すことができました.これは本当に大きかったです.

作ったプロダクトは,うるさいくらいに主張しないと周りの人に伝わりません.「いいものだったらかならず周りの人がよさをわかってくれる」ということはありません.悪いものでもアピールをするといいものとして評価される場合もあるくらいです.改めてこのことを学んだなと思います*4

最後になりましたが,非常に楽しい3日間でした.GW もまたハッカソンがあるはずなので,興味のあるものがあったらぜひ出場してみようと思います.ディープラーニング系のものがあったら出てみたいですね.

*1:私が,技術的な話はあまり必要ないと思ったので押しませんでした

*2:その点では,PlayFramework を使えたらよかったなとは思いました.

*3:circe が case class を上手に解釈してくれなくて,若干変なレスポンスになってしまい妥協してもらいました.

*4:もちろん,私たちの作ったものはとてもいいプロダクトですよ笑.

clap, serde, rss を使って自分の好きなサイトのフィードを Rust で取得する

自分の好きなサイトのフィードを取得してリストアップしてくれるツールを簡単に作ってみました.ライブラリに関する知識を整理しておきたいので,記事として残しておきます.

f:id:yuk1tyd:20180321230809p:plain:w150
Rust で CLI ツール

先日,Rust が CLI ツールにも今後注力すると発表しました.それを受けて,せっかくなので clap を使って CLI ツールを作ってみようと思ったのが動機です.製作時間は約30分くらいでした.わざわざ JSON で返しているので,CLI ツールである必要はまったくありませんし.サーバー化して UI をつけるのもいいかもしれません.

実装自体は残念ながら個人用に作ったので,だいぶハードコーディングしています.スケールしません.サイトを追加するたびに自身でプログラムを書き換える必要があります(笑).

GitHub

github.com

実行とその結果

あらかじめフィードを取得したいサイトの情報を登録しておき,設定したサイトのキーを引数として与えると,JSON に包まれたタイトルとリンクが入った結果が返ってくるという仕様です.HackerNews と Redditはてなブックマークのホットエントリを出力できるようにしました *1

動かし方ですが,

cmdrss hacker

と入力すると,HackerNews の注目のエントリ一覧が返ってきます.

{"title":"Building a data informed product culture","link":"https://medium.com/@MyMonese/developing-a-data-informed-product-culture-ce4d74b007e4"}
{"title":"Cambridge Analytica and SCL – A Very British Coup","link":"http://www.bellacaledonia.org.uk/2018/03/20/scl-a-very-british-coup/"}
{"title":"The Next Russian Attack Will Be Far Worse Than Bots and Trolls","link":"https://lawfareblog.com/next-russian-attack-will-be-far-worse-bots-and-trolls"}
{"title":"Illegal Secrecy? The Prosecution of Phantom Secure","link":"https://lawfareblog.com/illegal-secrecy-prosecution-phantom-secure-and-its-implications-going-dark-debate"}
{"title":"Building Multi-Use Web Workers","link":"https://matthewphillips.info/programming/building-multi-use-workers.html"}
{"title":"Invoice as a service: generates PDF invoice from json","link":"https://github.com/samber/invoice-as-a-service"}
(...)

ちなみに, clap は便利で,項目を入力すれば --help 等を自動で生成してくれます.

cmdrss --help

とコマンドを打つと,

cmdrss 1.0
yuk1ty
A RSS reader runs on command line.

USAGE:
    cmdrss <feeds>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <feeds>    みたいサイト [possible values: hacker, reddit, hatena]

と返ってきます.自分で実装することもできますが,結構手間なので clap を使うと CLI ツールを作るのがちょっと楽になることがわかります.

使用したクレート

今回この CLI ツールを作成するにあたって,次のクレートを使用しました.rss 以外は,Rust でのデファクトスタンダードのようになっているクレートばかりです.目にすることも多いでしょう:

  • clap: CLI ツールを作成する際に使用します.提供される API に従うだけで,お手軽に CLI ツールを作成することができます.
  • serde, serde_json, serde_derive: 「Ser(ialize)/De(serialize)」の略ですかね.serde, serde_jsonJSON をパースする際に使用する基本的な機能を提供しています.serde_derive は,#[derive(Serialize, Deserialize)] のような文法を可能にしてくれます.大抵の場合,3つセットで使います.
  • rss: RSS フィードを取得する際に使用します.内部的には,reqwest を使って,対象の URL に対して GET をかけるという構造になっています.取得した内容は Channel という構造体に保有されます

サンプルコード

#[macro_use]
extern crate clap;
#[macro_use]
extern crate serde_derive;
extern crate rss;
extern crate serde;
extern crate serde_json;

use clap::{App, Arg};
use std::str::FromStr;
use rss::Channel;

fn main() {
    let arg_matches = App::new("cmdrss")
        .version("1.0")
        .about("A RSS reader runs on command line.")
        .author("yuk1ty")
        .arg(
            Arg::from_usage("<feeds> 'みたいサイト'")
                .possible_values(&["hacker", "reddit", "hatena"]),
        )
        .get_matches();

    let ty = value_t!(arg_matches, "feeds", Rss).unwrap_or_else(|e| e.exit());

    match ty {
        Rss::HackerNews(url) => print_list_items(create_list(&url)),
        Rss::Reddit(url) => print_list_items(create_list(&url)),
        Rss::HatenaBookMark(url) => print_list_items(create_list(&url)),
    };
}

enum Rss {
    HackerNews(String),
    Reddit(String),
    HatenaBookMark(String),
}

impl FromStr for Rss {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "hacker" => Ok(Rss::HackerNews("https://hnrss.org/newest".to_string())),
            "reddit" => Ok(Rss::Reddit(
                "https://www.reddit.com/r/programming/.rss".to_string(),
            )),
            "hatena" => Ok(Rss::HatenaBookMark(
                "http://b.hatena.ne.jp/hotentry/it.rss".to_string(),
            )),
            _ => Err("Sorry, not matched in this app."),
        }
    }
}

#[derive(Serialize, Debug)]
struct FeedItem {
    title: String,
    link: String,
}

fn print_list_items(items: Vec<FeedItem>) {
    items
        .iter()
        .for_each(|item| println!("{}", serde_json::to_string(&item).unwrap()))
}

fn create_list(url: &str) -> Vec<FeedItem> {
    let channel = Channel::from_url(url).unwrap();
    let items: Vec<FeedItem> = channel
        .items()
        .iter()
        .map(|item| FeedItem {
            title: item.title().unwrap().to_string(),
            link: item.link().unwrap().to_string(),
        })
        .collect();
    items
}

処理の流れは単純です.コマンドを叩くと,フィードにアクセスして情報を取りそれをパースします.結果を構造体に詰めたあと,JSON 形式でコマンドラインに書き出しするだけです.

コマンドを用意する

fn main() {
    let arg_matches = App::new("cmdrss")
        .version("1.0")
        .about("A RSS reader runs on command line.")
        .author("yuk1ty")
        .arg(
            Arg::from_usage("<feeds> 'みたいサイト'")
                .possible_values(&["hacker", "reddit", "hatena"]),
        )
        .get_matches();

    let ty = value_t!(arg_matches, "feeds", Rss).unwrap_or_else(|e| e.exit());

    match ty {
        Rss::HackerNews(url) => print_list_items(create_list(&url)),
        Rss::Reddit(url) => print_list_items(create_list(&url)),
        Rss::HatenaBookMark(url) => print_list_items(create_list(&url)),
    };
}

enum Rss {
    HackerNews(String),
    Reddit(String),
    HatenaBookMark(String),
}

App によってコマンドを定義し,細かいオプションをメソッドチェーンで定義するという形式です.ちなみに私は試していませんが,コマンドの定義情報自体は .yml ファイルに定義しておくことも可能なようです.便利ですね.

コマンドの実行情報自体は,get_matches() 関数が返す ArgMatchers を使って,最終的にはパターンマッチをして書くことになります.今回は enum を有効に使いたかったので clap が提供している value_t! マクロを使って enum の情報とリンクさせ,enum によるパターンマッチをできるようにしました.また今回はやりませんでしたが, cmdrss --site hacker のように引数を与えることもできます.

RSS を取得する

fn create_list(url: &str) -> Vec<FeedItem> {
    let channel = Channel::from_url(url).unwrap();
    let items: Vec<FeedItem> = channel
        .items()
        .iter()
        .map(|item| FeedItem {
            title: item.title().unwrap().to_string(),
            link: item.link().unwrap().to_string(),
        })
        .collect();
    items
}

Channel を使ってフィードの取得を行っています.

注意が必要なのですが,Channel::from_url を利用するためには,Cargo.toml にいつもとは違う設定を行う必要があります.featuresfrom_url を ON にする必要があるので,それを忘れないようにしましょう.

rss = { version = "1.0", features = ["from_url"] }

さて,Channel をそのまま to_string すると GET した結果がそのまま表示されてしまうので,取得したデータを加工する必要があります.今回は (あまり意味がないといえばないですが) FeedItem という構造体を作って,そこに記事のタイトルとリンクを詰め込みました.

そしてこの FeedItem なのですが,最終的には JSON で返したいなと思っていました.その際に何を使うのでしょうか?そうです,serde を利用するのです.ということで,最後に serde が解釈できるようにオプションをつけていきましょう.

JSON

#[derive(Serialize, Debug)]
struct FeedItem {
    title: String,
    link: String,
}

fn print_list_items(items: Vec<FeedItem>) {
    items
        .iter()
        .for_each(|item| println!("{}", serde_json::to_string(&item).unwrap()))
}

#[derive(Serialize)] を構造体に付与することによって,簡単な JSON 化を行うことができます.ドキュメント.Deserialize をつけると,JSON から構造体に変換してくれます.

serde_json::to_string を使うことで JSON から文字列に変えてくれます.特段ハマリポイントもありません.シンプルです.

まとめ

CLI ツールと言えば Python や Go のイメージが少々強く,実際 Rust の所有権や借用の解決の手間を考えると実装に時間がかかると思うかもしれません.

しかし,それは慣れ次第なのかなとも思います.私自身,簡単なツールを作る程度であれば Python で同等のものを作るときとさほど作業時間は変わらないように感じました.むしろ,コンパイラがさまざまなバグを拾い上げてくれるので,コンパイルさえ通ればプログラムは正しく動くという安心感があります.

今後 Rust によって注力されると発表されている分野なので,これからも Rust で CLI ツールを作っていきたいですね.

*1:ちなみに,RedditXML のパースに失敗して落ちます.なんでだろう.