CRubyとmrubyのメソッド可視性(privateとpublic)について

hasumikin is a programmer.


この記事は、Ruby Advent Calendar 2024の17日目の記事であると同時に、mrubyファミリ Advent Calendar 2024の17日目の記事でもあります。


みなさんこんにちは。 ほんじつはprivateとpublicについて書きます。1

mrubyはCRubyとほぼ同じ言語機能を実装していますが、メソッドの可視性に関する機能はなぜか実装されていません。

メソッドの可視性とは(復習)

privateより後ろに定義したメソッドは、クラスの外から呼べません:

# CRuby
class MyClass
  private

  def private_method
    "Private Method"
  end
end

MyClass.new.private_method
#=> private method 'private_method' called for an instance of MyClass (NoMethodError)

自クラスの中からなら呼べます:

# CRuby
class MyClass
  def call_private_method
    private_method
  end

  private

  def private_method
    "Private Method"
  end
end

MyClass.new.call_private_method
#=> Private Method

言い方を換えると、privateなメソッドは関数形式でしか呼べません。 関数形式とは「レシーバを書かず、いきなりメソッドを書く」形式のことです。2

mrubyではどうなのか? そしてputs

ところがmrubyでは、privateと書いてもとくに効果が発揮されず(なぜかエラーにもならず)、MyClass.new.private_methodが動作します。 ということはつまりですね、こういうコードも動いてしまいます:

# mruby
[].puts "Hello" #=> Hello

Arrayインスタンスにputsメソッドが実装されているかのように動いてしまいましたね。 CRubyでは、これは動きません。 でも実はCRubyでも、Arrayインスタンスにはputsメソッドが継承されているのです。 なぜならKernelモジュールにputsインスタンスが書かれていて、以下の順序でそれが継承されています:

Array < Object < Kernel

CRubyで[].puts "Hello"が動作しないのは、Kernel#putsがprivateなメソッドだからです。 privateなメソッドは関数形式でしか呼べないので、レシーバを明示した書き方はダメなのです。

こう書けば動きます:

# CRuby
[].send(:puts, "Hello") #=> Hello

後述するModule#publicの用法により、これもいけます:

# CRuby
class Array
  public :puts
end

[].puts "Hello" #=> Hello

トップレベルに定義したメソッドの可視性

そしてこれは、意外と知られていないかもしれない、あるいは広く一般には意識されていないであろうCRubyの言語仕様です:

# CRuby
def top_level_method
  "You cannot see this"
end

# `class MyClass < Object; end` ←これと同じ
class MyClass
end

MyClass.new.top_level_method
#=> private method 'top_level_method' called for an instance of MyClass (NoMethodError)

NoMethodErrorになるのはなぜでしょう? トップレベルに定義したtop_level_methodはObjectクラスのインスタンスメソッドになります。 クラス定義時にSuperクラスを明示しない場合、Objectクラスを暗黙的に継承することは皆さん知っていると思います。 それならばMyClass#top_level_methodは呼べるはずですが、呼べません。

なぜなら、エラーメッセージから想像できるとおり、CRubyというRuby実装では、トップレベルのメソッドを暗黙的にprivateに定義するからです。 ですから、以下のように書けば動きます:

# CRuby
public def top_level_method # publicを明示的に書く
  "You can see this!"
end

class MyClass
end

MyClass.new.top_level_method
#=> You can see this!

トップレベルにメソッドを定義するのはたいてい書き捨てスクリプト(?)でしょうから、この仕様が適切なのはよくわかりますね。 あらゆるクラスにtop_level_methodを生やしたいわけじゃないので。

ただ、以下のコードは動くのです:

# CRuby
def top_level_method
  "You cannot see this、と思うじゃん?"
end

class MyClass
  def my_method
    top_level_method
  end
end

MyClass.new.my_method
#=> You cannot see this、と思うじゃん?

top_level_methodを関数呼び出ししているから呼べる。 それはそう、なのですが、これは呼べなくていいやつという気がしませんか?

mrubyでは

これまでの説明でわかったと思いますが、mrubyならば、Objectクラスを継承しているあらゆるインスタンスからトップレベルメソッドが呼べます:

# mruby
def top_level_method
  "You cannot see this(見えとるがな)"
end

class MyClass
end

MyClass.new.top_level_method
#=> You cannot see this(見えとるがな)

筆者の疑問は、なぜmrubyではprivatepublicを書いてもエラーにならず、しかし効果をなにも発揮しないのか?という点です。

エラーを出さないという選択についてひとつ言えそうなのは、privateを使用しているCRuby用のスクリプトをそのままmrubyで動かすときにエラーになると面倒くさいから(べつにちゃんと動くしね)、ということかなと思います。

しかしそもそもなんで可視性機能を実装しないのでしょうね? まつもとさんに質問しようと以前から思っているのですが、なぜかご本人に会うと忘れていて聞きそびれてしまうので、ここに書いておきます。 だれが聞いてください。

もうひとつの都合?

こんな理由もあるかもしれません。 privatepublicの実体はModule#privateModule#publicです。 これらのメソッドは2種類の振る舞いを持っています。

以下のように書くとき、privateメソッドは引数:private_methodを受け取ります:

# CRuby
private def private_method
  "private_method"
end

def public_method
  "public_method"
end

なぜなら、def式はメソッド名のシンボルを返すからです。 private(:private_method)が実行されることにより、private_methodメソッドがprivateになります。 だから、後続のpublic_methodはprivateなメソッドになりません。 これがひとつ目の振る舞い。

しかし、以下のように書くと、動作が異なります:

# CRuby
private

def private_method
  "Private Method"
end

def public_method # これもprivateになる
  "Public Method(嘘です)"
end

こちらの書き方のほうが広く使われているかもしれません。 引数をとらないprivateは、それ以降のメソッド定義をprivateにします。 言い換えると、コンテキストを変更するメソッドなのです。 これはmrubyとの相性が悪いかもしれない。

mrubyコンパイラ(mrbcコマンド)は、コマンドライン引数に複数のRubyスクリプトファイルパスを受け取ることができ、それらのファイルの中身を連結(ほんとうに単なる連結)してひとつのVMコードへとコンパイルします。 すべてが連結された状態でRubyとしてVaildであればよいので、クラス定義の途中で分割したファイルを気ままに連結しても構いません。 このとき、意図せずにprivateコンテキストになっているせいでバグるかもしれず、これにまつわる問題を華麗に解決できないから放置しているのかもしれません。 知らんけど。

とはいえ、mruby界隈でそんなアクロバティックなスクリプト分割をしている人は見たことがありません。 この理由はとくに重要ではなさそうです。 蛇足でした。

putsは偉い

putsメソッドは、世界のshugomaedaがその名前を提案してRuby言語に取り込まれた機能だそうです。 printprintlnより文字数が少ないから偉いんだそうです。 ところで、みなさんはputsをどう発音しますか?

筆者は「プットエス」と発音します。 太古の昔から「プッツ」と発音する人が多いことは知っていますが、putcを「プッチ」と発音する人には会ったことがありません。 だから「プッツ」には違和感があります。 「プッツ」と「プットシー」では一貫性がないじゃないですか? 長々とした記事になりましたが、これを言いたいためにこの記事を書きました。

それではよいお年をお迎えください!


  1. 本稿では、世界のshugomaedaが実装したと伝わるprotectedについては触れません。だれも使ってないので。 

  2. selfがレシーバになるときは、privateなメソッドも呼べます。