追記

Matzにっき


2013-07-31 [長年日記]

_ mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方

Twitterで質問を受けたので、 mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方 という解説を行った。が、1つ140文字のTwitterでの解説にはどうしても無理があるので、 こっちでまとめることにする。

まずは、Twitterの発言*1はこんな感じ。

arenaの目的。利用中のオブジェクトはGCに回収されないよう保護する必要がありますが、Cのスタックはポータブルに参照できません。そこでC関数実行中に生成したオブジェクトは全部「生きている」とみなす事で保守的に保護します。 yukihiro_matz 2013-07-31 08:16:45
arenaの目的(2)。この保護のためにオブジェクトを記録しておく領域がarenaです。mrubyではデフォルトで100個のオブジェクトを登録できます。 yukihiro_matz 2013-07-31 08:19:02
save/restoreの仕事。現状arenaのサイズは固定なのでC関数実行中にあまり沢山オブジェクトを生成するとarenaが溢れます。そこで沢山オブジェクトを生成する前後にsave/restoreを置くことでarenaのサイズを復元し、溢れを回避します yukihiro_matz 2013-07-31 08:27:02
save/restoreの使い方。オブジェクトを生成する領域の前後をsave/restoreで囲みます。ただし、囲まれた範囲内のオブジェクトが保護されなくなりますから、どうしても必要なものはrestore後mrb_gc_protect()で改めて保護してください。 yukihiro_matz 2013-07-31 08:33:61

これを再度まとめてみよう。

mrubyをCで拡張していると「arena overflow error」という「謎のエラー」に悩まされることがある。 これはmrubyで「保守的GC」を実現している「GC arena」という領域があふれたというエラーだ。

GC(ガーベージコレクター)は、オブジェクトがまだ「生きている」、 つまり、プログラムのどこかから参照されているかどうかを判定する必要がある。 これはルートと呼ばれる参照から直接・間接に参照可能かどうかで判別する。 ルートには、ローカル変数・グローバル変数・定数などが含まれる。

プログラムの実行がmruby VMの中でおさまっている時にはこれは問題ない。 VMの持つルートはすべてGCからアクセス可能だからだ。

問題はCで記述された関数を実行中の時。 Cの変数から参照されたオブジェクトも「生きている」わけだが、 mrubyのGCはCの変数の内容を感知できないので、 C変数からしか参照されていないオブジェクトは死んでいると誤解してしまう。

まだ生きているオブジェクトを回収してしまうのは、GCとしてもっともやってはいけないバグだ。

CRubyでは、Cのスタック領域を無理やりスキャンすることで、 Cの変数をルートとしてチェックしている。 もちろん、Cのスタックを単なるメモリ領域としてアクセスするわけだから、 それが整数を意味する値なのか、ポインタ値を意味する値なのか判別することはできない。 しかし、まあ、そこは「ポインタのように見える値は安全側に倒してポインタだと思って処理する」という 方針で処理している。この「安全側に倒す」というポリシーのことを「保守的」と呼ぶ。

され、このようなCRubyの「保守的GC」にはいくつか問題がある。

その最大のものは「移植性のある方法でスタック領域にアクセスする方法がない」ということだ。 つまり、移植性の高さを実現しようとするmrubyのような処理系では使えないってこと。

そこで、mrubyは別の方法で「保守的GC」を実現した。

問題なのは、C関数実行中に生成されたオブジェクトで、 Rubyの世界から参照されてないオブジェクトのうち、 C変数からは参照されているのでまだゴミ扱いしてはいけないものが存在する、 ということだ。

既に述べたようにCRubyは、Cスタックをスキャンしてゴミのように見えるがゴミでないものを保護している。

しかし、その方法が使えないmrubyは、より保守的なポリシーを採用した。 つまり、C関数実行中に生成されたオブジェクトは、極端に安全側に倒して、いっそ全部生きているとみなせば、 少なくともゴミでないものを回収してしまう問題は回避できるんじゃないかと。

この結果、本当はゴミであるものを回収できないので、若干効率が下がることになるが、 移植性が高いまま、保守的なGCを実現できることになる。 CRubyで時々発生する「最適化で参照が削除されてゴミでないのにGCされた」問題とも無縁になる。

このポリシーで、「C関数実行中に生成されたオブジェクト」を登録しておくテーブルが 「GC arena」である。arenaはスタック状になっていて、 C関数の実行が終わるとその間に登録されたオブジェクトはポップされる。

原則としてはこれだけで、普通の場合は、これでめでたしめでたしなのだが、 GC arenaの存在は別の問題を引き起こすことがある。 これが前述した「arena overflow error」だ。

