Ruby 3.0の未来へ―、言語設計者のまつもと氏が示す3つの方向性とRuby哲学

matz01

「Ruby 2が一般化した今、そろそろ新しいことを始めよう。これはRuby 3.0への扉を開く宣言です。未来へ備えよう。コミュニティとして死なないように、ワクワクする未来を妄想しよう。新しいことを始めよう」

こんな風に開発コミュニティに呼びかけたのは、世界的に人気の高いプログラミング言語「Ruby」(ルビー)の生みの親で言語設計者のまつもとゆきひろ氏だ。この「宣言」とも言える発言は、2014年9月に開催された「RubyKaigi 2014」で出てきたものだ。基調講演をしたまつもと氏は、講演の中で初めてRuby 3.0というメジャーバージョンへ言及して、次なるマイルストーンとして設定することを公の場で宣言した。

まつもと氏によれば、Rubyのコア開発者はこの何年かはRuby 2で忙しかったという。目の前にある現行バージョンのRuby 2をどうするかということや、機能の安定化、メモリ周辺の改善を行うのに集中していて、まつもと氏自身も未来のRubyを語るということを、ここ何年かしてこなかったのだという。

VM搭載に6年―、これまでのRubyの進化を振り返ると?

過去を振り返ると、2001年に米国で行われたRubyConf 2001の基調講演では、すでにRuby 2の構想について語っていたというまつもと氏。

「2001年当時、RubyでもVMを作ると言っていた。実際2001年にはVMを書いていたし、プロトタイプは1週間ぐらいで書いて動くには動いていた。ただ、プロトタイプではなくホントのRubyが動くようにするまでが大変。正直、私は挫折した(笑)」

Rubyはバージョン1.8系まではインタープリタ型となっていて、多くのモダンな言語が採用するVMを搭載していなかった。RubyがVM採用にいたるまで、Ruby 2の構想を語り初めた2001年から2007年のバージョン1.9リリースまで、6年ほどかかっている。

同様に、過去のカンファレンスでまつもと氏は毎年のように未来のアイデアについて語っているが、それらの実現が数年後、時には10年後ということもあった。

・ネイティブスレッド採用(2002年発表→2007年リリース)
・世代別GC(2002年発表→2013年リリース)
・キーワード引数(2003年発表→2013年リリース)

といったような具合だ。

構想を公表してから、実際にその機能を採用したRubyがリリースされるまで年単位で時間がかかっている。また、「メソッド可視性」や「traits」(mix)など、未実現の機能もある。実装するといって実際にリリースされなかったアイデアや機能は、全体の3分の1程度という。

機能の実装に年単位の時間がかかるのは何故?

プログラミング言語の機能実装に年単位の時間かかるのは何故なのか、ということへの回答は複数あり得そうだ。Javaのラムダ表記採用のように実装の方向性や仕様・文法の議論がまとまらないこともあるし、実装が複雑ということもあるだろう。

Rubyに関していえば、RubyKaigiの初日に登壇したHerokuの笹田耕一氏のによる基調講演、「Building the Ruby interpreter — What is easy and what is difficult?」(Rubyインタープリタを作る。何が簡単で何が難しいのか)に答えの1つがありそうだ。

sasada01

笹田氏は、Ruby 1.8系から1.9系への移行時に採用されて現在のバージョン2系でも使われているVMの「YARV」を開発した、言ってみればRubyの「エンジン」を作っている人物だ。Rubyの開発自体は昨年で20周年を迎えているが、笹田氏はRubyが大きく変化したバージョン1からバージョン2、そして今後変わるだろう2から3へかけてのコア部分を担うRuby開発チームの最重要人物の1人だ。

笹田氏が講演で繰り返したのは、アルゴリズムとしては良く研究されていて実験的な実装は1週間でできるようなことであっても、現に目の前にあって実用に供されている言語処理系に対して「使える実装」としていくことの大変さだ。まつもと氏が2001年に構想を発表し、「正直、私は挫折した」と語るVMを実装し、プロダクションでも使えるレベルのRubyに取り込んだのは笹田氏だ。

「VMとかJITとか言語処理系の開発をしているというと、スゴいって良く言われるけど、シンプルに作るだけだと実は簡単なんです。難しいのは人間がメンテできる状態で作って、最適化をはかること。Cとのインターオペラビリティを考えながらやるっていうのも凄く難しい」

