Don't Repeat Yourself

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

『A PHILOSOPHY OF SOFTWARE DESIGN』 の1章〜3章を読んだ

こういう本好きなのと,話題になっていたので買ってみて真面目に読んでみています.

読みながら感想等々書いていくと頭に残りそうなのでやってみます.ただあんまり要約すると,それはそれでネタバレ感がすごいので続きは本書をご購入の上ご覧くださいということでお願いします.いい本です.

4章くらいまで読んだんですが,一旦1〜3章がひとつの意味上の区切りとしてピッタリそうなので,ここまでの3つをまとめておきます.

1章

本の導入なので割愛.英語の本って1章のイントロダクションで丁寧に本の全体像を解説してるので読みやすいですよね.

2章

ここで Complexity という本書のコアとなる概念が登場してきます.どう翻訳するのがベストかはわかりませんが,Complexity = 複雑性と翻訳させてください.

本書では,複雑性は次のように定義されます.

Complexity is anything related to the structure of software system that makes it hard to understand and modify the system.

「複雑性とは,システムを理解・変更することを困難にするソフトウェアシステムの構造に関連するものです」とのことです.要するに,コードは読めないし,コードがスパゲッティで修正が難しくなっていることを複雑性と呼んでいる,くらいの理解で読みすすめていくことにします.

後半で,複雑性の具体的な症例として次の3つが載っていました.

  • Change amplification: 変更箇所がめっちゃ増える,くらいの意味だと思います
  • Cognitive load: 直訳すると「認知の負荷」っていうことになるんでしょうけど,要するに「コードが読めん,わからん」ということです.
  • Unknown unknowns: あるコードを修正する際に,どの条件を満たしたらそのコードを修正しきったかが把握できない状況のことをいうそうです.

ソフトウェアが複雑になる要因が解説されたあと,複雑性は増え続けるんだよという説明がなされて2章は終わりました.

今回の本では,この複雑性という概念が肝になってくることを予感させる章でした.そして,この複雑性をいかに減らしていくかという方針について,本書の残りの部分が割かれている構成になっていそうですね.ソフトウェアデザインの方針は,何を目標にして設定すればよいのか迷いがちで,だからこそ「複雑性の抑制」に焦点を当てたのはとてもいいなと思います.

3章

動くコードを書けばOK,なんじゃなくて,ちゃんとシステム全体を見通したコードを書くようにしようねという話.

用語が2つ投入されているので紹介しておきます.

  • Tactical Programming (戦術的プログラミング): 動けば OK というプログラミングスタイル *1
  • Strategic Programming (戦略的プログラミング): working code isn't enough (コードは動くだけじゃダメだ).長期的な視野をもって,よいソフトウェアデザインを追求していこうというプログラミングスタイル.

Strategic Programming をしようね,という話です.せやな.あとは,Strategic Programming への投資効果について書かれています.

Strategic Programming する上ではマインドセットも必要とのことです.それはそうですね.Strategic Programming を一番効率よく推し進める方法は,エンジニアみんなでちょっとずついいデザインになるようにコツコツ投資することです.

ここまでで,本書で重要視することがコンパクトにまとめられていました.4章以降は具体的にどのようにしたら,複雑性を抑制でき,戦略的プログラミングをできるのだろうか?という話が書いてあるように見えます.読み終わったらまた,まとめと感想を書きます.

*1:working code is enough,という対になる文章をあてることもできるでしょう.「戦術的プログラミング」と翻訳するとどうも,「短期的な視野」感が失われる….

AWS Lambda の新機能 Custom Runtime を Rust でトライ

f:id:yuk1tyd:20181201203101j:plain

この記事は CyberAgent Developers Advent Calendar 2018 3日目の記事です.

アドテクスタジオ所属の yuki です.社内の方向けに軽く自己紹介をしておくと,2017/11 中途入社です.アドテクスタジオの某プロダクトでテックリードをしています.

Rust が好きなので,基本社内でも社外でも Rust の話しかしていません.Rust のゼミを最近同僚の方と一緒に立ち上げるなど,Scala や Go 言語の採用事例の多いアドテクスタジオ内で,Rust の市民権を得ようと (笑) がんばっています.

先日,ついに念願の待ちに待った, Rust によって記述された関数を AWS Lambda 上で実行可能になったというアナウンスがありました.Custom Runtime という機能です.実はこれまでにも Lambda 上で Rust を実行する方法はあったのです.が,完全に Rust サポートが達成されたので,これからユーザーは Custom Runtime を使っていくことになるでしょう.