メモリが少ない環境でも動作することを考慮したmrubyはarenaのサイズを固定長にしており、 しかも、そのサイズはデフォルトで100とかなり小さめに設定されている。

実は当初はサイズ1000とかちょっと大きめにしていたのだが、 このテーブルサイズが厳しい環境があったのと、 後述するようなテクニックを使い、適切にarenaを管理すれば100でも普通に動くので、 現状は100にしている。

その結果、C関数の実行中に数多くのオブジェクトを生成すると、 arenaがあふれることになる。

その対策に用いるのが、表題のmrb_gc_arena_save()とmrb_gc_arena_restore()というふたつの関数である。

int mrb_gc_arena_save(mrb)はGC arenaの現在のスタック位置を返し、 void mrb_gc_arena_restore(mrb, idx)はarenaのスタック位置を保存された位置に戻す。

int arena_idx = mrb_gc_arena_save(mrb);

...なんかオブジェクトを作る処理...
mrb_gc_arena_restore(mrb, arena_idx);

というような使い方をする。

もともとのC関数の実行は、このようにsave/restoreに囲まれているのだが、 一時的にオブジェクトを作り、その後は不要になる領域を 明示的にsave/restoreを囲むことにより、arena overflowを避けるわけだ。

とはいうものの、具体例を見ないとわからないケースもあるだろう。 ここでは、Array#inspectのソースを見てみよう。

static mrb_value
inspect_ary(mrb_state *mrb, mrb_value ary, mrb_value list)
{
  mrb_int i;
  mrb_value s, arystr;
  char head[] = { '[' };
  char sep[] = { ',', ' ' };
  char tail[] = { ']' };

  /* check recursive */
  for(i=0; i<RARRAY_LEN(list); i++) {
    if (mrb_obj_equal(mrb, ary, RARRAY_PTR(list)[i])) {
      return mrb_str_new(mrb, "[...]", 5);
    }
  }

  mrb_ary_push(mrb, list, ary);

  arystr = mrb_str_buf_new(mrb, 64);
  mrb_str_buf_cat(mrb, arystr, head, sizeof(head));

  for(i=0; i<RARRAY_LEN(ary); i++) {
    int ai = mrb_gc_arena_save(mrb);

    if (i > 0) {
      mrb_str_buf_cat(mrb, arystr, sep, sizeof(sep));
    }
    if (mrb_array_p(RARRAY_PTR(ary)[i])) {
      s = inspect_ary(mrb, RARRAY_PTR(ary)[i], list);
    }
    else {
      s = mrb_inspect(mrb, RARRAY_PTR(ary)[i]);
    }
    mrb_str_buf_cat(mrb, arystr, RSTRING_PTR(s), RSTRING_LEN(s));
    mrb_gc_arena_restore(mrb, ai);
  }

  mrb_str_buf_cat(mrb, arystr, tail, sizeof(tail));
  mrb_ary_pop(mrb, list);

  return arystr;
}

実際のコードをそのまま引用してきたので、若干複雑になっているが、 Array#inspectの処理の本質は、 配列の各要素をinspectメソッドを用いて文字列化した上で、 それらを結合して配列全体のinspect表現を作ることにある。

全体のinspect表現の文字列を作ってしまえば、 処理途中に生成した各要素の文字列はもはや不要になる。 ということは、GC arenaにこれらのオブジェクトを登録しておく必要もない、ということだ。

そこで、ary_inspect()関数では、

  • mrb_gc_arena_save()でインデックスを保存
  • 要素のinspect表現文字列を取得
  • 生成中の配列inspect表現文字列に結合
  • mrb_gc_arena_restore()でインデックスを復旧

という手順により、arena領域の消費を抑えている。

ここで注意すべき点は、最終結果となる配列inspect表現となる文字列は、 mrb_gc_arena_save()の呼び出しよりも前に生成していることである。 こうしないと、必要なオブジェクトがGCで回収されてしまうことになる。

処理のパターンとしては、さまざまな一時オブジェクトを生成した上で、 そのうちの一部だけを参照し続けるというケースも考えられる。 このようなケースでは、ary_inspect()のように既存のオブジェクトに結合するような手は 使えないので、mrb_gc_arena_restore()の呼び出しの後に mrb_gc_protect(mrb, obj)を呼び出して、そのオブジェクトをarenaに再登録する必要がある。 ただし、mrb_gc_protect()は注意して用いないと、これ自身がarena overflow errorの原因になることがあるので 注意するように。

などということを、Twitterだけで説明するのは現実的じゃないよなあ。

追記

あ、そうそう。

