IT・技術研修ならCTC教育サービス

サイト内検索 企業情報 サイトマップ

研修コース検索

コラム

スーパーエンジニアの独り言

CTC 教育サービス

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes 

第33回 ミロガンダの秘密 2014年3月

 今回もRubyの神秘の謎について科学特捜隊の一員として調査したいと思います。
 オイリス島に生育する食肉植物にもなぞらえる「モジュール(Module)」についてです。

 モジュール(Module)はクラス(Class)と同様な機能を持ちますが、インスタンスを生成できないことが大きな違いになります。この違いを静止する「植物(Module)」と移動する「動物(Class)」と比喩させていただきます。
 以前の記事「ビリー・エリオット」にメタクラスについて参考となる記述があります。この大きな制約が故に、モジュールはクラスとは異なる利用方法となります。

 Rubyでは数値演算用のライブラリの一つとしてMathモジュールが提供されています。Mathモジュールには、三角関数や平方根を求める様々な数学関数が定義されています。Mathモジュールをインクルードして自分のクラスのメソッドとして使用すること、つまり「Mix-in(ミックスイン)」することで利用します。

 それでは、学校の授業で暗記させられた2の平方根を求めてみます。
 (「ひとよ、ひとよに、ひとみごろ」というあの語呂合わせです。)

code. 01
  # Ruby Programming Language
  include Math
  p sqrt(2) #=> 1.4142135623730951
  p golden_ratio = (1 + sqrt(5)) / 2    #=> 1.618033988749895

 sqrt(square root)メソッドで平方根を返してくれることが分かったので、以前の記事「ノッティングヒルの恋人 」フィボナッチ数列の巻でご紹介した黄金比率を平方根から求めてみました。

 またMathモジュールでの数学関数のメソッドは「モジュール関数(Module Function)」として定義されており、Mathモジュールの特異メソッドとして呼ぶこともできます。クラスメソッドの様に呼び出すことができるのです。
 特異メソッドについての解説は過去記事「よだかの星」をご参照ください。

code. 02
  golden_ratio = (1 + Math.sqrt(5)) / 2   #=> 1.618033988749895

 Mathモジュール内の数学関数は「モジュール関数」と定義されていることで二通りの使い方ができました。場合に応じて使い分けができそうです。
 今度は自作のクラスにMix-inで組み込んで使用した以下の例を見てみましょう。

code. 03
  # Ruby Programming Language
  class CarnivorousPlant
    include Math
    def golden_number(a)
        b = a.to_f * ((1 + sqrt(5)) / 2)
        return b
    end
  end
  
miloganda = CarnivorousPlant.new miloganda.golden_number(10) #=> 16.18033988749895 miloganda.sqrt(2) #=> private method ‘sqrt’ called for #<CarnivorousPlant:0x2115048> (NoMethodError)

 Mix-inすることでMathモジュールの平方根を求めるメソッド(sqrt)をクラス内部で利用することが可能になり、黄金比を求めるメソッドが実装できました。
 これは上手く機能しましたが、次に事件が発生しました。sqrtメソッドを生成したインスタンスから呼び出すとエラーになってしまったのです。
 Mix-inしたはずなのに、何故エラーになったのでしょうか?
 Mix-inすれば自分のクラスのインスタンスメソッドとして使用できるはずです。
 エラーメッセージをよく見るとプライベートメソッド(private method)だというのです。
 何故プライベートメソッドなのでしょうか?
 Mathモジュールのように、標準で提供されているモジュールをMix-inすると全てプライベートメソッドになってしまうのでしょうか?

 原因を調査するとsqrtメソッドが「モジュール関数」であることに起因するようです。
 どうやらメソッドをモジュール関数と指定することで自動的にprivateメソッドとなるのです。
 自作のモジュールを作成してこの挙動を再度確認してみましょう。

code. 04
  # Ruby Programming Language
  module Insectivorous
    def traps(protozoan)
      nitrogen = protozoan.to_i / 2.0
      return nitrogen
    end
    module_function :traps
  end
  
p Insectivorous.traps("50") #=> 25.0
class CarnivorousPlant include Insectivorous end
miloganda = CarnivorousPlant.new p miloganda.traps("100") #=> private method ‘traps’ called for #<CarnivorousPlant:0x1fb4bd8> (NoMethodError)

 自作モジュール(Insectivorous)内で定義したメソッド(traps)を"module_function"メソッドを使いモジュール関数とすることで特異メソッドとして呼び出すことができます。
 そこでモジュール関数を自作クラス(CarnivorousPlant)にMix-inするとprivateメソッドとなり、生成したインスタンスから呼び出す際にエラーになることが再現できました。

 では何故「モジュール関数にするとprivateメソッドとする」のでしょうか?

 何か理由があるはずです。目を凝らすとそのヒントが最初のコード(code. 01)に隠されていました。
 トップレベル(Top Level)でMathモジュールをMix-inするとsqrtメソッドがきちんと呼べました。モジュール関数であるためprivateメソッドであるはずにも関わらず、エラーが出なかったのは何故なのでしょう?

 ここで「プライベートメソッド(private method)」とは一体何者なのか再考する必要がありそうです。
 Rubyリファレンスマニュアルの「呼び出し制限」の項目を参照すると「privateに設定されたメソッドは関数形式でしか呼び出せません。」とストレートに書かれておりました。
 意訳するとprivateメソッドは「レシーバを指定して呼び出すことができない」メソッドであります。
 通常、メソッド呼び出しを行う際には呼び出し元となるレシーバ(オブジェクト)を指定する必要がありますが、これを抑制されているため、結果的にクラスの外部からメソッドを呼び出すことができないというアクセス(可視性)制限と筆者は理解していました。

 これは間違いではないのですが、"private"の本意(極意)はまさに「(レシーバを省略した)関数形式で呼び出す」ことそのものにあったのです。
 つまり本来の目的は「関数的メソッド」です。