「RubyのVMも10年前のお正月に1週間ぐらいで簡単なプログラムが動くまでは行きました。でも、ちゃんとProcを作るのとかはデータ構造とか大変。それでも1カ月もあれば時間で解決できるようになること。VMのコードを書いていくと、たくさん同じ内容を書いていくことになる。似たようなことをあちこちに書く。そこに性能改善を入れようとすると、バグが入り込みやすくて凄く大変。だからVM記述言語を作って、そこからCのコードを吐くようにして複雑性があがるのを防ぎました。VMのコード生成って、そんなに特殊なテクニックじゃないんですけど。まあやった、と」

世代別GCについても実装版をリリースするのに10年かかっているが、難しかったのは、矛盾なく実装することと、既存のコードと整合性を保つことだったという。

「世代別GCもアルゴリズム自体は簡単です。ただ、インタープリター全体を見渡して、ここで何をやらなきゃいけないっていうことをきちんとやるのが難しい。もう1つ、世代別GCで必要になるものにライト・バリアというのがある。もしライト・バリアを導入するとしたらCで書かれたソースコードの全部が対応しないといけない。でも、どう頑張ってもほかの人のソースコードはいじれないから、Rubyコミュニティが持つライブラリの全対応は現実的に無理なんです。だから解決策として、自分たちのアルゴリズムを開発して、これがうまく行ったということです」

処理系の開発をしていく中で、Ruby処理系自体の挙動がおかしくなることは日常茶飯事。いとも簡単にRuby自体が派手な壊れ方をすることがあるということを、笹田氏は冗談交じりにこう表現する。

「テストフレームワークがRubyで書いてある。だからRuby処理系に手を入れて、テストが全部走ったら、やったってなる。RSpecなんかでFが出て、何かのテストケースが1つでも失敗したら、ああ壊れたってみなさんいいますけど、そんなのぜんぜん壊れてないじゃんって(笑)。いや、すみません(苦笑)」

処理系の開発だとテストケースの実行すら最後までたどり着かない壊れ方をすることがあるという。

Ruby 3.0で取り組む3つの方向性

さて、まつもと氏は、プログラミング言語の進化に時間がかかるものという事情と、これまでの経緯を振り返りつつ、Ruby 3.0へ向けて新機能の実験や時代に則した変化を同時並行で進めていく必要があると指摘した。

「オープンソースコミュニティはサメのようなもの。泳ぎ続けていないと死ぬんです。そろそろRuby開発のコミュニティには燃料が必要かな、と。なので……」

そう話すと、おもむろに「Ruby 3.0」へ向けた以下の3つの方向性を示した。

・Concurrency(並行性)
・JIT(実行時コンパイル)
・Static typing(静的型付け)

これまでがそうであったように、「10年というスパンというかかるかもしれないし、もっと実現は早いかもしれない。考えはまとまっていない」としながらも構想の一部を説明した。

matz02

若者の間では人気? 静的型チェックをどうRubyに入れるのか

まつもと氏によれば、20世紀に生まれた言語、特にスクリプト言語には静的型付けのない言語の方が多い。Rubyはもちろん、PHPやPerl、JavaScriptなどは、みんな静的型付けがない。一方、最近出てきた言語であるScalaやDart、Goといったプログラミング言語は静的型付け言語に分類される。そして、Rubyとは色々な面で似た言語と言えるPythonでアノテーションを付ける文法の導入が検討されているなど、動的言語でも静的型付けの良さを取り入れる動きがあるという。

では、なぜ静的型付けが良いのか? まつもと氏は、次の3つの論点を挙げる。

・パフォーマンス
・ドキュメンテーション
・コンパイル時の型チェック

パフォーマンスのメリットについて、まつもと氏は懐疑的だ。

「速くなるのは嬉しい。誰も文句を言わないけど、ホントに静的型チェックが必要か? というと分からない。LuaJITやV8を見ると、動的型でも十分に速い。となると、実装の労力があれば、JITや特殊化というテクニックを使って高速化するのは十分に可能。だから、パフォーマンスのために静的型チェックが必要というのは正しい仮定ではない。思い込みに近い」

ドキュメンテーションというのは、ソースコードを読む際の補助情報としての型情報のことを指す。Rubyでは、メソッドが受け取るのが文字列か特定のオブジェクトなのかは、変数名から推測したり、コード本体を見ないと分からないケースがある。コメントに書かれていることもあるが、静的型付け言語では型情報が明示されていて、そのまま人間が見て分かる情報となる。