このmrubyのAPIには、改善が必要だなと思ってる点があって、 その最大のものは、トップレベルでmrb_funcall()を呼ぶと GC arenaに戻り値が登録されるのでそのうちarena overflow errorになることだ。

戻り値を使わないmrb_funcall()のようなものを用意すればいいんだと思うんだけど。 いい名前が思いつかないんだよなあ。

*1  tDiaryにTwitterの発言を引用するプラグインが欲しいなあ

本日のツッコミ(全1件) [ツッコミを入れる]

_ おんがえし [つい最近そのmrb_funcall()の問題ではまってarena_saveとarena_restoreで囲んで対処し..]

[]

2013-06-12 「ちょっと待った!小中学校でのプログラミング教育」 [長年日記]

先日、Webronza というところに寄稿したのだが、有料登録しないと後半が読めなくなっていた。で、交渉して公開許可を頂いたので、ここで全文掲載。

「ちょっと待った!小中学校でのプログラミング教育」

現代社会はもはやコンピュータがなければ成り立ちません。そして、コンピュータは誰かが作ってソフトウェアがなければ、まったく役に立ちません。コンピュータは自発的に仕事をしてくれないどころか、誰か人間がソフトウェアという形でどのように仕事をすれば良いのか教えてやらなければ、なんの働きもできないのです。コンピュータが社会に役に立っているのは、ソフトウェアがあるからです。

どんなに賢いように感じられるコンピュータでも、自らソフトウェアを開発することはできません。コンピュータは単純な計算をものすごく速く行うことができますし、それを積み重ねることで人間を越える能力を備えていますが、その一方で、なにか新しいことを創りだすなどの創造的な活動は苦手です。はっきりいうとまったくダメだと言ってもいいでしょう。当面の間は人間がソフトウェアを作って、コンピュータに仕事を教えてやるしかないのです。

社会におけるコンピュータの重要性は明らかで、そのコンピュータがソフトウェアがなければ役に立たず、そのソフトウェアは人間にしか作れないとなれば、ソフトウェアを開発する人間こそが真に重要だということになります。しかし、現状、誰でもがソフトウェアを開発することができない以上、ソフトウェアを開発する人、プログラマを養成することは急務です。とは言うものの、どのような教育を行えば、優秀なプログラマを育成できるのかについて誰でもが合意できる方法はまだ見つかっていないようです。

最近、注目されているのが若い人たち、特に小学生や中学生へのプログラミング教育です。確かに優秀なプログラマは若い頃からその才能を発揮している人が多いようです。また、プログラミング能力はあまり年齢に関係ないようで、天才と呼ばれる小学生プログラマがいると思えば、70歳をはるかに超えて現役で活躍する方もいらっしゃいます。

そこで、若いプログラマを育てるために、小学校や中学校での情報処理の教育やプログラミング教育に力を入れようという動きもあるようです。しかし、自分自身のプログラマとしての経験から考えると、これにはなかなか困難がつきまとうように思えます。第一の課題は「誰が教えるか」という点です。学校をプログラミング教育の現場にするためには、当然のことながらプログラミングを教える教師が必要です。しかし、現在の小学校・中学校の教員でプログラミングの能力を持つ人はごく少数でしょう。もちろん、教科書通りに教えることができる人は短期間で用意できるかもしれませんが、それでは子供たちにプログラミングに前向きな気持ちを伝えることは困難でしょう。中学生時代にプログラミングをはじめた私自身も含めて、若い頃からプログラミングに「はまった」人たちは、結局、コンピュータを使いこなすのが楽しいからこの道に進んだようなもので、教科書に書いてあるから、あるいは学校の授業だからという理由でプログラミングをはじめた人など見たことがありません。プログラミングを教えるというのであれば、少なくとも教える人はプログラミングの楽しさを自覚している人でなければ成果をあげられないと思いますし、そのような人をそれぞれの学校に配備するのは大変な困難ではないかと思います。

第二の課題は「どのように評価するか」ということです。学校の授業であるということは、なんらかの評価をする必要があるわけですが、これがまた困難です。まず、プログラミングの能力は、創造性をともなうという点である種芸術に似ています。そういう点では美術の授業に似ているのですが、問題はプログラミングの場合、人によっては非常に短期間で上達するため、小学生であってもプロを越える作品を完成させることがたびたびあることです。人によって出来栄えが10倍、100倍あるいは1000倍も違うようなものを学校の成績としてどのように評価したらよいのか途方にくれます。

第三の課題は「なにを教えるか」ですが、この点は最初のふたつの課題に比べればなきに等しいものです。

では、学校教育はプログラマ養成にふさわしくないのであれば、いったいどうすれば未来を担うプログラマを養成することができるでしょうか。