code. 05
  # Ruby Programming Language
  puts("Treasure of the Miloganda")

 「レシーバを指定できない」ことからprivateメソッドは、必然的に記述は関数形式になります。
 つまりこれが本来のprivateメソッドの効用なのではと思い付きます。
 また同時に「レシーバを指定できない」のですからクラスの内部からしか呼べません。これは前述のコード(code. 03, code. 04)の結果と一致します。
 試しに関数的メソッドを作成してわざとレシーバを指定して呼び出してみましょう。

code. 06
  # Ruby Programming Language
  def consume(nitrogen, oxygen = 80)
    nutrient = nitrogen + oxygen
    return nutrient
  end
  
puts(consume(50)) #=> 130 puts(self.consume(100)) #=> private method ‘consume’ called for main:Object (NoMethodError)

 やはりprivateが故に関数的メソッドが呼べることが伺い知れました。
 念のために、普段使っている組み込みのライブラリでレシーバを指定してたらどうでしょうか?

code. 07
  # Ruby Programming Language
  self.puts("Treasure of the Miloganda")
  #=> private method ‘puts’ called for main:Object (NoMethodError)

 確かにプライベートメソッドです。関数的メソッドはprivateメソッドです。
 "private"とは関数的メソッドを「産み出す」のが、その本来の用途だったと気が付きました。

 ところで関数的メソッドを実行している場所は、トップレベル(Top Level)と呼ばれます。
 トップレベル(Top Level)が提供されていることで、クラス(class)などのオブジェクト指向の難しいキーワードを登場させずに、スクリプトとして容易に用いることの手軽さがRubyの適用範囲や利便性を高めています。
 それ故にトップレベルが用意されており、そこで関数的メソッドが使えるのでしょう。

 ここで更なる疑問があります。トップレベルで関数的メソッドが呼べるのですから、トップレベルは何某か(なにがしか)のクラスの内部(内側)であるということになります。
 乗り出した船です。もう一つの隠された秘密であるトップレベルの正体についても言及します。

code. 08
  # Ruby Programming Language
  p self        #=> main
  p self.class  #=> Object

 トップレベル(Top Level)はObjectクラスのインスタンスであるmainが正体だったのです。
 C言語がmain関数でプログラムが始まるのと同じように、Rubyではmainオブジェクトからプログラムが開始されるのです。
 実は純粋なオブジェクト指向言語とも称されるRubyが簡易にスクリプトとして利用できる仕組みとして、その実行母体(オブジェクト)の正体を隠していたのです。
 そして関数的メソッドはスクリプトとして簡易に利用するための工夫なのです。

 ここまでの調査で関数的メソッドの条件は「Objectクラスのprivateメソッド」とすることで成り立つことが判明しました。先ほどの疑問であった「モジュール関数にするとprivateメソッドとなる」のは、つまりは「関数的メソッド」とするための方策であったと推論できます。

 推論に基づいてスーパークラスであるObjectクラスのprivateメソッド、つまり関数的メソッドを調べてみます。
 private_methodsメソッドを使うとレシーバのprivateメソッドの名前を調べることが可能です。
 そしてprivate_methodsメソッドの引数にfalseを指定するとレシーバクラスのprivateメソッドだけを表示します。

code. 09
  # Ruby Programming Language
  p Object.private_methods()
  #=> [:inherited, :initialize, :loop, :open, :puts, :gets, :p, ...]
  p Object.private_methods(false)
  #=> [:inherited, :initialize]

 筆者の実行環境ではObjectクラスが所有するprivateメソッドは全部で92個あるのに対して、Objectクラスで定義されているprivateメソッドはたった2個だけです。
 結果、Objectクラスはスーパークラスですから、ほぼMix-inされたprivateメソッドで占められているだと分かりました。

 ObjectクラスはKernelモジュールをインクルードしており、そこに普段お世話になっている"puts/gets"メソッドや"p"メソッド、ファイルの"open"メソッド、大域脱出の"loop"メソッドまであるのですが、それらはKernelモジュールでモジュール関数として定義されています。
 これらモジュール関数をObjectクラスにMix-inしている恩恵として関数的メソッドとして今まで使えていたのです(詳細はRubyリファレンスマニュアルをご覧くださいませ)。

 冒頭の疑問がやっと解決しました。
 モジュール関数はモジュールの特異メソッドとして呼ぶためだけの機能だと思っていましたが、どうやら関数的メソッドとして利用する目論見がやっと理解できました。
 その冒険の過程でトップレベルがObjectクラスだったことも分かりました。

 Rubyのプログラムを書き始めた途端に、知らないうちに大きな鯨に飲み込まれていて、気が付けば鯨のお腹の中に居た気分です。まるでピノキオ(Pinocchio)みたいですね。鯨のお腹の中をもっと探せばゼペット爺さんも助けを待っているかもしれません。

 次回もお楽しみに。

 


 

 [IT研修]注目キーワード   Python  UiPath(RPA)  最新技術動向  Microsoft Azure  Docker  Kubernetes