ちなみにこれまで Rust では,

このように wasm を介して実行してみたり,あるいは

Go 言語のバイナリになりすましてみたりと,さまざまな方法で実行が試みられてきました.だがしかし!Rustacean はこれで,自身の大好きな Rust で Lambda を実行できるようになったのです!すばらしい世の中になりました.

話がそれてしまいましたが,まず新機能がどういった機能なのか,概要を説明していきます.つぎに,Rust がどのような言語かをご存知ない方も多いかと思いますので,Rust がどういった言語かについて説明します.そのあと,実際に Rust によって記述された関数を Lambda 上で動かしていくことにしましょう.

AWS Lambda Custom Runtime とは?

2018 年の re:Invent で発表された新機能です.要するにさまざまな言語で Lambda を動かすことができますよという環境です.

f:id:yuk1tyd:20181201200419p:plain
従来の言語に加えて,「独自のランタイムを使用する」という項目が追加されている

これを使用するためには,Lambda 上で動かすために必要な処理が書かれた bootstrap というバイナリファイルを用意し,それを zip に固めて従来どおりアップロードするだけです.

Rust とは?

f:id:yuk1tyd:20181201201252p:plain

Rust は近年注目度の高まってきている言語です.StackOverflow の愛され言語ランキングでもここ数年トップを取り続けています.大きな特徴としては次のような点があげられると思います.

所有権,借用,ライフタイム (Ownership, Borrowing, Lifetimes)

Rust の代名詞といってもいいかもしれません.

所有権やライフタイムという仕組みによる強力かつ安全なリソース管理が可能です.メモリだけでなく,ネットワークコネクションの管理も自動的に行います.安全性の満たせないプログラムはコンパイル時に拾い上げられます.

GC がないという特徴を聞いたことがある方もいるかもしれません.それはこの3つの概念によって,安全にメモリの確保・解放を管理しているがゆえに実現されています.メモリ管理に関して,プログラマが余計なコードを追加することはありません.

強力な型システム

型によるリソース管理が行われます.したがって,メモリ安全でない操作はコンパイル時に検出されます.他の言語ではランタイム時に解決されるような問題が,Rust においてはコンパイル時に解決されてしまいます.コンパイラが強い味方です.

また,強いて言うならば型クラス指向の言語で,それゆえに抽象化の力が強いです.抽象化の威力を存分に発揮した柔軟なソフトウェアデザインが可能です.型推論もほとんど完璧に行われるため *1,型を記述する場面はほとんどありません.

あらゆる箇所に型をつけていこうという強い意志が感じられます.またそれを利用した RustBelt のような定理証明系による支援プロジェクトも活発に行われています.強力な型システムに支えられた並行・並列処理への強さも魅力のうちのひとつです.

システムプログラミング言語である

システムプログラミング言語なので OS が作れます.「誰もがシステムプログラマーになれるように」というのが,次の Rust のキャッチフレーズに選ばれたとおり,Rust を使うことで誰もがシステムプログラマーになれます.

最高クラスのパフォーマンスとゼロコスト抽象化

もちろん時と場合によりますが,大抵のケースにおいて,かなり高速なプログラミング言語である Go 言語よりもさらに速くC++ とほぼ互角のパフォーマンスを発揮します.その要因のひとつは,安全性に重きをおきつつゼロコスト抽象化にも妥協しておらず,実行時の余計なオーバーヘッドが発生しないためです.ビビるくらい速い ("blazingly fast") と公式ドキュメントで謳っている通りです.

Cargo

Rust には最初からパッケージマネージャとビルドツールがついています.cargo です.cargo を使用するだけで,ライブラリの依存関係の解決やビルドをすべて cargo xxxコマンドラインで打つだけで実行できます.他言語にはよくあった「どのビルドツールがいいのか?」問題は,Rust では発生しません.cargo 一択だからです.

もちろん,cargo fmt と打つだけでフォーマッタも走ります.したがって,Rust ではフォーマット問題も発生しません.

温かいコミュニティ,ドキュメントの親切さ

Rust の最大の資産になりうるのは,私は温かいコミュニティだと思います.Rust コミュニティには,「Rust は難しい」という自覚が (おそらく) あり,入門者の方が少しでもスムーズに入門できるように,さまざまな工夫の凝らされたドキュメントが豊富に用意されています.OSS でもドキュメント,あるいはコード内のコメントや Spec 用のテストを丁寧に書く文化が醸成されており,初めて使うライブラリの使い方がわからない…という場面に遭遇することが少ないように思います.