正しい答えは私自身も持っていないのですが、私の知っている優秀なプログラマのほとんどが自学自習でプログラミング能力を身につけている現実を考えると、あるいはプログラマとは、養成されるものではなく、発見されるものなのかもしれません。

ある調査によれば、大学におけるプログラミング教育の受講者の内6割程度はプログラミングの基本的な部分の理解に困難を感じ、それは他の学科の成績に必ずしも相関しなかったのだそうです。だとすると、世の中にはプログラミングに向いた性格の人がいて、成績その他では区別することができないことを意味します。であるならば、少しでもたくさんに人にプログラミングに触れる機会を与え、そしてそれに興味を持てた人、才能の片鱗を見せた人にはより豊かな機会を与えるようなプロジェクトを総合的に設計することで、未来のプログラマを「発掘」できるのかもしれません。そして、そのような才能あふれる人たちに適切な待遇を与えることが、このIT社会の競争力を強める最大の方策なのかもしれません。

本日のツッコミ(全21件) [ツッコミを入れる]

Before...

_ 大学生 [僕の高校では、特別なカリキュラムが組まれていて、ある日2日連続で、すごいプログラマーの人が来て講師をしてくださり、そ..]

_ pmint [授業外で活用できる、普段でも(アプリの利用で)触れる、アートとしてでなくても評価できるなどの点から、英語や美術よりも..]

_ 宮松利博 [ゲームばかりして、なかなか勉強しない小学生の甥に、ゲームを使う側から、作る側になってみては?とパソコンと一緒に一冊の..]

_ (´ε`;)ウーン… [いいことだとは思うけど、 例えばゲームプログラミングであれば今は中間ライブラリなしに本格的なものは作れないし、そう..]

_ 千代子 [MatzさんやIT企業の人からみると、もっと優秀なプログラマは必要だ!というのはわかりますが、3児の親としての立場か..]

_ (´ε`;)ウーン… [あ、でも プログラ授業に反対意見ってわけじゃないです。 どうせ今の最低な学校教育なるものが国を滅ぼすのは時間の問題..]

_ 千代子 [2回目です。上の投稿は脱字がありました。「もっと大事で優先させたいことがあるのが本音です」に訂正します。小学校での情..]

_ 参考に [>私は元々プログラミング教育が前提にしている「教育によって才能のない人が優秀なプログラマになることができる」という仮..]

_ (・(ェ)・) [>第一の課題は「誰が教えるか」という点です。学校をプログラミング教育の現場にするためには、当然のことながらプログラミ..]

_ Tori [ものづくりの点では別に単一のプログラミング言語による習得は重要ではないと思います。特に子供の頃にC言語を覚えても創造..]

本日のTrackBacks(全1件) []

_ GUNMA GIS GEEK:プログラミング教育はプログラマーの育成を目的としなくていい ちょっと待った!小中学校でのプログラミング教育 小中学校での..


2011-09-29 [長年日記]

_ RubyConf 2011 New Orleans

一年ぶりの更新か。「年刊Matzにっき」だな。

今年もニューオーリンズで開催されたRubyConf。同じ都市で二度開催は初めて。

で、しょっぱなが私のキーノート。

まあ、あんまり語ることはないので。スライドを見てもらおうか。

こんな感じ。

角谷さんオススメのspeakerdeck.comも使ってみた。

[]

2010-11-14 [長年日記]

_ [Ruby] RubyConf 2010 キーノート(3)

で、最後に「Diversity(多様性)」である。

初日のDave Thomasのキーノートでも3つの重要なこととして、 Diversifyをあげていた。多様性は重要なのである。

とはいえ、多様性はいいことばかりでもない。 Rubyにおける多様性といえば、昨今数々登場している別実装である。

  • CRuby (1.9, 1.8)
  • JRuby
  • Rubinius
  • IronRuby
  • MacRuby
  • MagLev

などなど、多くのRuby実装がある。これら以外にも「Rubyっぽい言語」まで含めると 本当にいくつあるのか見当もつかない。

昔はPythonの人たちに「Ruby(とPerl)は複雑すぎて、別実装は登場しそうにない。 Pythonを見てみろ、CPythonとJytonとIronPythonがある」などと言われたものだが、 今や別実装の数ではRubyの方がしのいでいる。Pythonの方もPyPyとか新しいものも登場してるが。

が、一方、このような多様性にはコストがかかる。 まあ、プラットフォームの違うJRubyはおいておくとしても、 CRubyとRubiniusとMacRubyで分散しているリソースを集約すれば、 もっと早く言語(実装)が進歩するような気もしないでもない。