「これはコメントよりもいい。コンパイラが矛盾を検出してくれるので、ドキュメントとコードの矛盾が起こらない優れたドキュメンテーションだ。メソッドも引数さえ分かれば、コードの中を見なくても、型宣言を見るだけで使える」

別言語になってしまっては意味がない

次の論点はコンパイル時の型チェックだ。

「コンパイル時のチェック。これは嬉しいことが多い。多くのバグは型の矛盾を伴う。だから多くのバグを検出できる。引数の型や数をチェックするときや、リファクタリング時には型情報によって、間違いを早めに網羅的に検出できるのは嬉しい」

では、現在のRubyに静的型チェックがないのはなぜか。「なくても動いたからというのが最大の理由」と身も蓋もない歴史的事情を語るまつもと氏だが、同時に静的型を入れることによって柔軟性が減ることは心配しているという。特にRubyらしさの根幹ともいえる「ダックタイピング」との相性の悪さを指摘する。

「静的型チェックをやると、文字列といえば文字列クラスのオブジェクトしか取らなくなる。ノミナル・タイピングと呼ぶそうです。文字列と同じ振る舞いをするのに、文字列として渡せなくなる。過去の資産、Rubyのプログラミングの哲学や、実装の多くはダックタイピングにに依存している。これまで築き上げてきた、この美しい世界を壊すのは嬉しくないわけです(笑)」

さらに、型情報のある世界とない世界の接合が難しいことも指摘する。

「静的型を入れるとしてもオプショナルにするという選択もある。でもそうすると型のある世界からない世界に渡すと、型情報が落ちてしまう。失われる。つまり結局はごく一部しか型チェックが行われなくなる」

「一方TypeScriptというのがある。これはJavaScriptに徹底的に型を付けている。例外的に変数を動的にすることはできる。既存のJavaScriptライブラリは外から型を与えることができる。TypeScriptから見ると9割は型があると見える。なんだけど……、Rubyの世界でそれをやってしまうと、それはもう別言語ですよね。(型情報をプログラマが)全部書くなんてない」

なぜ、そこまで型を書くことを嫌うのか

明示的に型情報を書くことを嫌う、ということの根底にはRubyの哲学があるようだ。

「プログラムの本質は、コンピューターにどんな仕事をさせたいかにある。型っていうのは、この手続きはこういう構造のデータを必要としますという情報。それは本来、手続きを見れば、意図として表現されている。少なくとも私は表現したつもりがあるわけです」

「それを改めて、これは文字列です、これはこのクラスですって書けって言われると……、いや、もう言ったじゃん! コンピューター分かってよって思う。型は書きたくない。徹底的に型を書きたくないわけですよ(笑)」

まつもと氏は型情報を明示的にあちこちに書くことを「DRYじゃない」という。DRYの原則とは、「Don’t repeat youself」の略で、同じコードや似たようなコードを何度も複数あちこちに書くな(書きたくない=維持が大変)という主にプログラミングにおける設計原則のこと。機械的に推測可能なものであれば、明示的に型情報を書くというのはDRYの原則に反するという意味だ。

「宣言なしで、コードから、この式はこういう処理が行われてるので、こういう型を要求してるのに違いない。一種の型推論みたいなことをする。そういうベストエフォートの技術がある。その式のオブジェクトにどんなメソッドをダックタイピング的に型を推論するというのがソフトタイピングというんだそうです」

Rubyはメタプログラミングがやりやすく柔軟性が高い。そのぶん「行儀の悪いプログラム」が書ける。そうしたプログラムは処理系実装者泣かせの面がある。

「Rubyのフルセットに型推論を入れるのは無理。これまでサブセットという概念を導入してなかった。それがRubyで静的型チェックがはやらなかった理由です」

逆に、内部的にサブセットを用意することで、静的型付けのメリットと、柔軟性の「いいとこ取り」ができるのでは、とまつもと氏。

「プログラムのほうから意図を汲み取って、ご主人さまは、このようなことを意図されているのでしょうかとコンパイラが人間様にお伺いを立ててくる。ドキュメンテーションを生成したり、エディタやIDEが補完できたり、コンパイル時の型エラーについて警告を出せるようになる。

ソフトタイピングは、有効なサブセットを使っている間はコンパイル時の型チェックがあってハッピーだし、そうでなければ、今までどおりのRubyにフォールバックする。2つの言語が1つに入るようなイメージ。ソフトタイピングが有効なサブセットを使っている間はボーナスがある。ボーナスがあれば、(型情報を意識した書き方、もしくは行儀の悪くない書き方に多くのプログラマを)誘導していけるんじゃないか」