Custom Runtime を使ってみる

ドキュメントに沿ってやってみようと思います*2.使用環境は Mac OS X です.エディタは CLion を使用しています.今回参考にしたドキュメントはこちら

toml の準備

サンプルプログラムでは次の crate を使用しますので,Cargo.toml に設定を追加します.

  • lambda_runtime: AWS 提供の Lambda Runtime が記述されたライブラリ.
  • serde, serde_json, serde_derive: Rust ではおなじみの JSON のパースを行うためのライブラリ.
  • log, simple_logger: ロギング機構.

また,Lambda に読み込ませるためにバイナリファイルの設定が若干必要なので,それも行いましょう.完成した Cargo.toml です.

[package]
name = "rust-lambda-testing"
version = "0.1.0"
authors = [{your author name}]

[dependencies]
lambda_runtime = "^0.1"
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"

[[bin]]
name = "bootstrap"
path = "src/main.rs"

クロスビルドのための準備

Lambda の環境に合うようにクロスビルドを行う必要があります.Mac の場合の設定方法は公式ドキュメントに載っていました.とても親切ですね.それに従って設定していきましょう.

まず,rustup ツールチェインに x86_64-unknown-linux-musl 向けの設定を追加します.

$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
 14.9 MiB /  14.9 MiB (100 %)   3.1 MiB/s ETA:   0 s                
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

次に,Mac であれば x86_64-unknown-linux-musl 向けのリンカを brew を経由して入れる必要があるので,入れておきます.

$ brew install filosottile/musl-cross/musl-cross

最後に .cargo/config に設定を追加しておきます.

mkdir .cargo

config ファイルを作成して,

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

と書いておきます.

これで準備は整いました.それでは,コードを軽く書いてビルドし,実際に動かしてみましょう.

コード

とりあえず動かしてみたいので,公式サンプルをそのまま使います.

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

use lambda::error::HandlerError;

use std::error::Error;

#[derive(Deserialize, Clone)]
struct CustomEvent {
    #[serde(rename = "firstName")]
    first_name: String,
}

#[derive(Serialize, Clone)]
struct CustomOutput {
    message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info)?;
    lambda!(my_handler);

    Ok(())
}

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty first name"));
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

ビルド & アップロード用の zip 生成

--release ビルドをしましょう.最適化が走り,Rust 本来の力を発揮できるためです.

cargo build --release --target x86_64-unknown-linux-musl

ところで私の環境でビルドすると,musl-gcc がないと言われました.先ほどインストールした brew の musl が認識されていなかったようです.次のようなエラーが出てしまい,ビルドに失敗しました.

--- stderr
thread 'main' panicked at '

Internal error occurred: Failed to find tool. Is `musl-gcc` installed?

', /[root dir name]/.cargo/registry/src/github.com-1ecc6299db9ec823/cc-1.0.25/src/lib.rs:2260:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

対策は公式ドキュメントにしれっと書いてありますが,

ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc

というリンクを設定しておくことで回避可能です.こうすると,ビルドが通ります.

次に Lambda アップロード用の zip ファイルを作ります.

zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

すると,このような zip ファイルが作成されるかと思います.これを Lambda にアップロードすることで,関数を実行することができます.

f:id:yuk1tyd:20181202165007p:plain
rust.zip が生成されました

実行

上記の zip ファイルを Lambda にアップロードします.その後,テスト関数に次のような JSON を入力し,テストを実行します.

{
  "firstName": "Rustacean"
}

まとめ

  • AWS Lambda Custom Runtime のおかげで,ついに Rust を Lambda で動かす環境が正式にサポートされました.
  • Rust のサンプルプログラムを作って遊びたい際に,Lambda 上で一度遊んでみるという選択肢が増えたという点でとてもすばらしいと思います.
  • もう少し重ためのプログラムを動かして,Rust の威力を存分に味わってみたいなと思いました→と思ったら,Rust の Advent Calendar の方ですでに計測してくださった方がいました.Go 言語と遜色ない性能がでているようですね.(追記あり) AWS Lambda が正式に Rust 対応したので KinesisFirehose にくっつけて性能計測した #rustlang #rust_jp - ソモサン

*1:Hindley-Milner ベースだが,ライフタイムのサポートがあるので完全なそれではありません.

*2:ここからは,Rust をすでに使用したことのある方を対象として書きすすめていきます.Rust のインストールなどは,Rust の公式サイトをご覧ください.また,詳しいプログラムの解説については,今回参考にした公式ドキュメントにかなり突っ込んで書いてありますので,そちらもあわせてご覧ください.