しかし、オープンソースプロジェクトでは結局はそれぞれの参加者がやりたいように関わって モチベーションを維持することの方がはるかに重要だ。

多様性は善で、コストは必要経費であるというのが私の認識である。

さて、多様性は善であるので、さらにそれを豊かにするために 私自ら新たな処理系を送り込もうと思う。

それは RiteVM であり、組み込みなど小規模なデバイス向けをターゲットとした処理系である。

現在のRubyは元々Unixをベースにして開発されたものであり、 UnixやPOSIX APIを提供しないような小さなデバイスや、 アプリケーションへの組み込みなどはあまり重視されてこなかった。

しかし、一方、組み込み分野などではデバイス性能の向上で ソフトウェアの比重が高まりつつあり、 実行速度やリアルタイム性がそれほど要求されない分野で Rubyのような「高級」な言語を使いたいという要求はそれなりにあるようだ。

そこで、以下のような処理系を新規に開発する

  • 小規模なRuby言語処理系
  • 機能はほぼRuby標準規格を満たす最小限。 さらにファイルI/Oなどは取り除く(後で追加可能)
  • 実装はLuaを意識
  • VM構造体を提供。グローバル変数を使わない
  • 機能はコンポーネントとして提供。必要に応じて外せる。 たとえばコンパイラを外してevalもなし、などが実現できる
  • ターゲットの要求に合わせてコンパイル時にさまざまに設定可能。 たとえば浮動小数点数をdoubleにするかfloatにするか、 Fixnumをintにするか、longにするか、long longにするか、など。 逆にいうと「どこでも同じように動く」は追求しない
  • 浮動小数点数はimmediate(shootout対策w)。
  • VMは32ビットワード命令、レジスタベース。命令セットもLuaに類似。
  • GCはレイテンシを重視。インクリメンタルアルゴリズムを採用。 世代別についても検討したい

このVM開発のプロジェクトコードネームは Rite と称することにする。 RiteVMの開発は、経済産業省 平成22年度「地域イノベーション創出研究開発事業」の一環として行われる。

これにより、たとえば

  • ゲームの組み込みスクリプト
  • デジタル家電のソフトウェア開発
  • 組み込み機器やロボットの非リアルタイム制御

などにRuby(のサブセット)が使えるようにする。

もちろん、今すぐにそうなるとは思っていないが、 今から準備すれば数年後には現実にできると考えている(ビジネス上の成功は別だけど)。

と、ぶちあげてしまったので、もう後に引けなくなったな。

FAQ

いつ Rite が使えるようになるの?
わかりません。が、上述の通り、経済産業省の事業なので事業が終了する2011年度末には なんらかの成果をあげないと、いろいろまずいことに...。
RiteVMはオープンソースになるの?
はい。MITライセンスを考えています。ただし、今回(とその周辺)の開発で ビジネスモデルを確立する必要があるので、あるいはGPL+商用ライセンスの MySQLモデルになるかもしれません。 いずれにせよ、オープンソースになることは期待して良いでしょう。
RiteVMが登場したらCRubyはお蔵入り?
そんなことはありません。Riteは分野限定の小規模実装ですから、 フル機能実装であるCRubyの代替にはならないでしょう。
C APIはどうなるの?
今までのCRubyのAPIとは異なるものになります。CRubyのAPIは非常に使いやすいけれども 組み込みにはあまり向かないからです。技術的には不可能ではなさそうですが、 互換レイヤーを導入する予定は当面ありません。
RiteVMはM17Nを提供しますか?
いいえ、コンパイル時にASCIIかUTF-8を選択してください。 エンコード変換機能も(標準では)提供しません。
native threadをサポートしますか?
いいえ、RiteVMそのものはスレッドをサポートしません。 native threadを活用したい時には各スレッドにVMを割り当ててください。 fiberはVMで提供するかもしれませんが、予定は未定です。
RiteVMはYARV/JRuby/Rubiniusより高い性能が出ますか?
たぶんそんなに性能は出ません。ただし、Floatの即値化とかのおかげで いくつかのベンチマークでは良い性能がでるかもしれません。
RiteVMの開発に貢献できますか?
オープンソース化されるまで待ってください。 githubで公開されると思います。gitの勉強しないとな
Riteって聞いたことあるんだけど
2003年のRubyConfキーノートでRuby 2.0の実装コードネームとして提案した名称です。 結局、新しいVMはYARVが採用されたわけですが、まさかこんな形で復活するとは。
まつもとさんはCRubyの開発から引退するんですか?
今でも十分貢献できてないんですが、引退するつもりはありません。 CRubyはRubyの第一実装であり続けますし、 私もRubyの設計者であり、コミュニティーのリーダーであり続けます。
本日のツッコミ(全5件) [ツッコミを入れる]

