関数型プログラマからみたRust

イントロ

私はRustをやる前にはCommon LispやSMLを主に使っていましたが、仕事ではScalaを使っていましたし他にもOCamlやSchemeやClojureやATS2やHaskellなどを書くこともありました。

私を含めた多くの関数型言語経験者人が一度は Rust for functional programmers を読んだことがあるかと思います。 このように関数型言語と比較して書かれるといかにも似た言語に見えるので私は興味を持ちました。そこで私は実際にRustに触れ始めたのです。

構文

let があるのでおよそOCamlなどに似ているという印象を受けました。 デフォルトでイミュータブルですしパターンマッチやシャドイングなもどするのでまさにMLのletそのものです。 行末にセミコロンが要求されますが、inの代わりだと思うことにして自分をなだめました。 式指向でreturnを書かなくて済むのも関数型言語らしさを感じました。

関数定義の構文は手続型言語風ですが手続き型言語に馴れた人に寄り添うために必要なんだと思って深くは気にしないことにしました。

一方、ジェネリクスの構文については憤りすら感じました。 どうして <> を採用したのか、そもそも比較演算子の <, >と紛らわしい上に<<の扱いでも困ることは目に見えていました。 私はML風に前置、あるいは少なくともHaskellのように関数と引数のように書くべきだと思いましたし、少なくともScalaのように型と値でカッコを分けるくらいはできるはずです。 しかしながら、不平を言っても始まらないので我慢して次に進むことにしました。

言語機能

トレイト、タプル、代数的データ型とマッチ式、高階関数、型推論。 これらは明らかに関数型言語から輸入されたものです。 これはとても喜ばしいことです、なぜなら今までしていた通りのプログラミングを新しい言語でもできるからです! 関数の引数と返り値に型宣言が必要なのはMLプログラマとしては少し残念でしたがHaskellやIdrisも同じようなスタイルなのでそういうものと自分で納得しました。 そして私はさっそく、倉庫番のゲームを作り始めました。あの、十分にシンプルで、コンソールで動かせて、楽しいゲームです。

しかしすぐにそううまい話はないと気付きます。 再帰的データ型を定義するとすぐにエラーになりますし(Boxが必要と気付くまでしばらくかかりました)、ハッシュテーブルを使うとすぐミュータブルが必要になります(ハッシュテーブルの操作が破壊的ということをすっかり忘れていました)。 そして何より所有権のせいで思うようにプログラムが書けませんでした。 関数型プログラミングではメモリは無尽蔵にあると思ってプログラミングするのでRustのパラダイムとはとてつもない隔りがありました。 最初は、所有権についてはCleanやGHCにある一意型、あるいはATS2の線形型のようなものだと思えばそう難しくないと踏んでいましたが、実際のところ何もかも思い通りにいきませんでした。 プログラム全てで所有権を意識するのが思いの外難しかったのです。 それでもコンパイル時にコードの安全性を検査出来るのは喜ばしいことだと思って、“ボローチェッカとの戦い"を数日続けました。

“ボローチェッカとの戦い"をしばらく続けて気付いたのは、コンパイラは理不尽な理由で怒ったりしていないということでした。 関数型言語ではデータほとんどイミュータブルなのでデータをコピーしたいのか参照したいのかあまり区別する必要がありませんでしたがRustでは厳密に区別しているだけのことでした。 むしろ、所有権の概念は、一度コンパイラの気持ちを理解してしまえば所有権や参照を意識せずにプログラミングするのが気持ち悪いとさえ感じるほどに自然でした。

それ以外の点ではトレイトや導出はMLプログラマからすると羨望の的でしたし(余談ですがRustの導出は昔はHaskell風に#[deriving(Show)]と書いていましたが、いつからか#[derive(Debug)]と書くようになったようです。)、 オブジェクト指向についても上手くやっていると感じました(関数型プログラミング言語でもオブジェクト指向はします!)。

Lispプログラマから一言あるとすれば、マクロです。RustのマクロはSchemeのsyntax-rules相当の機能しかなく、Common Lispプログラマの私にはもの足りませんでした。 macro_rulesでも無いよりはましですし、C言語のマクロよりは素晴らしいものです。 それでもどうしても伝統的なマクロが欲しくなりますが、不安定なコンパイラプラグインの機能しか選択肢がありません(訳注: 現在のRustにはproc_macroが入ったので伝統的なマクロも書けます)。

ツール

私の好きなEmacsのサポートはしっかりしていましたし、大きな不満はありませんでした。 REPLこそありませんがむしろSMLに比べると補完や定義ジャンプなどリッチな機能さえありました。

それよりも、ビルドツールについてです。 Rustは関数型プログラムで永遠のテーマだった分割コンパイルをするとインライン化が妨げられるが全プログラムコンパイルをするとコンパイル時間が掛かりすぎるという問題にモジュールより上位のクレートという単位を設けることで解決を見出したのです。 それに素晴らしきCargo。依存関係の地獄はありませんし、Makefileを書く必要もありません。パッケージインストーラがランタイムにまで侵入してくることもありません。 私達がプログラムを書くことに集中させてくれます。

まとめ

関数型プログラマがRustを使うと、所有権やミュータビリティ、メモリへの厳しさに最初は面くらうもののすぐに馴れて普段通りにプログラムを書けるようになります。 もちろん、Rustはむしろ手続き型な言語なので関数型プログラムそのまま、とはいきませんが普段使っている道具がそのまま使えるのでハードルは低いですし、むしろ手続き型プログラムに馴れた人が戸惑わないか心配にすらなります(初めてC++プログラマがRustをスラスラ書けると聞いたときには大変驚きました)。 Rustは関数型プログラマ、手続き型プログラマ問わず良い言語なのでみなさんも最初の一歩を踏み出しましょう!

メタ

私のブログは記事によって意識的に文体を変えているのですが、今回は新たな試みとして「海外でちょっと流行った記事の野良和訳」風に書いてみました。 なので海外風に「明らかに」などの強いワードを用いたり誤訳っぽいワードを埋めたいあまつさえ(訳注:〜)なんてものが出現しますが特に原文はないです。 案外疲れました。

Written by κeen