Shinjuku.rs で actix-web の話をしました (ちょっとした解説付き)

11/21 に Shinjuku.rs で登壇しました.今回は actix-web に関する話をしました.もちろん LT ではすべてを話しきることができませんでしたので,今回も裏話を記事にして書き留めておこうかと思います (自身の頭の整理にもなるためです).

forcia.connpass.com

基礎知識編

同期 I/O vs 非同期 I/O,ブロッキング I/O vs ノンブロッキング I/O

非同期 I/O については,ユーザー数の多いサービスや,あるいはアドテクなどではとくに意識することが多いのですが,案外他の種類のアプリケーションを作っている方には馴染みのない概念かもしれないと思い説明を加えました.ただ実はこれがすべてというわけではなく,他の対となる概念と比較対象しないとよさや何をしたいかがよくわからないものでもあります.なので,今回の記事では,非同期 I/O について学ぶ際に登場してくる (であろう) 4つの概念を一気に説明していきます.

まずこの話を始める際の前提として,2人の登場人物が存在します.

  • プロセス
  • OS

プロセスが OS に I/O タスクを投げます.このイメージを忘れないでください.

プロセスが OS にタスクを投げ,そのタスクに関するなんらかの返答を受け取る方式の種別が同期・非同期です.同期 I/O の場合は,OS に I/O タスクを投げ,入出力の準備ができたらアプリケーションから実データを受け取ります.非同期 I/O の場合には,OS に I/O タスクを投げ,入出力の準備ができたら通知を受け取ります.両者の違いはそこにあります.

プロセスが OS にタスクを投げ,そのタスクの結果をどのように受け取るかの種別がブロッキング・ノンブロッキングですブロッキング I/O の場合には,プロセスが依頼したタスクを OS が終えるまで,プロセスはその結果を待ちます.一方でノンブロッキング I/O に場合には,プロセスは依頼したタスクを OS がこなしている間,別の処理をこなすなどしてその返りを待ちません.OS から通知があった際に結果を受け取ります.

そして,2つの種別は掛け算になります.つまり,「同期・ブロッキング」「同期・ノンブロッキング」「非同期・ブロッキング」「非同期・ノンブロッキング」の4種類の方式が存在します.それら4つの方式は,それぞれ異なるシステムコールを使用します.表にすると下記のようになるでしょう:

ブロッキング ノンブロッキング
同期 read/write read/write (O_NONBLOCK)
非同期 select/poll (Linux なら epoll を使うなど) AIO

ここまでの話をまとめておきます.

  • 同期 I/O は OS にタスクを投げたあと,実データを受け取る.
  • 非同期 I/O は OS にタスクを投げたあと,通知を受け取る.
  • ブロッキング I/O はタスクを OS に依頼するとその結果を待つ.
  • ノンブロッキング I/O はタスクを依頼しても結果を待たず別の処理をこなす.OS から通知があって初めて結果を受け取る.
  • 同期・ブロッキングreadwrite というシステムコールを使用する.
  • 同期・ノンブロッキングは,readwrite というシステムコールを使用するが,O_NONBLOCK というフラグがついている.
  • 非同期・ブロッキングselectpoll というシステムコールを使用するが,効率が悪いので epollkqueue などを用いる.
  • 非同期・ノンブロッキングは AIO というシステムコールを用いると言われているが実装が成熟していないらしい.

並行処理と並列処理

これはいわゆる言葉の定義の問題になってきて,前提次第によってはさまざまな定義が出てきてしまうかもしれませんが,一般には次のように区別されることが多いです.

  • 並行処理: CPU数,コア数の限界を超えて,複数の仕事を行うこと (1コア内で複数処理を行うことをイメージするとわかりやすいです)
  • 並列処理: 複数の処理を同時に起こすことによって,効率よく仕事を行うこと

並行処理がわかりにくいと思うので簡単に解説します.簡単化のために,CPU を1コアだと仮定します.1コアだったとすると,たとえばブラウザを開きながら調べごとをしつつ, IntelliJ を使ってプログラミングを行うということは現実的に不可能なように思えます.が,これは並行処理を使うと仮定するとできます.なぜかというと,1コア CPU の中で,瞬間的にブラウザの処理と IntelliJ の処理を切り替えながら処理を行っているためです.

人間が気づかないくらい短い間隔で,複数の処理を切り替えながら実行しているのです.これが,並行処理が論理的に複数の処理を実行可能であるゆえんです.