_ ささだ [RiteVM を (C)Ruby 2.0 のベースにしたほうが、色々幸せになれそうな気がします。性能とかも。]

_ とりまん [ゲームに組み込むならバインダー周りの整備も期待したいです。]

_ まつもと [バインダーってなんでしょう? swigとかtoluaのようなもの?]

_ morita [たぶんそうでしょうね。< バインダ IDL をつかわずテンプレートでちょこっと細工すると C++ のネイティブメソッ..]

_ yachi [組込み、期待してます!スクリプトの再利用、機器間移動、クロス開発などが容易なようにプロファイルみたいな仕組みもできる..]

[]

2010-11-13 [長年日記]

_ [Ruby] RubyConf 2010 キーノート(2)

前回に続いて 未来(≒Ruby 2.0)の話を。

今回、紹介した「未来」の機能は以下の通り。

  • Traits
  • Method Combination
  • Keyword arguments
  • Namespaces

今まで話してきたことじゃん、と思うでしょうが、その通り。 違いは

  • これらの機能が単なるアイディアではなく、どのように実装すべきかほぼ見えている
  • 実装した機能を突っ込む場所(trunk)が明確になっている

点です。特に後者は大きい。

Traits

Traitsの定義は

a trait is a collection of methods, used as a "simple conceptual model for structuring object oriented programs".

from Wikipedia (en)

ということで、モジュールとほぼ同じようなものです。 実際、今回導入するTraitsは言語要素の実体としてはモジュールを利用します。

ただ、モジュールの機能を取り込むのにincludeではない 別のやり方(mix)を導入することによって、includeが持ついくつかの問題を解消しよう というものです。includeの方が便利なこともあるので、includeもなくなりません。

includeの問題は

  • 名前の重複を検出できないこと
  • モジュールのinclude関係が後から変化した場合の一貫性のない挙動
  • メソッドを後から修飾する(wrapする)方法が提供されない

ことです。

擬似的な多重継承であるincludeは、 includeされたモジュールが継承ライン(ancestors)に含まれるようになります。 この時、状況によっては予測困難なことが発生します。

ひとつはincludeされた複数のモジュールで同名のメソッドが定義されていた場合、 その重複が意図されたもの(override)か、偶然か(conflict)か、 区別する手段がないところです(名称重複問題)。

もうひとつは、いくつかの状況で継承ラインに並ぶモジュールの順序が予測しがたい (ので、メソッド名の重複時にどれが優先になるのか直感的でない)ことです。

module American
  attr_accessor :address
end
module Japanese
  attr_accessor :address
end
class JapaneseAmerican
  include American
  include Japanese
end
JapaneseAmerican.new.address
# which address?
p JapaneseAmerican.ancestors
# => [JapaneseAmerican, Japanese, American, Object, Kernel]

この例ではaddressという属性(メソッド)がAmericanとJapaneseの間で 重複していますが、これが意図的な重複なのか偶然かは言語にはわかりません。 継承ラインの順にしたがってメソッドを呼び出すだけです。

実際にはJapaneseモジュールが優先されてそのaddressメソッドが呼ばれるのですが、 ひとめでそれが分かるのは、だいぶ「訓練されたRubyist」です。

現在のRubyでは、includeされた時、 「スーパークラスですでにそのモジュールがincludeされていた時には 二重にincludeしない」という挙動になっています*1。ですから、 スーパークラスでincludeされていることに気がつかなかった場合、 includeしても継承ラインのその場所にモジュールが登場しなかった ということが起こりえます。

それから、モジュールが既にincludeされてから、 そのモジュールに対してincludeを行った場合、 既に存在するクラスの継承ラインには新たにincludeされるようになったモジュールは含まれません。 つまり、includeのタイミングによって継承ラインへの反映のされ方が異なるわけです。 ちょっと気持ち悪いです。

これらを(ある程度)解決する手段がmixメソッドです。

mixメソッドをincludeの代わりに使うと、

  • 現在モジュールに定義されているメソッドを クラス/モジュールに注入する
  • mixされたモジュールは継承ラインに登場しない
  • メソッド名の重複は例外になる
  • 例外がイヤならモジュールを書き換える、 または重複したメソッドの名称を変更する
  • 定数を取り込むかどうかを指定できる。 デフォルトは取り込まない