matz03

並列処理をRubyにどう入れていくのか

まつもと氏は静的型付けについて詳しく論じたが、並列処理についてはあまり言及がなかった。むしろ笹田氏のほうが詳しく論じた。

過去10年にわたってRubyの高速化に取り組んできた笹田氏は、並列実行をRubyに入れるだけなら簡単だという。

「単純に並列スレッドを提供するだけなら簡単なんです。私の博士論文の一部は、Rubyの上で並列スレッドを提供するもの。でもスレッドプログラミングって、ぶっちゃけ大変ですよね。大変だと、やっぱりRubyやっぱ良くないんじゃないのってことになる。だから良いプログラミングモデルか、デバッガが必要なんじゃないかと思っています」

「1995年に出た論文で、 Why threads are a bad idea (for most purposes) というのがあります。20年前の話ですけど、スレッドプログラミングはトップレベルのプログラマにしかできない、と。すべての状態をシェアしてるので、同期をしないといけない。それを1つでも忘れるとバグになる」

「人類が本当にスレッドプログラミングができるようになるかっていうと分からない。もうちょっと普通の人にもできるように、良い並列プログラミングモデルがあるといいなと思っています」

スレッドプログラミングでは、「データ競合」、「原子性違反」(atomicity violation:複数のスレッドから同時に状態を触ってしまう)、「オーダー違反」の3つが良くあるプログラマのミス。これに加えてデバッグの難しさもあるという。

「非決定的な挙動も問題です。バグの再現性が低いことがある。そのぐらいスレッドプログラミングは難しい。もしRubyに並列スレッドを入れてしまうと、こういう大変さを経験してRubyプログラミング自体が楽しくなくなる。Ruby体験はハッピーなものであってほしいという思いから、そのままは入れたくないなと思っています」

書きやすくて安全なもの、そして性能が出るものを

「ほかの言語を見ていると、いろんなデータモデル、実行モデルを提供している」と、現在広く知られたプログラミング言語で使われているアプローチを笹田氏は紹介した。ClojureやF#のようにSTM(Software Transactional Memory)を使うもの、ErlangやScaleのようにアクターモデルを取り入れたもの、GoのようにCSPを使うものなどが、抽象度の高い並列モデル・データモデルだろう。

これらのモデルを整理すると、「安全なコードしか書けないけど制約がある」というのと「自由に書けるけど安全じゃない」という軸があって、単純化すると性能と自由さはトレードオフの関係にある。例えば、関数型でデータを全てイミュータブルにすると状態を変更する処理が書きづらくなる。逆に最高性能を出すにはスレッドが速いと分かっていても、デバッグが大変だし、ミスがあるという状態だ。だから「書きやすくて安全なものをみんな探している状態」だと笹田氏はいう。

これは「freeとGCの関係にも似ている」という。C言語ではmalloc/freeを使ってプログラマが自分でメモリ管理を行う。しかし、プログラミングが煩雑になりミスも入り込みやすい。一方、メモリ管理を自動化するGCはプログラミングの労力を大きく省き、バグも減らすことができるが、パフォーマンスではfreeにはかなわない。freeが速いのはみんな分かっている。しかし、今ではほとんどのプログラミングでGCを使うほうが選ばれている。

これと似た議論で、いまはスレッドやロック機構が並行・並列プログラミングで使わわれているが、Rubyのような言語では「並列処理も安全にしか書けないものがいいんじゃないかなと思っている」と笹田氏は言う。例えば最近人気上昇中のGoは、かなり安全なほうだが、その気になれば共有メモリを複数スレッド(Goroutine)から触ることもできるし、たとえチャンネルによるメッセージキューを使った場合でも、オブジェクトのリファレンスを渡すことができてしまう。自分が何をやっているか良く分かったプログラマなら良いが、本当の意味で安全と言えない、というのが笹田氏の指摘だ。

いちばん簡単なのはRuby処理系をforkしてプロセス(状態)を分けてしまうこと。このアプローチで笹田氏は「MVM」という研究を大学でやったものの、いくつか問題があって実用に至っていないという。似たもので、小さ目のRuby処理系を単一プロセスの中で複数同時に動かすというアイデアもあって、これは「面白いかなと思っている」そうだ。

