どうも id:todays_mitsui です。この記事は はてなエンジニア Advent Calendar 2023 の23日目の記事です。
昨日は id:cateiru さん、明日は id:motemen さんです。
『達人プログラマー』という有名な書籍があって、そのなかで毎年少なくとも一つの言語を学習することを薦めています。
言語が異なると、同じ問題でも違った解決方法が採用されます。つまり、いくつかの異なったアプローチを学習するこにより、幅広い思考が可能になるわけです。これによって、ぬかるみにはまってしまう事態を避けられるようになります。
『達人プログラマー』第1章 達人の哲学 より引用
私事ではありますが今年はなんと2つの新しい言語を学ぶことができました。Perl と Rust です。
それぞれの言語から感じたおもしろい対比について書きます。
3行まとめ
- Perl は自由で hackable
- Rust は堅苦しくて教育的
Perl は自由で hackable
Perl を学んでいて驚いたのはオブジェクト指向を実現するための素朴な方法と、それを補うための多くのモジュールでした。
Perl において class はただの package で、bless $obj, 'Some::Pkg';
によって変数と package を結びつけます。
そしてメソッド呼び出し $obj->some_method($arg)
は Some::Pkg::some_method($obj, $arg)
の糖衣でしかありません。
package Rectangle { sub new { my ($class, $width, $height) = @_; my $self = { width => $width, height => $height }; return bless $self, $class; } sub area { my ($self) = @_; return $self->{width} * $self->{height}; } } package main { my $rect = Rectangle->new(3, 4); print 'width: ' . $rect->{width} . ', height: ' . $rect->{height} . "\n"; # => width: 3, height: 4 print 'area: ' . $rect->area . "\n"; # => 12 }
おお、なんとシンプル。私はこのシンプルさが好きです。
シンプルな言語機能とたくさんのモジュール
が、この素朴な仕組みだけで現実世界のコードを書こうと思うと DRY でなかったり private なプロパティが無かったり不足を感じる人も多いようです。
というわけでオブジェクト指向を実現する (アシストする) モジュールがたくさんあります。
- Moose
- Moo
- Object::Tiny
- Class::Accessor
- Class::Accessor::Fast
- Class::Accessor::Lite
- Class::Accessor::Lite::Lazy
- Class::Accessor::Typed
はい、
プロジェクトではこれらのモジュールを自由に選択して使うことが可能で。幸か不幸か class 毎に別のモジュールを選択して書くことも可能です。
なんというか、無駄に学習量が多いなと感じました。
たかがオブジェクト指向をやるために (デザインパターンをゴリゴリやっていこうとか言ってるわけでもないのに!)、これらのモジュールで できること/できないこと/読み方/書き方 を学ばないと始まらないとしたら、それは不毛だなと思います。
Perl は多くのことを許す
私はこれらのモジュールの乱立を批判的に見ているわけではなく「Perl って自由だなー」という感想を持っています。
ここで言う "自由" は "hackable" と読み替えることもできます。Perl はとても hackable な言語で、「こんな書き方できたらいいのに」と想像するものが大抵ユーザーランドで実装できてしまう柔軟性を持っています。
ここではオブジェクト指向まわりのモジュールを例に挙げましたが、他にも Perl の自由で柔軟な仕様を活かしたモジュールは多くあって。それらはときにマジカルに見えます。
そんな私のお気に入りは Exporter モジュールです。
モジュールの export の仕組み自体がモジュールになっている面白さ。そして Exporter モジュールを使わずに import メソッドの機構を直接利用すれば、これらの挙動をもっと魔改造できることにロマンを感じました。
Rust は堅苦しくて教育的
自由で hackable な Perl とは対照的に、Rust は何と云うか堅苦しいです。
所有権・借用システムがあり。その規則に反するコードは実行どころかコンパイルもさせてもらえません。
私はある程度のルールや制約はパズルとして取り組めるタイプなので楽しめました。が、分かりづらいコンパイルエラーで "なぜか動かない" という状態になることもありました。
例えば、ハッシュマップから1要素を取り出して返す関数を書いてみます。
fn take<T>(hash: HashMap<&str, T>, key: &str, default_value: T) -> T { match hash.get(key) { None => default_value, Some(value) => *value, } }
この関数は rust の所有権チェッカーによって検査され、以下のようなエラーになります。
// error: cannot move out of `*value` which is behind a shared reference fn take<T>(hash: HashMap<&str, T>, key: &str, default_value: T) -> T { match hash.get(key) { None => default_value, Some(value) => *value, // ^^^^^^ move occurs because `*value` has type `T`, which does not implement the `Copy` trait } }
「借用したものの所有権を他に渡してはいけない」というエラーが出ています。
この場合 *value
を返す代わりに value.clone()
してクローンした値を返してあげるといいんですが。value
がとても大きなデータだったらクローンするときにその分のメモリが使われしまいます。
Rust のコンパイルエラーは (元が間違っていたのでない限り) 回避できない
さてもっと上手いやりかたでクローンせずに所有権つきの値を返す方法はないのでしょうか?
実は在りません。
はい Rust を学習し始めた当初、この手のエラーをどうにか回避する書き方はないかと試行錯誤しましたが時間の無駄に終わりました。
この例における制約は「一つの値の所有権を持てるのは一つの変数だけ」というルールから派生しています。
Rust はこのルールに対しての抜け道は決して用意していません。用意されている選択肢は「所有権をあきらめて借用でガマンするか」「クローンの痛み(コスト)を受け入れるか」の2択です。 *1
この経験から私が学んだのは「コレクション型にランダムアクセスする場合、得られるのは借用だけ」というルールと。より高次の気づきとしての「Rust は痛み(コスト)を隠蔽したりしない」という事実です。
Rust がコンパイルエラーを出すときは「そう上手くやれない事情」がある
私が Rust は教育的と感じるのはこの部分においてです。
Rust の所有権・借用システムや型システムはコストやリスクを裏側に隠したりせず、むしろコストやリスクを表明します。
Rust がコンパイルエラーを出すときは何か「そのように上手くやれない事情」があるのです。
Rust を書こうと思うとスタックとヒープについて知らなければいけません。所有権についても知らなければいけません。
最初に知らなければいけないことが多いという意味で「初期の学習コストが高い」と嫌厭されることもありますが。遅かれ早かれ知るべきことを逐一指摘してくれているだけと見ることもできます。
Rust はコストやリスクを便利なライブラリの裏側に隠しておいて、いざ問題が起こったときに「そういうリスクがあることはドキュメントの隅に小さな文字で書いておいたでしょう?」と云うようないじわるな言語ではありません。もっと手前で、あなたがプログラムを実行するタイミングで眼に見える形で止めます。
私は Rust ほどユーザーを親切丁寧に教育してくれる言語を知りません。それは驚異的なことです。
まとめ
という訳で今年学んだ2つの言語 Perl と Rust についてつらつらと書いてきました。
何かを学ぶって本当にいいものですね。
私からは以上です。
おまけ: どのように学んだか
誰も興味無いと思いますが Perl と Rust をそれぞれどのように学んだかメモしておきます。
Perl
まずは本を2冊ほどザーッと読みました。
どちらも1週間ほどあれば無理なく読めるボリュームです。
抽象的すぎず具体的すぎず読みやすい内容だと思います。
Perl は言語仕様がシンプルなので、上記の2冊をザッと流せば無理なく読み書きできるようになるでしょう。
...というのは嘘で。Perl 世界はマジカルな文法とググラビリティの低い演算子で溢れています。
それらを読み解く足掛かりとしては ChatGPT を大いに利用しました。Google 検索では上手くヒットしない記号だらけの演算子についても ChatGPT はちゃんと教えてくれました。
ChatGPT を活用した Perl 学習サポートについては、勉強会で発表したときの資料をブログにあげています。
Rust
Rust についても本を読みました。
が、この本を読むだけで Rust を書けるようになるかと云うと厳しいと思います。本がダメなわけではなく、水泳の本を読んだだけで泳げるようにはならないというのに似ています。
泳ぎを学ぶにはやはり実際に水に触れて、勇気を出して頭まで潜ってみるのが大切で。Rust で云うと、実際にコードを書き進めないかぎり真の理解はえられないだろうなと思います。
私の場合は、自分が書いた既存のプログラムを Rust でリライトすることで Rust コードに触れました。
かつて自分が書いたコードなのでドメイン知識自体はすでに持っていて、Rust の文法やルールを守ることに集中しました。
加えて GitHub Copilot に大いに助けられました。
Rust はしっかりした型システムがあるおかげで Copilot との相性がとてもいいです。Copilot に補完されながらでもコードが書き進められれば自信が得られますし。補完されたコードを見て「え、そんな書き方できるんだ」と学びになることも多かったです。
オススメの書籍も一応あるので貼っておきます。
wasm をコンパイルターゲットとして Rust でゲームを作るという内容です。
まぁまぁ歯応えはありますが、とりあえず動くものを作りながら手を動かしつつ学びたい人にオススメです。
私からは以上です。
*1:Rc を使っても良さそうですが、Rc には参照カウントの痛み(コスト)がついてきます