という振舞いになります。これにより

  • 名称の重複はエラーになるので、見逃しがない
  • 名称変更ができるので、明示的に解消できる
  • あくまでも「現時点での状態の注入」なので、 継承ラインが変化した時の「おかしさ」がない。 問題は解決していないが、気分は良い(苦笑)

ということが実現できます。

たとえば以下のようなコードでは

module American
  attr_accessor :address
end
module Japanese
  attr_accessor :address
end
class JapaneseAmerican
  mix American
  mix Japanese  # => address conflict!
end

addressメソッドが重なっているからmixできません。 無事mixさせるためには名称衝突を明示的に回避します。

class JapaneseAmerican
  mix American, :address => :us_address
  mix Japanese, :address => :jp_address
end

これで、addressという名前による重複はなくなりました。

なぜ、includeにオプションをつけるのではなく、 新しいメソッドを導入して言語をより複雑にするかというと、 個人的にmixの挙動の方が望ましいと思っていて、 ユーザーをそちらに誘導するためには、より短い名前の方が望ましいと思ったからです。

Traitsを実現するmixメソッドの実装ですが、 RubyKaigiでこれを紹介したその日には中田さんが着手していて、 パッチは完成しているそうです。

ただ、各種プレゼンテーションでは説明しなかった以下の課題があり、 これらについては結論を出す必要があります。

  • mixされるモジュールが別のモジュールをincludeしていた場合にはどうなるか。 おそらくは例外になる。mixとincludeは混ぜるべきではない。
  • mixで別名を付けて問題解決、と読めるような言い方をしているが、 実際にはモジュール内部で名前を書き換える前のメソッドを読んでいる可能性がある。 それをどうするか。なにもしない(重複する方が悪い)とする考えもあるが、 それだとせっかく苦労してTraitsを導入しようとしているのが まったく無駄になるので、名前を書き換えたメソッド呼び出しを モジュールのメソッド定義実体から探し出してメソッドをコピーする という(Bertrand MayerのOOSCに記述されていたアイディア)を導入することを考える
  • インスタンス変数の名称重複を解決する手段がない。 これはサブクラスからは見えないインスタンス変数を導入し、 mix対象のクラスではそちらを使うこと推奨とするべきではないかと 考えています。1.9向けのパッチは既に書いてありますが、 プライベートなインスタンス変数の記法を @_fooをにするか、@__fooにするか、 はたまたまったく違うナニかを考えるのかが難しくて現状では放置されています。 mixが導入されたらより必要になるでしょうね。

Method Combination

RubyKaigiではmixの一部として導入する話をしていたMethod Combinationだが、 mixでいちいち「どのメソッドをラップするか」とか指定するのが以上にめんどくさいことに 後で気がつきました。ので、分離。

今回の案はprependというメソッドを導入すること。「include、mixに続いて またもうひとつ?」という声が聞こえてきそうだが、私もそう思います。でも必要なのよ。

prependはそのモジュールが提供する機能を、現在のクラス/モジュールの「前」に 追加する機能。

module Foo
  def foo
    p :before
    super
    p :after
  end
end
class Bar
  def foo
    p :foo
  end
  prepend Foo
end
Bar.new.foo # :before, :foo, :after

とように使う。prependしたモジュールFooで定義されたfooメソッドが、 prepend先のメソッドfooをラップしているのが分かるでしょうか。

prependメソッドは、RailsコミッタでもあるYehuda Katzの提案で、 これがあればRailsのalias_method_chainを撲滅できる、と息巻いていた。 私もそう思う。

具体的な実装はまだないんだけど、たぶんT_ICLASSのようなものを 継承チェーンに置いて、そっちを先に検索するようにするんじゃないかなあ。

Keyword Arguments

引数、特にオプショナル引数がどんな働きをするのか忘れる人は私だけじゃないと思います。 たとえば、 public_instance_methods メソッドはオプショナル引数を受け付けるのだけど、 それが「オプショナル引数を付けると、それが真であった時にスーパークラスのメソッドを含む」のか、 それとも逆かというのは私でもいつも忘れてしまいます。正解はfalseを付けた時に含まない。

これをたとえば

aClass.public_instance_methods(include_super: false)

と書けたら、ずっと覚えやすくなるというものです。

Rubyのキーワード引数は、1.9で追加されたシンボル記法のハッシュが 引数リストの末尾に付いているだけです。

2.0で新たに追加されたのは、メソッド定義側でこれを簡単に受け取れる記法です。

例としてはこんな感じ。

呼び出し側

1.step(by: 2, to: 20) do |i|
  p i
end

呼び出され側

def step(by: step, to: limit)
  ...
end

後、「**」で辞書形式で受けとるとか、ちょっとした機能追加もありますが、 基本的にはこれだけ。