「オーナースレッド」というアイデアも

笹田氏は最近「オーナースレッド」というアイデアを温めているという。これは、すべてのオブジェクトが、その所持者であるオーナースレッドを持っていて、オーナースレッド以外からはオブジェクトにアクセスできなくするという一種のアクセス制限の仕組みだ。動的言語でオーナースレッドが使われた例はないそうだが、「これでスレッドプログラミングがうまくいくかもと考えてみたりしている」(笹田氏)。

ただ、現実的にはRubyではローカル変数へのアクセスがグローバルなアクセスを伴うこともあって、難しい課題があるようだ。オーナースレッドというアイデアについて、別途まつもと氏に話を聞くと、「あまりうまく行く感じがしない。でも、笹田くんがやりたいと言っているので、私の考えはあまり言わないようにしている」と、にこやかな回答だった。まつもと氏は、むしろErlangやScalaにあるようなアクターモデルを並列・並行プログラミングの本命の1つと考えているようだ。

sasada02「どのアプローチでやったとしても難しい。でも何とかならないか、安全なスレッド並列性を提供できないかなと思ってる。これはすごく面白いテーマなので、誰か一緒に考えてくれると嬉しいです」(笹田氏)

並列プログラミングのデバッグについては、「スレッドのデバッグツールや方法論は研究でいろいろ出てきている。データ競合を自動で検出するというのも研究では出てきているし、JRubyではアカデミックなカンファレンスで発表されたりもしている。OSのスケジューラをいじって、どこでコンテキストをスイッチしたかが分かるように再現するというアプローチもある」と、研究レベルでは参考にできるものが多くあるという。

トレードオフのバランスを取るとき、何を軸にするか

2人の講演で際立ったのは、「それをやってしまうとRubyではなくなる」というRubyの背後にある哲学の徹底ぶりだ。ただ単にRubyに並行性や静的型チェックを入れるということをすると、Rubyの本質を損なって別言語となってしまう。そこで、どうするのか、ということをアイデア段階で披露したのが両者の講演だった。

トレードオフというと、普通はメモリ使用量と実行速度というコンピューターサイエンス的なことを指すことが多い。高速化のためにソースコードを複雑にするとバグが入りやすく、メンテナスや機能拡張が難しくなるというエンジニアリング的なことを指すこともあるかもしれない。

しかし、RubyがRubyである理由は、恐らく従来あまり顧みられてこなかった、もう1つのトレードオフに最大限の労力を払っているからではないだろうか。それは人間(プログラマ)にとっての「書きやすさと読みやすさ」に徹底してこだわっているということだ。

プログラマの書きやすさを無視した言語は、厳密にいえばBrainf*ckなどの一部のジョーク言語以外に存在しないだろう。アセンブリ言語も含めて、すべてのプログラミング言語は人間に歩み寄っている。とはいえ、多くの言語は「このぐらいならプログラマがコンピューターに歩み寄るべきだ」という妥協を抱えているのが現実だ。専門家なのだから、歩み寄って当然だということだ。

コンピューターの都合を優先し、速度の犠牲を最小化したまま多くの機能を取り入れた高級言語としてC++がある。これが一方の「極」にある。その逆に人間側の都合を譲らず、分かりやすさや書きやすさに振り切ったもう一方の対極点にRubyがある。まつもと氏も笹田氏もそう見ている。そして過去10年でRuby開発チームが取り組んできたのは、人間側に振り切った地点にとどまりつつ、できる限りの性能を出すために工夫を繰り返すということだった。

笹田氏は、こう総括する。

「豊富な言語機能を使おうとすると、書きやすさや読みやすさが両立しなかった。その両立を発明したのがRubyの良いところ。性能が犠牲になっていたものを、解消しようとしてきたのがRubyの歴史」

「(Ruby言語の開発者となるには)どういうトレードオフがあるかを知るのが大事。こういう用途だと、こういうケースがある。別のケースだとこうだ、ということを知っておくのが重要なミッション。さらに重要なのは、トレードオフで、どちらか一方を犠牲にするというのではなく、どっちもよくなる道を探ること。それがエンジニア、あるいは研究者としての自分の役目だ」(笹田氏)。

Ruby 3.0の声を聞くのが何年後になるのか分からない。ただ、RubyがRubyらしさを保ち続けたまま、並列性や型チェックのメリットをどう取り入れていくのか。Rubyの今後の進化を楽しみにする開発者は多いことだろう。