並列処理はそうではなくて,物理的に同時に複数の処理を行います.つまり,そのまま複数人で複数の仕事を行うということです.なお,並列処理は並行処理の中に含まれます.

参考

いくつかあるとは思いますが,私は次の本に載っている内容で理解しました.

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

アクターモデルについて

アクターモデルに関する説明は,上述のスライドの中で十分かなとは思っています.Akka に関する説明ではありますが,次の記事が図も説明もとてもわかりやすいと思うので,イマイチイメージが掴めなかった方はぜひご覧ください.

enterprisegeeks.hatenablog.com

ところで,アクターモデルのいいところとは一体なんでしょうか?並行処理をする上で避けては通れない問題の中に,いわゆる競合状態というものがあります.アクターモデルは,その競合状態に対する有効な解決策として考案されました.

競合状態は銀行口座の例がよく出されるのでそれを使用して考えてみます.次の銀行口座をイメージした疑似コードをご覧ください.

もし預金残高が $100 以上あれば {
    預金残高 - $100
    $100 分のお金を引き出す
}

シングルタスクの状況下においては,上記のプログラムは常に正しく動くはずです.しかし,マルチタスクのは以下ではどうなるでしょうか.次のような状況を考えてみましょう.

(預金残高が $150 あるとします.まず,プログラム A が走っているものとします)
A: 預金残高が $100 以上あるか?→YES
(プログラムを切り替えます.プログラム B に切り替わります)
B: 預金残高が $100 以上あるか?→YES
B: 預金残高 - $100 して,$100 分のお金を引き出す
(預金残高は $50 になっています.ここで,プログラム A に切り替わります)
A: 預金残高 - $100 して,$100 分のお金を引き出す
(あれ…?すでに残高は $50 しかないのに,$100 引き出されてしまった〜!不整合発生!)

このような状況のことを競合状態 (race condition) と呼びます.並行処理で同期的な処理を求められる場合に起こることが多く,スレッドセーフでない実装という呼び方をしたりもします.

競合状態は,2つ以上の処理が変数を共有しているような場合に起こります.上述の例ですと,預金残高という変数を,プログラム A とプログラム B が共有しているために競合状態が発生しました.

よくある解決のアプローチは,Lock (あるいは Mutex や Semaphore) を用いる方法です.メモリ共有されている箇所にアクセスする際には必ずロックを用いてアクセスするようにするということです.Java などではこちらをよく用いますね.Rust でも,Mutex が用意されている通りです.しかしロックにはロック特有の問題があります.デッドロックです.他にもいろいろな問題点があります.

あるいは,Rust でも登場してきますが,メモリを共有するとしても書き換えを不可能にしてしまえばいいのではないか?というアプローチを取る場合もあります.イミュータブルなものを扱おうという思想です.最近では多くの言語にこの方法も取り入れられてきているように思います.

アクターモデルのアプローチはロックではなく,メモリをそもそも共有させないことでした.アクターはそのかわりにメッセージを送ることで解決をはかりました.メッセージを投げるだけ投げて,相手側の処理は非同期的に行わせ,最終的に終わったときに終わったメッセージを受け取ることで処理を完了するようにしました.

アクターモデルにデメリットはあるのか?という話ですが,なくはない (でも大体の場合は回避策が用意されているものです) ようです.私は正直概要をさらっと知っている程度です.次のスライドが参考になるかなと思いましたので,興味のある方はご覧ください.

https://techno-tanoc.github.io/ex_slide/#/

※ Elixir は Erlang VM の上に乗っている Ruby のような文法をもった新しい言語です.

tokio

この記事がとても詳しいです.私もこの記事で理解をしました.

Tokio internals: Understanding Rust's asynchronous I/O framework from the bottom up : Caffeinated Bitstream

tokio, mio, future の関係性なども完璧に説明されておりすばらしいです.

まとめ

同期・非同期,あるいは並行処理なのか並列処理なのかといった微妙な話題を取り扱ったので,簡単にまとめとして記事を書いてみた次第です.説明が足りない部分については,ぜひ私にダイレクトメッセージをください.正直この分野は私もまだ勉強中です.

actix-web の内部実装についても軽く見てみたのですが,結構興味深かったので後日あげようと思います.

リポジトリ

github.com

ライプニッツのモナド (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 アプリを作ったこともあるので,その話でもいいかもしれませんね.業務利用のチャンスはしばらくなさそうですが.

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