Namespace

技術的な詳細などについては同じRubyConfで前田(修吾)くんが発表したスライドを見てもらった方が良いと思います。

Rubyではopen classといって既存のクラスの定義を書き換えちゃうことができる。 メソッドの追加も自由自在だ。このように既存のクラスに「パッチ」を当てちゃう技法のことを 「Monkey Patching」と呼ぶことも多い。

これは「ゲリラ・パッチング」→「ゴリラ・パッチング」→「モンキー・パッチング」と 変化して生まれた用語なんだって。 まあ、Rubyはクラスなんてものは変化するもんじゃないって「硬い」言語よりも 大きな自由を提供してることは確かだよね。 DHHは今回のRubyConfのキーノートで「今後はMonkey PatchingじゃなくてFreedom Patchingと呼ぼう」と 叫んでた。メル・ギブソンの『ブレーヴハート』を引用しつつ。「ふりーだーーむ」。

まあ、フリーダムなのは素晴らしいことなんだけど、影響力が大きすぎるというのもまた事実。 やろうと思えば整数のプラスメソッドを書き換えて、1+2 = 42 のような変更だってできちゃうから。 でも、大抵のプログラムは副作用でまともに動作しなくなるよね。

で、問題はこのような変更の影響の範囲がグローバル(プログラム全体)であることで、 仮にこのような修正をなんらかの「スコープ」に閉じ込めることができたなら、 もっと安全に、もっと安心して「フリーダム・パッチング」を活用できる、はず。

そのような「スコープ」のために、昔からClassboxとかSelector Namespaceとかが提案されてきた のですが、今回、前田くんが実装したのはSelector Namespaceの一種であるRefinment。

たとえば、以下のようなプログラムがあったとします。っていうか、あります。

class Integer
  def /(other)
    return quo(other)
  end
end
p 1/2 # => (1/2)

これは割り算演算子(/)を再定義して、整除ではなく結果を有理数にしようもので、 標準添付ライブラリの mathn の本質部分です。 しかし、整数の割り算の結果が整数であることを期待しているコードは当然存在するわけで、 そのようなコードは上のような変更で破綻する可能性があります。

そこで今回導入しようというのがrefinmentです(呼び名は変わるかもしれません)。 文法としては以下のようになります。

module MathN
  refine Integer do
    def /(other)
      return quo(other)
    end
  end
  p 1/2 # => (1/2)
end
p 1/2   # => 0

Refinementの単位としてはモジュールを使います。またモジュールです。大活躍ですね。

モジュールの中では既存のクラスをrefineできます。 refineの中で定義された修正はそのrefinment(ネームスペース)の中でだけ有効です。 ですから、MathNモジュールの中では 1/2 は有理数の (1/2) であり、 その外側では今まで通り整除になっています。有効範囲はレキシカルであり、 Refinmentはブロックの外側には影響を与えません。

Classboxとの違いは、そこを通じて呼び出された先(レキシカルスコープの外)に 「置き換え」が影響を与えるかで、いろいろ検討した結果、 多くのプログラミング言語がダイナミックスコープをあきらめたのと同様の理由で 「置き換え」はレキシカルになるべきだとの結論を出しました。

モジュールとして実現されたネームスペースを使うには usingメソッドを使います。 こんな感じ。

module Rationalize
  using MathN
  p 1/2 # => (1/2)
end
p 1/2 # => 0

これでRationalizeモジュールの中ではMathNが提供するRefinementが利用できます。

さらに、今までメソッドの中でメソッドをネストして定義した場合、 そのメソッドはクラスに直接定義されてあんまり意味ないじゃん、みたいな状態になっていたのですが、 今後はそのメソッドの範囲内でだけ有効なRefinementにネストの内側のメソッドが定義されるので、 完全にプライベートなメソッドとして使うことができます。

class Foo
   def foo
      def bar
        ...
      end
      bar # 呼べる
   end

   def quux
     bar  # 呼べない
   end
end

この変更はかなり大規模かつ複雑なものですが、前田くんのところでは実際に動作しています。 早く trunk に突っ込みたいものです。しかし、NaCl取締役の激務をこなしつつ、 こんなスーパーなパッチを作っちゃう前田くんに拍手。

まとめ

歴史編で見てきた通り、ずっと昔からキーワード引数などについて話してきましたが、 とうとう現実になりそうです。長かった。

*1  MacRubyでは違うらしい。Ruby 1.9でそのような変更をしたかったが、YARVが継承ラインに同じモジュールが2度登場しないことを前提にしていたため断念

[]

追記

track feed Matzにっき Creative Commons License This work is licensed under a Creative Commons License.