おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(23)minitest、ripper

アプリ作成の記事でminitestを使いました。 今回はminitestについて、また一般にテストについて、私の考えを書こうと思います。

テストとは

一般にテストはプログラムのバグを除くために広範に使われている手法です。 開発に従事している方は既に良くご存知のことと思います。 私は「仕事として」つまり給料をもらって開発をしたことがありません。 おそらくプロの開発の方は、私よりもっとテストの経験も知識もお持ちだと思います。 そういう方は、この記事を読む必要はないかもしれません。

また、テストには「テスト駆動開発」とか「振る舞い駆動開発」などというのもあって、ひとつの開発手法にもなっています。

  • テスト駆動開発=>Test Driven Development=>TDD
  • 振る舞い駆動開発=>Behavior Driven Development=>BDD

そのような手法では、まずテストを書いてからアプリを書く、といわれます。 ですが、私はどちらが先でも良いのではないかと思っています。

テストを書くときは、あまりプログラムの細かい部分に立ち入るよりも、その振る舞い(behavior)が期待通りかをテストするようにします。 そうすることで、そのプログラムの内部構造を変更、改良したのちも同じテストプログラムでテストすることができます。 つまり、テストは内部構造まで立ち入らず、振る舞いのみにフォーカスすべきです。

また、テストではその規模も問題になります。

  • ユニットテスト: クラスあるいはメソッドなどのプログラムを構成する単位に対して行うテスト。 比較的簡単に作成、テストができる
  • 統合テスト: ユニットテストの対象よりも大きなプログラムの部分、あるいは全体について行うテスト。 プログラムの構成によってその実際は変わるが、例えばユニットテスト済みの単体同士の連携などをテストする。 このような大きなテストでは入出力、ネットワーク、データベースなどを扱うために、特別な方法が必要になることが多い。

この記事ではユニットテストを扱います。

自作テストプログラム

minitestはテスト用のフレームワークですが、それを使わずに自分でテストプログラムを作ることもできます。

例えば、あるメソッドが数字を表す文字列を引数に取り、その数値を返す、というものだとしましょう。 メソッド名をto_numberとしておきましょう。 今回は先にto_numberを作ってから、そのテストプログラムを作ることにします。

def to_number(s)
  s.to_i
end

引数の文字列をto_iメソッドで整数にして返す、というメソッドです。 おそらく、皆さんはすぐにこのプログラムの欠点を見つけたことでしょう。 問題ありありのプログラムですが、テストの説明には適しています。

テストプログラムを考える上でのポイントは「メソッドの振る舞い」をテストするということです。 この場合、「引数が与えられたら、その引数の表す数値を返す」というのが振る舞いですが、もう少し詳しく考えないといけません。 引数と返し値の組み合わせを「引数=>返し値」で表すことにします。

  • 数字を表す文字列=>対応する数(ただし、整数と浮動小数点数の2種類の数がある。ここでは複素数、有理想は想定しない)
  • 数字ではない文字列=>??
  • 文字列以外のオブジェクト=>??

??と書いたところは仕様が決められていなかったので、そう書きました。 テストを考えただけで、仕様の不十分さも分かりました。 これもテストを導入するメリットの1つです。 つまり、振る舞いに対するテストを書くためには、振る舞いをより完全に定義しなければならなくなります。

今回は??のところはnilを返して、エラー処理は呼び出し側に任せることにしましょう。

テストはこんな感じです。

def to_number(s)
  s.to_i
end

# テストプログラム
def test_to_number
  a = to_number("0")
  print "0 にならずに #{a} が返された\n" unless a == 0
  a = to_number("-0.1")
  print "-0.1にならずに #{a} が返された\n" unless a == -0.1
  a = to_number("1.23e10")
  print "1.23e10にならずに #{a} が返された\n" unless a == 1.23e10
  a = to_number("abc")
  print "nilにならずに #{a} が返された\n" unless a == nil
  a = to_number(100)
  print "nilにならずに #{a} が返された\n" unless a == nil
end

test_to_number
#=> -0.1にならずに 0 が返された
#=> 1.23e10にならずに 1 が返された
#=> nilにならずに 0 が返された
#=> nilにならずに 100 が返された

最初のテストだけ通って、残りの4つは通りませんでした。 テストを通ることを「成功(サクセス)」通らないことを「失敗(フェイル)」といいます。 またテストを通ることを「パスする」ともいいます。

to_numberは引数について場合分けして、それぞれに対して適切な値を返さなければなりません。

  • 整数を表す文字列=>その整数を返す
  • 浮動小数点数を表す文字列=>その浮動小数点数を返す
  • それ以外の文字列=>nilを返す
  • 文字列でないオブジェクト=>nilを返す

メソッドto_numberを書き直してみましょう。

def to_number(s)
  return nil unless s.is_a?(String)
  case s
  when /\A-?\d+\Z/
    s.to_i
  when /\A-?\d+\.\d+([Ee]\d+)?\Z/
    s.to_f
  else
    nil
  end    
end

正規表現を使って、文字列が数字にマッチする場合に数値に変換しています。 結構複雑なことをしなければならないのですね。 これでテストは通るようになります。

Ripper

ちょっと脇道にそれますが、このように文字列が数値を表すかどうかをチェックするのは一種の字句解析に当たります。 rubyはプログラムを読む時に「字句解析」をして、その文字列をキーワード、識別子、区切り記号、数値などに分解したのちに構文解析、実行に移ります。 そこで、rubyの字句解析を使って数値を取り出すこともひとつの方法として考えられます。 ripperという標準添付ライブラリはRuby プログラムを解析するためのライブラリです。 メソッドRipper.lexは字句解析をし、字句(トークン)の配列を返します。

require 'ripper'
p Ripper.lex("123 -1.2e5")
#=> [[[1, 0], :on_int, "123", END], [[1, 3], :on_sp, " ", END], [[1, 4], :on_op, "-", BEG], [[1, 5], :on_float, "1.2e5", END]]

配列の中身の詳細はRubyのドキュメントを参照してください。 注目すべきは、各配列の2番目のシンボルです。

これを利用すると、正規表現を使わずに「整数」「浮動小数点数」「その他」に文字列を分類できます。 to_numberをRipper.lexを使って書き直してみましょう。

require 'ripper'

# メソッドの再々定義
def to_number(s)
  return nil unless s.is_a?(String)
  a = Ripper.lex(s)
  unless a.size == 1 || (a.size == 2 && a[0][1] == :on_op && a[0][2] == "-")
    return nil
  end
  case a[-1][1]
  when :on_int
    s.to_i
  when :on_float
    s.to_f
  else
    nil
  end    
end

Ripperを使うことはあまりないでしょうが、Ruby同様の字句解析をしたいときはRipperが便利です。 本題とはあまり関係ないことを書きましたが、Ripperも面白いと思っていたので、この機会に紹介しました。

minitest

to_numberメソッドをテストするminitestのプログラムを作ってみましょう。 次のプログラムはto_numberに対して8つのテストをします。 to_numberメソッドはこのプログラムの前に挿入しておくこととします。 (require_relativeメソッドで取り込んでも良いです)。

require 'minitest/autorun'
class TestToNumber < Minitest::Test
  def setup
  end
  def teardown
  end

  def test_to_number
    [["0",0],["12",12],["-5",-5],["12.34",12.34],["2.27421e5",2.27421e5],["-23.56",-23.56],["abc",nil], [123,nil]].each do |s,v|
      if v == nil
        assert_nil to_number(s)
      else
        assert_equal v, to_number(s)
      end
    end
  end
end

要点を次に記します。

  • minitest/autorunをrequireメソッドで取り込む。 なお、minitestは標準ライブラリなのでインストール不要
  • MiniTest::TesTのサブクラスとして、テスト用のクラスを定義する。 クラス内には、setup、teardown、テスト用のメソッドを記述する
  • setupはテスト前に行う準備を記述する。 良くあるのは、インスタンスの生成など
  • teardownはテスト後の後始末をする。 上記のプログラムではsetupとteardownは何もしていないので、記述しなくても良い
  • テストのメソッドはその名前の先頭にtest_をつける。 ここではto_numberメソッドのテストなのでtest_to_numberとした。 ここでは、2重配列を使い、その8つの要素に対してeachメソッドを用い、テストする。 配列のそれぞれの要素は`["0",0]のように、数字の文字列(=>引数)と数字(返されるオブジェクト)となっている。 最初の例では"0"を引数にしてメソッドを呼び出したとき、0が返されることが期待されている。 期待通りならテストは通過(パス)し、そうでなければ失敗(フェイル)となる。
  • assert_nilは実行結果とnilを比較する。 つまり、期待される結果がnilで実行結果がassert_nilの引数になる。
  • assert_equalはminitestが提供するテスト用メソッドで、2つの引数をとる。 第1引数には期待されるオブジェクト、第2引数には実際にテストで得られたオブジェクトを書く。 両者が一致すればパス、一致しなければフェイルになる。 なお、期待されるオブジェクトがnilのときは、assert_equal nil, (テストで得られたオブジェクト)とすることもできるが、これは古い書き方で現在は推奨されていない。 assert_nilが推奨される書き方になっている。 テスト通過の場合は何も表示されず、失敗のときは期待された(expected)オブジェクトと実際の(actual)オブジェクトが表示される。

実行してみます。

$ ruby example23.rb
Run options: --seed 7828

# Running:

.

Finished in 0.005369s, 186.2654 runs/s, 1490.1234 assertions/s.
1 runs, 8 assertions, 0 failures, 0 errors, 0 skips

テストは成功したので、フェイルの表示が出ません。 ドットはテストするメソッド(この場合はtest_to_number)の個数分だけ表示されます。 一番下の行の「8 assertions」は8つのassert_・・・のテスト(アサーションと呼ぶ)が行われ、失敗0エラー0だったことを示しています。 なお、失敗は「期待した値と実行結果の値が異なること」、エラーは「プログラムのエラー(間違い)」で、違うものを表します。

このテストはトップレベルのメソッドを対象としました。 もしこのメソッドがトップレベルで定義されたインスタンス変数を使っているときは、その定義をsetupで行わなければなりません。 (トップレベルで行う定義をそっくりコピペすれば良い)。 トップレベルのインスタンス変数はmain(トップレベルのオブジェクト)のインスタンス変数になります。 これに対して、minitestのテストはMinitest::Testクラスのサブクラスに定義されたメソッドで行われるので、そこで参照されるインスタンス変数は「そのサブクラスのインスタンスの変数」になります。 したがって、mainのインスタンス変数は参照できないことに注意してください。 これを解決するには、さきほど述べたように、setupメソッドでインスタンス変数を定義しておきます。

標準出力のテスト

次のプログラムはテストするまでもない簡単なものですが、標準出力のテスト例として取り上げます。

@n = 0
def inc
  @n += 1
  p @n
end

inc
inc
inc

メソッドincはインスタンス変数@nを1つ大きくし、標準出力に出力します。 トップレベルでincを3回呼ぶと、

1
2
3

と、呼び出すごとに1つずつ大きくなって表示されます。

このテストは次のようになります。

class TestInc < Minitest::Test
  def setup
    @n=0
  end
  def teardown
  end

  def test_inc
    assert_output("1\n") {inc}
    assert_output("2\n") {inc}
    assert_output("3\n") {inc}
  end
end

大事なことはsetupでインスタンス変数@nの初期化をしなければなりません。 前項で述べたように、トップレベルのインスタンスがmainであるのに対し、test_incメソッドのインスタンスはTestIncクラスのインスタンスです。 したがって、トップレベルのインスタンス変数を参照することはできません。 setupでの@nの初期化は必須で、これがないとincの@n += 1を実行するときに、@nが未定義で参照されるためにnilになり、nilには+メソッドがないのでエラーになります。 これはエラーであってフェイルではありません。

このテストではassert_outputメソッドを使っています。 引数が期待される結果、それをブロックの実行の標準出力と比較して異なるとフェイル・メッセージが表示されます。

自作のテストプログラムで標準出力をテストするには、出力を捕まえる(出力先を変更する)必要があり、大変ですが、minitestを使えばassert_outputを使うだけですみます。

minitestのアサーションは豊富です。 minitestのドキュメントに詳細がありますので、参考にして下さい。

徒然Ruby(22)public、private、protected

今回はメソッドの呼び出し制限ついて説明します。 呼び出し制限にはpublic、private、protectedの3つがあります。

メソッドの公開と非公開

英語のpublicとprivateは「公的な」「私的な」という形容詞ですが、同時に「公開の」「非公開の」という意味も持っています。 プログラム上では後者の意味でよく使われる単語です。 Rubyではprivateを「メソッドをクラス定義の外に非公開にする」ために良く用います。

# プライベートメソッド(外から参照できないメソッド)の例
class Statistic
  def initialize array
    @array = array
  end
  def show
    print "合計は #{@array.sum}\n"
    print "平均は #{average(@array)}\n"
    print "標準偏差は #{stdev(@array)}\n"
  end
  private
  def average(array)
    (array.sum.to_f/array.size).round(1)
  end
  def stdev(array)
    mean = average(array)
    v = array.inject(0){|a,b| a+(b-mean)*(b-mean)}/array.size
    Math.sqrt(v).round(1)
  end
end

d = Statistic.new([5,6,7,8])
d.show
#=> 合計は 26
#=> 平均は 6.5
#=> 標準偏差は 1.1
p d.average([5,6,7,8]) #=> エラー、Privateメソッドを非関数形式で呼び出したため
p average([5,6,7,8]) #=> エラー、averageはトップレベルで未定義なため

この例ではStatisticクラスを定義しています。 このクラスは配列を引数にインスタンスを生成、初期化し、showメソッドで配列の統計量(合計、平均、標準偏差)を表示します。 クラスには3つのインスタンスメソッドshow、average、stdevが定義されていますが、クラス外部で使えるのはshowのみになっています。 最後の2行で、外部からaverageを呼び出していますが(片方はドットつき、他方はドットなし)いずれもエラーになります。 これはaverageがクラス定義の外部では呼び出せないことを示しています。 stdevも同様に外部からは呼び出せません。

これはクラス定義の中ほどにあるprivateメソッドによって、以下の行で定義されたクラス定義内のメソッドが「呼び出し制限がprivateであるメソッド」になるためです。 「呼び出し制限がprivaeteであるメソッド」を短く「privateメソッド」といいますが、「呼び出し制限を設定する方のprivateメソッド」と混乱しないように注意してください。 これは文脈から判断することになります。 最初のshowメソッドはprivateメソッドの前なのでpublicメソッドになります。 クラス定義におけるデフォルトはpublicです。

averageやstdevのようにクラス定義内では使うが、外からは使うことのないメソッドは、プログラム上で良く発生します。 そのようなメソッドを外部から呼び出すと問題が発生することが予想される場合は、privateメソッドにしておくべきです。 (上記の例ではそのような問題発生はありませんが、privateメソッドの例示のために設定しました)。

さて、privateの良くある使い方を説明しましたが、実はprivateの本当の意味は「非公開」ではなく、「関数形式でしか呼び出せない」なのです。 そこで、以下では呼び出し制限の定義とメソッド振る舞いについて解説します。

メソッドとレシーバ

まず、話の前提として「メソッドはレシーバとセットで呼び出される」ということを確認しておきたいと思います。 次の例を見てください

class A
  def intro
    print "私はクラスAのインスタンスです\n"
  end
end
a = A.new
a.intro #=> 私はクラスAのインスタンスです

メソッドintroがクラスAのインスタンス・メソッドとして定義されました。 そのメソッドを呼び出すには

  • クラスAのインスタンスを作成し、変数aに代入する
  • インスタンス、ドット、メソッド名」の形、この例ではa.introの形でメソッドを呼び出す

このときのインスタンスを「メソッドのレシーバ」と呼びます。

もうひとつ大事なことは「レシーバのクラスでそのメソッドを定義済みである」ということがメソッド呼び出しの前提です。 上記の例ではクラスAでintroを定義しているのでa(の指すインスタンス)をレシーバとしてintroを呼び出せるわけです。

※ 以下では「a(の指すインスタンス)」を単に「a」と書くことにします。

もし、定義されてなければエラーになります。

class A
  # introを定義しない
end
a = A.new
a.intro #=> エラー(NoMethodError)

Aのサブクラス(直接の子でなくても孫から先でも良い)はインスタンス・メソッドを受け継ぐので、メソッド呼び出しが可能です。

class A
  def intro
    print "私はクラスAのインスタンスです\n"
  end
end

class B < A
end

b = B.new
b.intro #=> 私はクラスAのインスタンスです

以上をまとめると

  • メソッドはレシーバとセットで呼び出される
  • レシーバのクラスでメソッドが定義されている、またはスーパークラスでメソッドが定義されていることが、メソッド呼び出しの前提である

ということになります。

呼び出し制限

呼び出し制限にはpublic、private、protectedの3つがあり、メソッドはこの3つのいずれかを持っています。

publicメソッド

publicメソッドは制限なしに呼び出せます。 つまり、プログラムのどこでもそのメソッドを呼び出せます。 また、クラスでメソッドを定義するとき、そのメソッドの呼び出し制限のデフォルトはpublicです。 最初の例をもう一度見てみましょう。

class A
  def intro
    print "私はクラスAのインスタンスです\n"
  end
end
a = A.new
a.intro #=> 私はクラスAのインスタンスです

メソッドintroの呼び出し制限はデフォルトのpublicです。 introはレシーバaとセットで、クラス定義の外で呼び出すことができます。 また、クラス定義の内部で呼び出すことも可能です。

class C
  def calc(x)
    print "合計は#{sum(x)}、平均は#{average(x)}\n"
  end
  def sum(x)
    x.sum
  end
  def average(x)
    x.sum.to_f/x.size
  end
end
c = C.new
c.calc([1,2,3,4]) #=> 合計は10、平均は2.5
c2 = C.new
c2.calc([1,2,3,4]) #=> 合計は10、平均は2.5

クラスCの内部では3つのメソッドcalc、sum、averageが定義されています。 そして、メソッドcalcの中では、sumとaverageの2つのメソッドが呼ばれています。 ここで注意したいのは、これらのメソッドは「インスタンス、ドット」の部分が無いということです。 つまりレシーバが指定されていません。 このように、レシーバが省略されている呼び出し形式を「関数形式」といいます。 関数形式の呼び出しでは、レシーバはあるのですが、それが書かれていないだけなのです。 そのレシーバを「デフォルトのレシーバ」と、ここでは言うことにします。

では、デフォルトのレシーバは何かと言うと、それは(外部から)calcを呼び出したときのレシーバです。 c=C.newでクラスCのインスタンスが変数cに代入され、c.calc([1,2,3,4])でそのインスタンスをレシーバとしてcalcが呼ばれた時点でレシーバが確定します。 つまり、レシーバは変数cの指すインスタンスです。 同様に、最終行のc2.calc([1,2,3,4])ではc2の指すインスタンスがレシーバです。

calcメソッドを実行中にsumとaverageを関数形式で呼び出すとき、そのレシーバはcalcのレシーバを用います。

  • c.calcを実行中=>関数形式の呼び出しは、c.sum、c.averageと同じ=>cがデフォルトのレシーバ
  • c2.calcを実行中=>関数形式の呼び出しは、c2.sum、c2.averageと同じ=>c2がデフォルトのレシーバ

メソッド定義の中では「self」という疑似変数がデフォルトのレシーバを指します。 selfの指すインスタンスは、calcが呼ばれた時点で確定します。

プログラムの3行目は

print "合計は#{self.sum(x)}、平均は#{self.average(x)}\n"

とレシーバselfを明示して書いても同じことになります。 ですが、特に何か意図するところがなければ、selfは省略するのが普通です。

ここでの説明をまとめると

  • 関数形式のメソッド呼び出しではレシーバが省略される
  • レシーバが省略されたときは、selfがレシーバになる
  • selfが確定するのは、selfを含むメソッドが呼ばれたとき。 selfにはそのメソッドのレシーバが代入される

ということになります。

self

今までのselfの説明はメソッド定義の中に限った話でした。 selfは他の場所では何を指しているのでしょうか? 一般にselfは次のようになっています。

  • トップレベルでは、オブジェクトmain(Objectクラスのインスタンス
  • クラス定義またはモジュール定義の中ではクラス(またはモジュール)自身

となります。

※ mainについてはRubyのドキュメントに記載があります。

注意が必要なのはメソッド定義の部分です。 「メソッドの定義」と「メソッドの呼び出し(または実行)」は別のことだと理解してください。 「メソッドを定義」しているときのselfと「メソッドを実行」しているときのselfは別物です。 メソッド定義には、メソッドを実行するときの振る舞いを書くのですから、selfも実行時の振る舞いが想定されなければなりません。 すなわち「selfには定義中のメソッドが実行されたときのレシーバが代入される」ということを想定してメソッド定義を書かなければなりません。

次の例で、selfが何を指すか確認してください。 表中央のselfはRubyがその行を実行中のselfです。 (「実行」には「クラス定義を実行」「メソッド定義を実行」などもあることに注意してください)。

プログラム self メソッド実行中のself
class A
def intro A
print "私はクラスAのインスタンスです\n" A introを呼び出したときのレシーバ
end A
end
a = A.new main
a.intro #=> 私はクラスAのインスタンスです main

※のところはselfはmainだと思われるが未確認。

トップレベルとクラス、モジュール定義はselfが分かりやすいですけれども、メソッド定義の中のselfを考えるのは面倒な場合があります。 次の例を考えてみましょう。

def average2(array)
  (array.sum.to_f/array.size).round(1)
end
def stdev2(array)
  mean = average2(array)
  v = array.inject(0){|a,b| a+(b-mean)*(b-mean)}/array.size
  Math.sqrt(v).round(1)
end

class Statistic2
  def initialize array
    @array = array
  end
  def show
    print "合計は #{@array.sum}\n"
    print "平均は #{average2(@array)}\n"
    print "標準偏差は #{stdev2(@array)}\n"
  end
end

d = Statistic2.new([2,4,3,9])
d.show
#=> 合計は 18
#=> 平均は 4.5
#=> 標準偏差は 2.7
  • average2とstdev2は引数の配列のデータの平均、標準偏差を返す。これらはトップレベルで定義されている
  • Statistic2クラスは、@arrayをインスタンス変数に持つ
  • インスタンスを作る時に、newメソッドに配列を引数として渡し、それを@arrayに代入する
  • showメソッドで合計、平均、標準偏差を表示する
  • showメソッドの中でaverage2とstdev2メソッドが呼ばれている。

さて、このとき、selfはそれぞれの場所で何を表しているでしょうか?

  • トップレベルでは、selfは「mainというオブジェクト」を指す。
  • average2とstdev2の定義の内部のselfは、メソッド実行時にはそれが呼ばれたときのレシーバが代入される
  • class〜endのshowメソッド定義のselfは、そのメソッド実行時にはそれが呼ばれたときのレシーバが代入される
  • Statistic2クラスのインスタンスdが生成される。
  • dをレシーバとしてshowメソッドが呼ばれる。 実行中のshowメソッドのselfはd
  • showの中でaverage2とstdev2が関数形式で呼ばれる。 関数形式のときはselfをレシーバとして呼ぶので、ここではdがレシーバになる
  • stdev2の中でaverage2が関数形式で呼ばれている。 関数形式なのでレシーバはself。 このときのselfはdである。
  • dはStatistic2クラスのオブジェクトである。 Statistic2クラスではaverage2もstdev2も定義されていないが、dはaverage2とstdev2のレシーバになりうるのだろうか?

細かい分析と検討の結果、最後に?のついた疑問が出てきました。

この疑問に答えるには、トップクラスで定義されたメソッドの扱いを知る必要があります。 Rubyのドキュメントには次のように書かれています。

トップレベルで定義したメソッドは Object の private インスタンスメソッドとして定義されます。

このことから、average2とstdev2はObjectクラスのメソッドです。

  • average2とstdev2はObjectクラスのインスタンス・メソッド
  • Statistic2クラスはスーパークラスの指定なく定義されているが、それでもObjectはStatistic2のスーパークラスになる。 (Objectはデフォルトですべての定義されるクラスのスーパークラス
  • スーパークラスインスタンス・メソッドはサブクラスに継承される
  • したがって、average2とstdev2はStatistic2クラスにも継承されている
  • 変数dの指すオブジェクトをレシーバとしてaverage2とstdev2を呼ぶことが可能である

このように、selfがどのインスタンスを指していようと、そのインスタンスのクラスはObjectのサブクラスなので、トップレベルで定義されたメソッドを継承しています。 よって、selfはそのメソッドのレシーバになれるので、関数形式でそのメソッドを呼ぶことができるのです。

長い考察でしたが、簡潔にいうと

「トップレベルで定義したメソッドは、プログラムのどこでも関数形式で呼び出すことができる」

ということになります。

privateメソッド

privateメソッドは関数形式(メソッド名だけでの呼び出し)でのみ呼び出せます(ただし「self.メソッド名」ならば呼び出すことが可能)。 ということは、privateメソッドは「selfをレシーバとしてのみ呼び出せる」ということです。

また、一般にメソッド呼び出しにおけるレシーバは次の条件を満たしていなければなりません。

  • レシーバのクラスでそのメソッドが定義されている
  • または、レシーバのクラスのスーパークラスがそのメソッドを定義している。 (逆に言えば、レシーバが「メソッドを定義したクラスのサブクラスのインスタンス」である)

この2つを組み合わせると、privateメソッドを呼び出すにはselfの指すオブジェクトのクラスが上記の2条件のいずれかを満たすことが必要です。 説明が複雑になるので、例をあげて説明します。

  • privateメソッドprvがクラスCで定義されている
  • publicメソッドpubがクラスCで定義されている
  • Cのインスタンスをcとする

トップクラスではselfはmainを指していて、mailはObjectクラスのインスタンスで、Objectではprvメソッドが定義されていないので、prvを関数形式で(つまりselfであるmainをレシーバとして)呼ぶことはできません。 同様にトップクラスでpubも関数形式では呼べませんが、pubは「インスタンス、ドット、メソッド名」の形式でも呼び出せるので、c.pubとして呼び出すことができます。

c.pubが実行されている間は常にselfとcは同じオブジェクトを指しています。 ですから、この実行の最中はprvを関数形式で呼び出すことが可能です。

それはどのようなときでしょうか? それはpubの定義の中で直接または間接にprvを呼び出すときです。

以上の考察をまとめると

  • privateメソッドは、そのクラス定義のメソッド定義(これはそのメソッド自身でも他のメソッドでも良い)の中で直接または間接に呼び出すことができる
  • それ以外はprivateメソッドを呼び出すことができない。

ということになります。 「直接または間接」というのは後ほど説明します。

ただし、ひとつ注意が必要で、メソッドが定義されたクラスのサブクラスはメソッドを継承できるということから、サブクラス中のメソッド定義でもprivateメソッドを呼び出せます。

privateの本来の意味は「呼び出し形式が関数形式に限る」ということなのですが、その結果として「クラスとサブクラスの定義外では(通常は)呼び出せない」ということになります。 それがprivate(非公開)という名前になっている理由だと思われます。

実際問題、クラス定義の中だけで使われる関数は良くでてきます。 そのような関数にはprivateの設定をしましょう。

さて、クラス定義の中のメソッドはデフォルトでpublicでした。 privateにするためにはprivateメソッドを使います。 このことは冒頭の例でも示しました。 privateメソッドを引数なしで呼び出すとその後のメソッドはすべてprivateメソッドになります。

また、privateメソッドを引数つきで使うと、その引数のメソッドがprivateメソッドになります。 このときはprivateメソッドに先立って対象となるメソッドを定義しておかなければなりません。 引数のメソッド名にはシンボルまたは文字列を使いますが、シンボルを使う方が普通です。

private :average

これにより、averageメソッドだけがprivateメソッドになります。 引数なしのprivateメソッドと異なり、引数付きのprivateメソッドでは以後のメソッドは引き続きデフォルトがpublicであるとして定義されます。

privateメソッドに対応するpublicメソッドとprotectedメソッドがあります。 これらはメソッド名を引数にとり、そのメソッドの呼び出し制限を設定します。 また、引数なしで実行されると、それ以後に定義されるメソッドすべての呼び出し制限を設定します。

最後に「直接または間接」を詳しく説明しましょう。 「直接呼び出す」の意味は明らかですから、「間接的に呼び出す」例を示します。

class D
  def a
    print "privateメソッドaです\n"
  end
  def b
    c #トップレベルのメソッドcを呼び出す
  end
  private :a
end
def c
  a # privateメソッドaを呼び出す
end
d = D.new
d.b #=> privateメソッドaです
  • このプログラムではprivate :aによって、メソッドaの呼び出し制限がprivateになってる
  • メソッドcはトップレベルのメソッドで、aを関数形式で呼び出している
  • クラスDのインスタンスを生成し変数dに代入。d.bでpublicメソッドbを呼び出す
  • メソッドbのselfはdである。bの中でトップレベルのメソッドcを呼ぶ メソッドcはObjectのメソッドなのでサブクラスDで継承されている。 したがって、インスタンスdはメソッドcのレシーバになりうる。 よってメソッドcの呼び出しはエラーにならない
  • cが呼ばれたときselfはdである。 dはaのレシーバになれるので、関数形式のaの呼び出しはエラーにならずに行われる
  • aが呼び出されたので「privateメソッドaです」が表示される

この例ではprivateメソッドaがクラス定義の外で正常に呼び出されました。 それは、メソッドcがDのメソッドbから呼び出されたため、selfがdのままの状態でメソッドcが実行できたからです。 この場合privateメソッドaはDのメソッドbによって、cを通して間接に呼ばれています。 これが「間接的な呼び出し」の意味です。

bとは独立にcがaを呼び出すことはできません。

これは、privateメソッドがクラス外で呼び出せる例外です。 通常はprivateメソッドはクラス外から呼び出せないと考えても問題ありません。

protectedメソッド

protectedメソッドはprivateメソッドに似た使い方ができますが、定義は違います。 protectedメソッドは「そのメソッドを持つオブジェクトが selfであるコンテキストでのみ」呼び出せます。

ここで難しいのが「コンテキスト」だと思います。 私も正確な定義は分かっていないのですが、実行時の「状態」をデータとしてまとめたものを一般にコンテキストというので、Rubyでも同様だと思います。 protectedの理解自体にはコンテキスト全体の理解までは必要ありません。 selfの状態だけを理解するだけで十分です。

さて、実行状態において、そのメソッドを持つオブジェクトがselfというのはどういうときでしょうか。 privateの最後の例では、メソッドがb=>c=>aと呼ばれる間ずっとselfとdは同じオブジェクトを指していました。 この状態が「そのメソッドを持つオブジェクトがself」です。 そのときにはprotectedメソッドは(関数形式でも関数形式でなくても)呼び出すことができます。

class E
  def initialize(name)
    @name = name
  end
  def a
    print "protectedメソッドaです\n"
    print "レシーバは#{@name}です\n"
  end
  def b(e)
    e.a # selfはdになっているのでprotectedメソッドを使える
  end
  protected :a
end
d = E.new("d")
e = E.new("e")
d.b(e) #=> privateメソッドaです レシーバはeです

この例ではpublicメソッドbを呼び出した時に、selfはdと同じものを指しています。 「そのメソッドを持つオブジェクト(d)が selfであるコンテキスト」になっています。 そこで、protectedメソッドaが使えますが、ここではeをレシーバにして呼び出しています。 このように、関数形式ではない形でも呼び出せるのはprivateとの違いです。

まとめ

  • publicメソッドは制限なしに呼び出せる
  • privateメソッドは関数形式(メソッド名のみでの呼び出し)でのみ呼び出せる(ただし「self.メソッド名」ならば呼び出すことができる)
  • protectedメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキストでのみ呼び出せる

この呼び出し制限から、privateメソッドとprotectedメソッドはクラス定義外では(一部例外を除いて)呼び出すことができません。 メソッドを「非公開」にするにはprivateでもprotectedでも同様の効果が期待できますが、privateを使っている例がほとんどです。 privateという名前が「非公開」をイメージしやすいからでしょうか。

徒然Ruby(21)特異メソッド、名前空間、モジュール関数

今回は特異メソッド、特異クラス定義、名前空間、モジュール関数について説明します。

特異メソッド

特異メソッドは英語ではsingleton method(一人っ子のメソッド)といいます。 英語の方が意味をとりやすいと思います。 特異メソッドは個々のオブジェクト(インスタンス)だけがレシーバになるメソッドのことを言います。

次の例はクラスAのインスタンスを2つ作り、そのうちの一方に特異メソッドを作ったときの実行結果を示すものです。 オブジェクトaに特異メソッドを定義するにはdef a.introのように「オブジェクト、ドット、メソッド名」のようにします。

class A
end

a = A.new
b = A.new

def a.intro
  print "私はaです\n"
end

a.intro #=> 私はaです
b.intro #=> エラー (NoMethodError)

実行します。

私はaです
example21.rb:12:in `<main>': undefined method `intro' for #<A:0x00007f6f3d737bd0> (NoMethodError)

b.intro
 ^^^^^^

aにはintroメソッドを定義したので「a.intro」の結果「私はaです」が出力されましたが、bには定義されていないので、「b.intro」はエラーになりました。

通常のメソッドは同じクラスから生成したインスタンスすべてで可能ですが、特異メソッドはそれが定義された個別のインスタンスでのみ可能です。 特異メソッドは様々なケースで使えると思いますが、例えば次のようなケースが考えられます。

  • クラスUserはあるシステムを使用するユーザを定義する
  • 個々のユーザはUserクラスのインスタンスとして生成する
  • ユーザの中には一人だけ管理者ユーザがいる。つまりUserクラスのインスタンスの中にひとつだけ管理者ユーザ・インスタンスがある
  • 管理者ユーザ・インスタンスには特別のメソッドを付与する(特異メソッドになる)

クラスメソッド

Rubyでは個々のクラス(例えばIntegerやString)はClassクラスのインスタンスです。 話がややこしくなりますが、クラスはクラスであると同時にインスタンスでもあるわけです。

クラス名は大文字で始まりますが、この文字は定数を表しています。 クラスを定義すると、そのクラスオブジェクト(Classクラスのインスタンス)が生成され、定数に代入されるのです。 つまり大文字で始まる名前は、クラスを表す名前であると同時に定数の名前でもあります。

クラスはインスタンスですから、変数に代入することもできます。

class A
end

c = A
p c  #=> A
p c.class #=> Class

変数cにはクラスA(のオブジェクト)が代入され、「p c」でクラス名Aが、「p c.class」でクラスAのクラスであるClassが表示されます。

このようにクラスを変数に代入することはできますが、通常はそういうことはしません。 それはプログラムを分かりにくくなることに繋がります。 定数を参照すれば、それがクラスだと直感的に分かります。 定数だけをクラスに用いるのが良い習慣です。

さて、クラスはオブジェクトですから、特異メソッドを定義することができます。 クラスの特異メソッドをクラスメソッドといいます。

class A
end

def A.intro
  print "私はクラスAです\n"
end

A.intro #=> 私はクラスAです
selfを使った定義

クラスメソッドの定義はクラスの内側でもできます。

class A
  def A.intro
    print "私はクラスAです\n"
  end
end

A.intro #=> 私はクラスAです

この方法では疑似変数selfを使うことができます。 「class A〜end」では、selfは「ClassクラスのインスタンスであるクラスA」を指します。 したがって、この区間では定数Aとselfは同じオブジェクトを指します。 ですので、

class A
  def self.intro
    print "私はクラスAです\n"
  end
end

A.intro #=> 私はクラスAです

のように、selfを使ってクラスメソッドを定義することができるわけです。

クラスメソッドの継承

クラスメソッドはそのサブクラスにも継承されます。 上記のプログラムに続き、以下を追加するとBにもAのクラスメソッドintroが継承されます。

class B < A
end
B.intro #=>私はクラスAです
モジュールの特異メソッド

特異メソッドは個々のオブジェクトにメソッドを定義するものでした。 クラスメソッドはクラス(クラスはオブジェクトでもある)の特異メソッドです。 ではモジュールについてはどうでしょうか? モジュールはクラス同様にオブジェクトなのでしょうか? 答えはイエスです。 モジュールはModuleクラスのインスタンスです。 したがって、モジュールの特異メソッドを作ることができます。

module C
end

def C.intro
  print "私はモジュールCです\n"
end

C.intro #=> 私はモジュールCです

モジュール定義の中ではselfを使った特異メソッド定義が可能です(クラスの場合と同じ)。

モジュールの場合はクラスと違い、サブモジュールを作ることができませんから、特異メソッドの継承はありません。 また、モジュールをincludeしても特異メソッドの継承はできません。 インクルードは通常のメソッドと定数を引き継ぐだけです。

名前空間

トップレベルに同じ名前のメソッドを2つ作ることはできません。 それをすると、2番めの定義が1番めの定義の「再定義」になり、以後2番めの定義だけが有効になります。 しかし、異なる動作をする2つのメソッドに同じ名前をつけたいときもあります。 例えば、あるプログラムはMarkdownファイルをHTMLにもPDFにも変換できるとします。 「変換する」メソッドは、convertという名前が良いでしょう。 HTMLに変換するのもconvert、PDFに変換するのもconvertにしたいのです。

このとき、convertにプリフィックスをつけて区別することが可能です。

def html_convert
... ... ...
end
def pdf_convert
... ... ...
end

このようなプリフィックス名により、区別することを「名前空間」を使うといいます。 HTMLに関するメソッドにはすべてhtml_をつけ、PDFに関するメソッドについてはpdf_をつけるわけです。

同様のことはmoduleの特異メソッドでも実現できます。

module HTML
end
module PDF
end

def HTML.convert
... ... ...
end
def PDF.convert
... ... ...
end

このようにモジュールの特異メソッドは名前空間を付与することと同一の効果があります。 同じことをクラスでもできますが、クラスには継承があるため、サブクラスを作ると別の名前空間で同じ内容のものが作られてしまいます。 したがって、名前空間としてはモジュールの利用が適しています。

しかし、クラスの特異メソッドも名前空間に似た効果があります。 例えば、IOクラスにはreadという特異メソッドがあります。

s = IO.read(ファイル名)

このメソッドはファイルを読み込み、その文字列を変数sに代入します。

単にreadという名前のメソッドでは名前の衝突が不安になりますが、IO.がついているので、これはIOクラスの特異メソッドだと分かります。 これは名前空間に似た効果です。 また、IOのサブクラスにFileがあります。 特異メソッドはサブクラスに引き継がれるので

s = File.read(ファイル名)

でも同じreadメソッドが呼ばれます。 これはファイルクラスの特異メソッドと見えますが、実はIOの特異メソッドの継承です。

特異メソッドは一般のオブジェクト(通常のクラスから生成したインスタンス)に定義するよりも、モジュールやクラスに定義する場合が多いように思います。 実際Rubyのドキュメントを見ると、クラスメソッドは数多く定義されています。 最もよく使うのはインスタンスを生成するnewメソッドでしょう。 これはほとんどのクラスの祖先であるObjectクラスの特異メソッドです。 各クラスはそれを受け継いでいるので、newが使えるのですね。

特異クラス定義

特異クラス定義とは、次のようにclass <<(式)〜endという構文のことをいいます。 ここで定義されるメソッドは(式)の指すオブジェクトの特異メソッドになります。

class A
end
a = A.new
b = A.new
class << a
  def intro
    print "私はaです\n"
  end
end
a.intro #=> 私はaです
b.intro #=> エラー(NoMethodError)

classに<< aをつけると、aの指すオブジェクトについて定義をすることになります。 そこで定義されるメソッドはすべてaの指すオブジェクトの特異メソッドです。 この記法の良いところは「多くの特異メソッドを少ないタイプ量で記述できる」ことです。

また、selfをこの記法に使い、クラスメソッドを定義することができます。

class A
  class << self
    def intro
      print "私はクラスAです\n"
    end
  end
end
A.intro #=> 私はクラスAです

これにより、class A〜endの中で、普通のメソッドもクラスメソッドも定義することができ、非常に効率的になります。 この記法が最も良いかもしれません。 モジュールにはこのタイプの記法がないので、個別に作ることになります。 ですが、次に述べるモジュール関数を作る方法を適用すると簡単に特異メソッドと通常のメソッドの2つができます。

モジュール関数

ビルトイン・オブジェクトの中にMathモジュールがあります。 数学関数を提供するモジュールです。 例えば正弦関数はMath.sin(角度)として呼ぶことができます。 また、モジュールをインクルードしておけば、sin(角度)とモジュール名を省略することができます。

p Math.sin(Math::PI/3) #=> 0.8660254037844386
include Math
p sin(PI/3) #=> 0.8660254037844386
  • Math::PIはモジュールMathの中で定義されている定数。 定数の場合はドットではなく::を使ってモジュール下の定数であることを示す。 クラス定義の中で定義された定数も「クラス名::定数」となる。 これはMathモジュール外部からの参照で用いられる
  • includeによって、モジュール内の定数やメソッドが取り込まれる。 PIにMath::を付けなくて良い。 また、sinメソッドはMathモジュールの通常のメソッドである

ここで注意が必要なのは、Math.sinとsinメソッドの違いです

  • Math.sin=>Mathモジュールの特異メソッド
  • sin=>Mathモジュールの通常のメソッドがincludeで取り込まれたもの

この2つがセットになっているので、インクルードを使う/使わないの選択が可能になります。 これは便利なことなので、モジュールの提供するメソッドに名前空間をつける目的であれば、両方のメソッドの定義をするべきです。 (Math.がつかないsinはインクルードが必要なので、その時点で名前の衝突を避けることができる)。 このようなメソッドを「モジュール関数」といいます。

以前紹介したモジュールのミックス・インは通常のメソッドを複数のクラスに提供する仕組みでした。 ミックス・インのメソッドは、特異メソッドを含まないので、モジュール関数ではありません。

モジュール関数は通常のメソッドと特異メソッドの2つを定義しなければならないのですが、それは非効率です。 それを解決するメソッドがmodule_functionメソッドです。

  • 通常のメソッドを定義する
  • module_function メソッド名によって、そのメソッドがモジュール関数になる。 メソッド名はシンボルで表す
module X
  def intro
    print "私はモジュールXのモジュール関数です\n"
  end
  module_function :intro
end
X.intro #=> 私はモジュールXのモジュール関数です
include X
intro #=> 私はモジュールXのモジュール関数です

有用な関数をプログラム全体で使えるように提供したいときには、このようなモジュール関数として提供するのが良い方法です。 特にライブラリの作成においては、モジュール関数にするのがベストな選択です。 (ライブラリでなければ、トップレベルのメソッドでも十分です)。

定数の名前空間

前項で述べたように、モジュール内で定義された定数をモジュール外から参照するには「モジュール名::定数名」としなければなりませんでした。 これは「モジュールで、定数にも名前空間をつけることができる」ということを示しています。

定数は変数と同様にオブジェクトを指しますが、変数と違い再代入ができません。 その定数が生きている間はずっと同じオブジェクトを指します。 ユーザが定義する定数は数字や文字列(特にフリーズした文字列)が多いと思いますが、その他にクラス名やモジュール名も定数です。 このことから「クラスやモジュールにも名前空間を適用することができる」ということができます。

module ABC
  class A
    def intro
      print "モジュールABCのAです\n"
    end
  end
end
module EFG
  class A
    def intro
      print "モジュールEFGのAです\n"
    end
  end
end

a = ABC::A.new
b = EFG::A.new
a.intro #=> モジュールABCのAです
b.intro #=> モジュールEFGのAです

プログラムが大きくなり、クラス名の名前の衝突が心配であれば(特にライブラリでは)モジュールを使った名前空間の提供が解決手段となるかもしれません。

徒然Ruby(20)アプリ制作、インストール、テスト

2023/11/4追記

この記事の内容は古くなり、不適当な部分があります。 それはGitHubにあるCalcレポジトリが大幅に更新されたことによります。 そのため、下記の記事の代わりとして、GitHubページの徒然なるままにRuby -- アプリ制作、インストール、テストを参照してください。 ご迷惑をおかけしますが、よろしくお願いいたします。

追記終わり

だいぶRubyの説明は進みましたから、このあたりでアプリを作る上でのポイントを述べたいと思います。 そのために、簡単な電卓プログラムを作り、GitHubにアップロードしたので、参考にしてください。

ファイル名で起動する方法

Rubyプログラムは

$ ruby ファイル名

で起動できるのですが、「ruby」と入力するのは煩わしいものです。 アプリ作成中は仕方がないとしても、完成したアプリはファイル名だけで起動したいですね。 それを実現するには、次の3行をファイル名の先頭に付けます。

#!/bin/sh
exec ruby -x "$0" "$@"
#!ruby

お呪いのようなものだと考えて、コピペしても構いません。 一応説明すると、まず1行目の#!はシバン(shebang)と呼ばれ、Unix系のOSではスクリプトを実行するプログラムを指定します。 この場合は「/bin/shによってこのファイルを実行する」のですから、シェルスクリプトとしての実行になります。 (/bin/bashではなく/bin/shとなっているのは、システムによってはbash以外のシェルが使われているかもしれないからです。 /bin/shはいろいろなシェル共通の呼び出しを提供します)。

2行目はシェルのコマンドで、execはそのシェルのプロセスで(新規プロセスを生成せずに)コマンドを実行する、というものです。 $0は起動されたファイル名(スクリプトファイルのファイル名)、$@は引数すべてを表します。 これから、もしこのスクリプトファイルのファイル名がcopyコマンドラインから次のように呼ばれたとすると、

copy file1 file2

まず、`/bin/sh/が起動され、2行目が実行されます。 2行目は

ruby -x copy file1 file2

コマンドラインから入力するのとほぼ同じことになります。

-xrubyのオプションで、「スクリプトを読み込む時に、`#!'で始まり, "ruby"という文字列を含む行までを読み飛ばす」というものです。 このことにより、最初の3行(#!/bin/sh〜#!ruby)が読み飛ばされ、4行目からrubyプログラムが実行されます。

Unix系OSのシバングは指定されたファイルを実行するので、

#!/usr/bin/ruby

でも良いように思われますが、これだとまずい場合もあります。 最もありそうなケースはrbenvでインストールしたrubyです。 rubyは$HOME/.rbenv以下に保存されるので、/usr/bin/rubyでは呼び出せません。 ($HOMEはユーザのホームディレクトリで、シェルからは~でも参照できます)。 このrubyはシェルから呼ぶことにより起動できるので、いったんシェルを起動してからrubyを起動する、という面倒なやり方が必要なのです。

コマンドへの引数の処理

Unix系OSではコマンドラインの構成が

コマンド 引数1 引数2 ・・・・

となっています。 Rubyでは、引数はARGVという配列に代入されます。 例えば

$ ruby_echo Hello world

とコマンドruby_echoが呼ばれたとき、

  • ARGV[0]には文字列"Hello"が代入されている
  • ARGV[1]には文字列"world"が代入されている

となります。 コマンドラインでは半角空白が引数の区切り文字になります。 区切り文字はARGVの中には入りません。 もし、空白も入れたいというときには、シングルクォートを用います。

$ruby_echo 'Hello world'

この場合はARGV[0]に文字列"Hello world"が代入されます。 引数は1個ということになります。

引数が何個あるかは配列の要素の数を返すメソッドsizeを使います。 ARGV.sizeが引数の数です。

ruby_echoのプログラムは簡単です。

#!/bin/sh
exec ruby -x "$0" "$@"
#!ruby

print ARGV.join(' '), "\n"

配列ARGVの各要素をjoinメソッドで繋げて文字列にします。 そのとき要素の区切りには、引数' '(半角空白)が用いられます。

インストーラ

作成例として、電卓プログラムcalc.rbを考えてみましょう。 このプログラムをコマンド名calcでインストールしたいとします。 その場合は$HOME/bin以下にファイルを置き、実行属性をつければよいのです。 FileUtilsモジュールを使うのが便利です。

require 'fileutils'
include FileUtils

def install
  cp "calc.rb", "#{Dir.home}/bin/calc"
  chmod 0755, "#{Dir.home}/bin/calc"
end
  • Dir.homeはユーザのホームディレクトリ($HOMEと同じ)を返すメソッド
  • cpはFileUtilsモジュールのメソッドで、ファイルをコピーする。
  • chmodはFileUtilsモジュールのメソッドでファイルの属性を指定する。 0755は8進整数を表す。このファイル属性は
    • 所有者は読み、書き、実行可
    • グループメンバーは読み、実行可で、書き込不可
    • その他ユーザは読み、実行可で、書き込不可 となる

実行属性をつけないと、ファイル名での起動はできません。 以上のようにインストールするとコマンドラインから

$ calc

で起動できるようになります。

アプリケーションを作るときには、インストーラも作っておきましょう。 また、アンインストーラインストーラに含め、オプションで切り替えても良い)も入れておくと良いです。 GitHubのCalcにはインストーラinstall.rbが添付されているので参考にしてください。

上記ではユーザ領域へのインストールでしたが、他のLinuxユーザにも使えるようにするには/usr/local/binにインストールします。 このときは管理者権限が必要なので、Ubuntuなどではsuコマンドを使います。 例えば

$ su ruby install.rb

のように起動します。

しかし、システム領域にインストールする必要はほとんどないと思います。

テスト

Rubyの標準のテスト・スートはminitestです。 名前はミニですが、結構大きいプログラムで、ドキュメントの量もあります。 minitestは別の記事で詳しく述べようと思いますが、ここではポイントを絞って書きたいと思います。

テストプログラムはRubyで書きます。 calcのテストは次のような感じになります。

require 'minitest/autorun'
require_relative 'lib_calc.rb'

class TestCalc < Minitest::Test
  def setup
    @calc = Calc.new
  end
  def teardown
  end

  def test_lex
    assert_equal [[func.to_sym,nil],[:'(',nil],[:id,"x"],[:')',nil]], @calc.lex("#{func}(x)")
... ... ...
... ... ...
  end

  def test_parse
    assert_output("100\n"){@calc.run("10*10")}
... ... ...
... ... ...
  end
end
  • minitesti/autorunをrequireで取り込む
  • テストしたいファイルlib_calc.rbはテストプログラムと同一ディレクトリにある。 取り込みにはrequire_relativeを使う
  • テスト用のクラス(ここではTestCalcという名前になっている)をMinitest::Testのサブクラスとして定義する
  • クラス内にはsetup、teardown、各テスト用のメソッドがある。
    • setup=>各テストの前に準備作業をするためのメソッド
    • teardown=>各テスト後の後始末をするためのメソッド
  • テスト用のメソッドにはtest_というプレフィックスをつける
  • assert_equial A, B は「Aが正常に機能したときの結果のオブジェクト」「Bが実行結果のオブジェクト」で、それらが一致すればテストを通過したことになり、一致しないとメッセージが出力される。 前者をexpected(期待される結果)、後者をactual(実際に行った結果)としてメッセージに書き込まれる
  • assert_output(A){ B }はAが標準出力への期待される出力、ブロックは実行メソッド。 メソッド(B)を実行した出力とAが一致すればテスト通過、一致しなければメッセージが出力される

テストがすべて通ると次のように出力されます。

$ ruby test.rb
Run options: --seed 43869

Running:

..

Finished in 0.006940s, 288.1909 runs/s, 8501.6308 assertions/s.
2 runs, 59 assertions, 0 failures, 0 errors, 0 skips
$ 

ドットはテスト項目、つまりTestCalcの各メソッドを表しています。 エラーがあると次のようなメッセージが出力されます。

Run options: --seed 34278

Running:

.F

Failure:
TestCalc#test_parse [test.rb:24]:
In stdout.
--- expected
+++ actual
@@ -1,2 +1,2 @@
-"100.0
+"100
 "



rails test test.rb:23



Finished in 0.010876s, 183.8891 runs/s, 4505.2836 assertions/s.
2 runs, 49 assertions, 1 failures, 0 errors, 0 skips

Failureはテストで失敗したことを示しています。 この他にErrorが出ることがありますが、それはプログラムを実行した時にエラーがあったことを意味しており、テストの結果ではありません。 上記ではexpectedがマイナスでactualがプラスで表されているので、「100.0」になることを期待してテストしたが、実際は「100」になったということを表しています。

すべての場合をテストするのは無理なので、典型的な例をテストすることになります。 プログラムのエラーは境界で起こりやすいです。 例えば正負が問題になるプログラムでは0が境界です。 「(変数)>= 0」を使わなければいけないのに「(変数)> 0」を使うといったバグは0以外ではフェイル(失敗)が起こりません。 ですから「境界をテストする」ことは非常に重要です。

GitHubのcalcにはtest.rbというテストプログラムがついているので参考にしてください。

なお、minitestを使うことを考えると、トップレベルだけでプログラムを作るのは得策ではありません。 仮にインスタンス変数をトップレベルで使うと、minitestからは参照できなくなります。 それは、テストのためのメソッドがTestCalcクラスで定義されているので、テスト時のインスタンスはトップレベルではないためです。 ですから、上の例のように、アプリの中でクラスを定義し、それをsetupメソッドでインスタンス生成して使うのが良い方法です。

Readme.md

簡単なドキュメントは付けておくべきです。 仮に公開しなくても、将来自分自身が見直す時に役に立ちます。 2週間別の仕事をすると、元の仕事内容を思い出すのに結構な時間がかかります。 そのときにドキュメントは役に立つでしょう。

GitHubに公開する場合はReadme.mdのようなファイル名をつけることになっています。 拡張子のmdはMarkdown形式を表します。 Markdownはhtmlと比べ格段に見やすく、書きやすいので勧められる形式です。 Markdownの説明は、次の記事を参考にしてください。

ただし、はてな記法などのはてな独自の記法はGitHubでは使えません(GitHubMarkdownはGFM)。

GitとGitHub

プログラムを公開するならばGitHubは無料で、機能が充実していて、有力な選択肢です。 GitHubとGitについては「はじめてのJekyll+GitHub Pages」の中に書かれていますので、以下を参考にしてください。

  • 第3章 GitHub pagesクイックスタート
  • 第7章 Gitの使い方
  • 第10章 GitをSSHで使う方法

が参考になります。 このうち第10章のSSHで使う方法は知らなくても大丈夫です。

今回はアプリ開発の実際を見てきましたが、いかがだったでしょうか。 簡単なアプリで良いのでぜひ作ってGitHubにあげてみてください。 作れば作るほどプログラミングのレベルは上がります。

徒然Ruby(19)Case文

if〜elsif〜・・・〜else〜endは皆さん良く使うでしょうか? これは場合分けで良く使われる方法です。 これと同様の制御構造にcase文があります。 Cのswitch文に似ていますが、より強力な機能を持っています。 if-else-endよりも高い能力があるといえます。

case文の使い方

case文は次のような構造で使います。

case [式]
[when 式 [, 式] ...[, `*' 式] [then]
  式..]..
[when `*' 式 [then]
  式..]..
[else
  式..]
end
  • caseの次にある「式」に対してwhenの次の「式」(またはその並びのうちのどれか)が一致したときthen以下が実行される。 最初に一致したwhen節があれば、残りのwhen節の比較、実行は行われず、case文全体の次(endの先)に実行が進む
  • whenの式との比較は===メソッドを使う(==ではない)。 ===メソッドのレシーバはwhen節の式の値となるオブジェクトである。 case文の後の式は===メソッドの引数となる
  • どのwhen節にも一致しなければelse節が(あれば)実行される
  • then後の式を次行以下に書く場合はthenを省略できる

これより、次のcase文とif-else文はほぼ同等です。

case x
when 1 then p "x is 1."
when 2 then p "x is 2."
else p "x is not 1 or 2."
end

if 1 === x then p "x is 1."
elsif 2 === x then p "x is 2."
else p "x is not 1 or 2."
end

なお、when節の式の前にアスタリスク*)を付けると、式は展開されます。

when *[1,2,3] => when 1,2,3 となる

case文のような構造を条件分岐ともいいます。 条件分岐はプログラムで最も良く現れる構造です。

==と===の違い

=====はオブジェクトごとにメソッドとして定義されています。 ほとんどのメソッドの祖先であるObjectクラスでは、=====の別名になっていますので、その子孫クラスで===を独自に定義していなければ=====は同じ動作をします。 =====と違うのはビルトイン・クラスでは次のものです。

  • Methodクラス。 ==はオブジェクトとして等しいかどうかを返す。 ===は右辺を引数にメソッドオブジェクトを実行した結果を返す
  • Moduleクラス。 ==は同じモジュールかどうかを返す。 ===は右辺がそのモジュールのサブクラス(子だけでなく子孫すべて)のインスタンスであるときtrueを返す。 すなわち、A===bb.kind_of?(A)は同じ値を返す。 なお、ClassクラスはModuleのサブクラスで===メソッドを継承しており、同様に使うことができる RefinementクラスもModuleのサブクラスで===メソッドを継承しているが、このクラスを使うのはライブラリなどに限ると思われる
  • Procクラス。 ==はオブジェクトとして等しいかどうかを返す。 同じ手続きを表すProcオブジェクトでも異なるインスタンスはfalseになる。 p proc{|x| x*x} == proc{|x| x*x} #=> false。 実際問題として==はほとんど役に立たない。 ===は右辺を引数にProcオブジェクトを実行した結果を返す。 Methodクラスの実装と考え方は全く同じ
  • Rangeクラス。 ==は同じ範囲を表していればtrueを返す。 すなわち、それぞれの端が同じ(==)であり、端の含み方が同じ(..なのか...なのか)ときにtrue。 ===は引数がRangeオブジェクトの範囲の中にあればtrue。
  • Regexpクラス。 ==正規表現として同じであればtrueを返す。 ===は引数の文字列またはシンボルがマッチすればtrueを返す。 if文では良く=~が用いられるが、=~===はほとんど同じ(ただ返し値は異なる)。

===はcase文専用です。 ですので、完全なイコールでなく、マッチに近いメソッドになっています。

特にProcは非常に柔軟なマッチを可能にしています。 Methodも同様ですが、Procの方がよく使われるのではないかと思います。

引数のチェック(クラスのwhen節)

Rubyの変数は任意のオブジェクトを代入できます。 これは変数自体には型がないということです。 そのため、コンパイル時にメソッドの引数の型をチェックすることができません。

※ Rubyはソースを中間コードにコンパイルしてから実行しています

C言語は変数に型があり、パラメータに対しても型を指定できます。

int square (int x) {
  return x*x
}

このCの関数のパラメータは整数型です。 コンパイル時にこの関数を整数型以外の引数で呼び出している文があれば、エラーになります。

Rubyでは、このようなチェックは実行時に行うしかありません。 メソッドの最初に引数のオブジェクトがどのようなクラスのオブジェクトかを調べます。 次の例は引数を2乗して返すメソッドです。

# 引数を2乗して返すメソッド
def square x
  case x
  when Numeric
    x*x
  else # impossible to calculate, return nil
    return nil
  end
end

p square("abc") #=> nil (文字列は数字でない)
p square(2) #=> 4
p square(1.2) #=> 1.44
p square(Complex("1+2i")) #=> (-3+4i) 複素数も計算できる
p square(Rational("2/3")) #=> (4/9) 分数(有理数)も計算できる

数字のクラスInteger(整数)、Float(浮動小数点数)、Complex(複素数)、Rational(有理数)はすべてNumeric(数)のサブクラスです。 when Numericの節では、上記の4つのオブジェクトであるかをひとつのチェックで済ましています。 else(それ以外)のときはnilを返すことにします。 これは積極的なエラー対策ではなく、呼び出し側にエラー処理を任せるやり方です。

squareを呼び出したとき、文字列ではnilが返り、整数、浮動小数点数複素数有理数では2乗された値が返されています。

このような引数のクラスチェックは特にライブラリでは重要です。 ライブラリの作成者と使用者は異なるのが普通であり、作成者に予想外の使われ方をするかもしれません。 そのためこのようなチェックは非常に重要になります。

引数のチェック(Procオブジェクトのwhen節)

Procオブジェクトを使うと複雑な条件を作ることができます。 次の例は配列の各要素を2乗するメソッドsquare_elementsです。

各要素を2乗するだけなら、配列のmapメソッドで簡単に実現できますが、要素に数以外のものが入っているとエラーが起こります。 そこで、配列の要素がすべてNumericのサブクラスのインスタンスかどうかを調べます。 そのためにProcオブジェクトを使います。

@is_array_of_Numeric = lambda do |a|
  return false unless a.instance_of? Array # lambdaではreturnで手続きオブジェクトを抜けることができる
  b = a.map{|e| e.kind_of?(Numeric)}.uniq #要素のクラスの配列を作り、重複を除く
  case b.size
  when 0 then false # 空の配列だった
  when 1 then b[0] # true(Numericクラス)かfalse(そうでない)を返す
  else        false # Numericとそうでない要素が混じっていた
  end
end

def square_elements a
  case a
  when @is_array_of_Numeric
    a.map{|x| x*x}
  else
    a
  end
end

p square_elements([1,2,3]) #=> [1, 4, 9]
p square_elements([1.0, 2,Complex("2+3i"),Rational("5/7")]) #=> [1.0, 4, (-5+12i), (25/49)]
p square_elements(["a","b","c"]) #=> ["a", "b", "c"]
p square_elements([[1,2],[3,4],[5,6]]) #=>[[1, 2], [3, 4], [5, 6]]
p square_elements([[1,"2",3.0]]) #=> [[1, "2", 3.0]]

インスタンス変数(@つき変数)をProcオブジェクト名に使っているのは、メソッド定義の中で参照できるようにするためです。 型チェックはかなり複雑です

  • 引数が配列でない=>false
  • 引数が空の配列=>false
  • 引数の要素がすべてNumericのサブクラスのインスタンス=>true
  • 引数の要素にNumericのサブクラス以外が混ざっている=>false

これをメソッドの中で書くこともできますし、例のようにメソッドの外でProcオブジェクトにすることもできます。 もちろん、メソッドの中でProcオブジェクトを生成しても良いのですが、メソッドがコールされるたびにProcオブジェクトが生成され、効率が悪くなります。 このチェックが複数のメソッドで必要ならば、このようにメソッド外でProcオブジェクトを作るのが有効なやり方です。

字句解析(正規表現のwhen節)

字句解析は、主にプログラミング言語の処理系で行われます。 例えば、次のようなRubyプログラムをrubyが処理することを考えてみましょう。

ab = 10 * cd

このとき、これを次のように解析します。

文字列 タイプ
ab 識別子
= 等号
10 整数
* 掛け算
cd 識別子

この表はruby処理系の実際の動作を説明するものではありません。 あくまでも、字句解析の例として提供するものです。

「識別子」は定数名、変数名やメソッド名などに用いられる文字列です。

字句解析は頭から順にその言語の要素になるもの(トークンという)を取り出し、そのタイプと内容を返していくものです。

ここでは、簡単な電卓プログラムの字句解析を考えてみましょう。 電卓には、変数、整数、四則、代入、表示ができるとします。

トーク タイプ 内容
英文字列 :id 変数名
数字の文字列 :num 整数
+ :'+' 加算記号
- :'-' 減算記号
* :'*' 乗算記号
/ :'/' 除算記号
( :'(' 左括弧
) :')' 右括弧
= :'=' 代入
print :print 表示命令

全部で10種類のタイプのトークンがあります。 入力文字列を分析し、トークンの列の配列resultを返すメソッド「lex」を作ってみます。

例えば、入力が

abc = (2+3)*6
print abc

であったとき(つまり文字列"abc = (2+3)*6\nprint abc\n"であったとき)、lexの出力は

[[:id, "abc"], [:"=", nil], [:"(", nil], [:num, 2], [:+, nil], [:num, 3], [:")", nil], [:*, nil], [:num, 6], [:print, nil], [:id, "abc"]]

になります。 字句解析の流れは、入力の最初の文字に対するcase文の条件分岐が主になります。

def lex(s)
  result = []
  while true
    break if s == ""
    case s[0]
    when /[[:alpha:]]/
      m = /\A([[:alpha:]]+)(.*)\Z/m.match(s)
      if m[1] == "print"
        result << [:print, nil]
      else
        result << [:id, m[1]]
      end
      s = m[2]
    when /[[:digit:]]/
      m = /\A([[:digit:]]+)(.*)\Z/m.match(s)
      result << [:num, m[1].to_i]
      s = m[2]
    when /[+\-*\/()=]/
      result << [s[0].to_sym, nil]
      s = s[1..-1]
    when /\s/
      s = s[1..-1] 
    else
      raise "Unexpected character."
    end
  end
  result
end

p lex("abc = (2+3)*6\nprint abc\n")
  • 入力(sに代入される引数の文字列)の1文字目によって場合分けをする
  • /[[:alpha:]]/は英字(大文字小文字の両方可)にマッチする正規表現
  • 正規表現/\A([[:alpha:]]+)(.*)\Z/mにおいて、\A\Zはそれぞれ文字列の先頭と末尾にマッチ。 [[:alpha:]]+は1文字以上の英字の繰り返しにマッチ。 .*は任意の文字の0個以上の繰り返しにマッチ。 .はデフォルトでは改行にマッチしないが、正規表現の最後にmオプションがつくときは、改行にもマッチする。 ()が2箇所にあるので、MatchDataオブジェクトの[1][2]でマッチした部分文字列を参照できる
  • 文字列m[1]がprintであれば、予約語なので[:print, nil]を配列resultに加える。 printでなければ、変数なので[:id, m[1]]、すなわち識別子タイプを表す:idと変数名の文字列をセットにしてresultに加える。 s = m[2]でsにはマッチした文字列を除いた残りを代入する
  • /[[:digit:]]/は数字(0-9)にマッチ。 変数のときと同様に数字の並びを取り出し、:numタイプと整数値をセットにしてresultに加える
  • /[+\-*\/()=]/は四則、括弧とイコールのどれかにマッチする。 それぞれの記号を表すシンボルをタイプとし、対応する内容が無いのでnilとセットにし、resultに追加する。 `s[1..-1]は先頭の1文字を削除し、次の文字から最後の文字までを返す
  • /\s/は空白文字にマッチ。 空白文字にはタブや改行も含まれる。 この文字は区切りとしての意味しかない。 resultに付け加えるものは何もない
  • それ以外は、電卓で使う文字ではないので、raiseメソッドで例外を発生させ、プログラムを停止する

この例では、正規表現をcase文に使いました。 ===正規表現のマッチを意味するので、このようにwhen節に使うことができます。

以上case文を見てきましたが、if-else-end文よりも条件のチェック部分に工夫があります。 これは具体的には===メソッドで実現されています。 もし、新たにクラスを作成する時に、そのオブジェクトがwhen節で使える可能性があれば、かならず===メソッドを定義してください。

徒然Ruby(18)Lambda

Procオブジェクトを生成するメソッドlambdaについて説明します。

lambdaの使い方

lambdaはKernelモジュールのメソッドで、使い方は前の記事のprocと同じです。 lamdaの後にブロックを書き、そのブロックがProcオブジェクトになります。

a = lambda{|x| print x, "\n"}
a.call("Hello")
a["Hi"]
  • ブロックをProcオブジェクトにして変数aに代入
  • callメソッドでブロックを実行
  • []メソッドでもブロックを実行できる

lambdaの代わりにprocを使っても結果は同じです。

lambdaとprocの違い

Lambdaとprocで作られるProcオブジェクトの振る舞いには次の違いがあります。

  • lambdaで作られたProcオブジェクトのパラメータの数と、呼び出し側の引数の数が違うとエラーになる。 procで作られたProcオブジェクトではエラーにならず、パラメータが余ればnilが代入され、引数が余れば捨てられる。
  • lambdaで作られたProcオブジェクトの中でreturnあるいはbreakが呼ばれたときエラーにならず、Procオブジェクトを抜ける。
a = proc{|x,y| p x; p y}
a.call(1)
a.call(1,2,3)

これを実行すると

1
nil
1
2

となります。 procの代わりにlambdaを使うと引数の違いによるエラーになります。

return、break、nextの違いを表にまとめると次のようになります。

return break next
proc proc定義の外で定義されたメソッドを抜ける エラー Procオブジェクトを抜ける
lambda Procオブジェクトを抜ける Procオブジェクトを抜ける Procオブジェクトを抜ける

また、メソッド定義の中でprocメソッドで作られるProcオブジェクトを、メソッドの外で呼び出したとき、そのオブジェクトの中にreturnまたはbreakがあるとエラーになります。 lambdaではエラーになりません。

def b
  @a = proc{return}
  @a.call
  p 10
end

b
@a.call

これを実行すると

example18.rb:2:in `block in b': unexpected return (LocalJumpError)
        from example18.rb:8:in `<main>'
  • 7行目でメソッドbを呼び出している。 メソッドbはreturn文のあるブロックをProcオブジェクトにして@aに代入し、続けて@a(Procオブジェクト)を呼び出している。 Procオブジェクトはreturnを実行し、メソッドbを抜ける。 そのため次の行のp 10は実行されない
  • 8行目でProcオブジェクトが呼ばれるが、メソッド内で定義した「returnを含むProcオブジェクト」をメソッド外で呼んだのでエラー(LocalJumpError)になる

細かい説明になりましたが、ざっくりというと「lambdaで作ったProcオブジェクトは、procで作ったProcオブジェクトより、振る舞いがメソッドに近い」といえます(メソッドも引数の数のチェックがあり、returnで呼び出し側に戻ります)。

このことから、「名前のないメソッド」のようにProcオブジェクトを扱いたいときはlambdaを使うのが良いといえます。 ネット上のrubyのプログラムを見ると、procよりlambdaが多く用いられているようです。 それはこのような理由によるものと推察されます。

逆に&を使ってブロックとして使うProcオブジェクトはprocで生成するのが良いと思います。 なぜならprocの後に続くブロックをそのまま&引数の代わりにあてはめてデバッグ(テスト)することができるからです。 lambdaの場合、returnやbreakが記述されていると、直接のブロックにしたとき動作がおかしくなりますし、引数のチェックも本来のブロックとは異なります。 もちろん、注意して使えばlambdaで生成したProcオブジェクトに&をつけてブロックにしても問題はありません。

Procクラスのインスタンス・メソッド

<<、>>

a << bは糖衣構文で、a.<<(b)に等しくなります。 すなわち<<はProcクラスのインスタンスメソッドです。 このメソッドは「bに引き続きaを実行する新たなProcオブジェクト」を返します。 ちょっと分かりにくいと思うので、図で説明します。 a、bともに、整数をパラメータで受け取り、整数を返すProcオブジェクトだとします。 これを図で次のように表しましょう。

整数 --->>> (a) --->>> 整数
整数 --->>> (b) --->>> 整数

これをb、aの順に連続的に行います。

整数 --->>> (b) --->>> 整数 --->>> (a) --->>> 整数

左端の整数に対して右端の整数を返すProcオブジェクトがa << bです。 例えばaが「2を加える」、bが「2乗する」としましょう。

整数(x) --->>> (b) --->>> 整数(x^2) --->>> (a) --->>> 整数(x^2+2)

となります。 ここでハットマーク(^)は累乗を表します。

a = lambda{|x| x+2}
b = lambda{|x| x*x}
c = a << b
p c.call(5)
c = b << a
p c.call(5)

実行すると次のようになります。

27
49

a << bでは「5を2乗して、2を加える」ので27になり、b << aでは「2を加えてから2乗する」ので49になります。

パラメータと返し値が両方整数の例を示しましたが、どのようなオブジェクトでも良いし、パラメータと返し値が複数(その場合は配列を返すことになる)でも構いません。 ただ、a << bにおいては、bの返し値がaの引数になるので、そのオブジェクトのタイプと数が一致してないと上手く動作しません。

次の例は、文字列を単語に分解して配列にし、その配列の要素数を求めるプログラムです。

文字列 --->>> b --->>> 配列 --->>> a --->>> 整数

入出力が文字列、配列、整数の異なる3種類のオブジェクトになっています。

a = lambda{|x| x.size}
b = lambda{|x| x.split(/\W/)}
e = "I declare before you all that my whole life, whether it be long or short, "\
"shall be devoted to your service and to the service of our great imperial family to which we all belong."
p (a << b).call(e)

実行すると37(語)となります。 なお、これはエリザベス女王の1947年のスピーチです。

a >> bは左から右への演算記号であり、<<と逆に「aを実行してからbを実行」するProcオブジェクトを返します。

ところで、先程の例では変数名がaとbで、なんとも気が利かない名前です。 これをcountとget_wordsにすれば

p (count << get_words).call(e)

で読みやすくなるのではないでしょうか。

curry

curryはラムダ計算という数学の理論に出てくる「curry化」という変換です。 例をあげて説明します。 数式でx+yという関数は変数が2つある関数です。

 f(x,y)=x+y

xとyに2と3を代入すれば、関数値は5になります。 このとき、この操作を2つに分けてみましょう。 まず2を代入した関数を考え、次に3を代入して値を求めます。

 f(2,y)=2+y

この関数はyだけの関数ですので改めてgと書けば

 g(y)=2+y

y=3を代入すると最終的な値5が求められます。

 g(3)=2+3=5

この操作は次のように2段階になります。

2に対して「g(y)=2+yという関数」が対応 => 一般にはxに対してg(y)=f(x,y)が対応。
この対応を新たに関数hとすると、h(x)=f(x,y)=g(y)。
3に対して関数g(y)の値が5になる
まとめると
2 --->>> h --->>> g(y)=2+y
3 --->>> g --->>> 5
すなわち
(h(2))(3)=g(3)=5

関数hの値が「数ではなくて関数」であるところがポイントです。 元の2変数関数f(x,y)は2つの1変数関数h(x)とg(y)に分解され、

 f(x,y) = (h(x))(y)

となりました。 これをcurry化といいます。

curryメソッドはProcオブジェクトのcurry化をします。

a = lambda{|x,y| x+y}
b = a.curry #=> lambda{|x| lambda{|y| x+y}}
c = b.call(2) #=> lambda{|y| 2+y}
p c.call(3) #=> 5

コメントでそれぞれの計算結果を書いておきました。 2行目がややこしいのですが「xに対してProcオブジェクトlambda{|y| x+y}が対応する」というProcオブジェクトがcurry化によって返されます。 これが「xに対してg(y)=x+yが対応する」ということに対応しているのですが、ややこしいですね。

ラムダ計算をするときは、アロー演算子を用いるほうがラムダ計算らしいです。 また、callメソッドの代わりに[]を使うと更にラムダ計算らしくなります。

# アロー演算子での表現
a = ->(x,y){x+y}
b = a.curry #=> ->(x){->(y){x+y}}
c = b[2] #=> ->(y){2+y}
p c[3] #=> 5

配列のmapメソッドで各要素を2乗する計算をcurryを使って表現してみます。

# 配列のmapで要素を2乗するのをProcオブジェクトで表しcurry化
a = ->(x,y){y.map(&x)} # yが配列でxがProcオブジェクト
b = ->(x){x*x}
p a.curry[b][[1,2,3,4]]

square = a.curry[b] # cは配列要素を2乗するProcオブジェクト
p square[[5,6,7,8,9,10]]

実行すると

[1, 4, 9, 16]
[25, 36, 49, 64, 81, 100]

となります。 最後にsquareという「配列要素を2乗する」Procオブジェクトを手に入れることができました。 これはcurry化のひとつのメリットだと思います。 つまり「整数を2乗する」手続きから「配列の各要素を2乗する」といういわば「格上げ」された手続きを得ることができました。

同様に「整数に2を加える」を「配列の各要素に2を加える」に格上げしてみましょう。 そして、さらに<<演算子で両者を繋げてみます。

add2 = a.curry[->(x){x+2}]
p add2[[5,6,7,8,9,10]] #=> [7, 8, 9, 10, 11, 12
p (add2 << square)[[5,6,7,8,9,10]] #=> [27, 38, 51, 66, 83, 102]
p (square << add2)[[5,6,7,8,9,10]] #=> [49, 64, 81, 100, 121, 144]

このように「a.curry」によって任意の整数に対する手続きは配列に対する手続きに格上げできます。 「配列の要素に特定の操作を行う」ことがプログラムのあちこちで必要なとき、その手続きオブジェクトをcurryメソッドを用いて作っておけば無駄な作業が省け、可読性もあがります。

まとめ

最後の方が難しくなってしまいましたが、いかがだったでしょうか。 Procオブジェクトは使い方によってはプログラムを分かりやすく効率的にします。 メソッドに慣れている人が多いと思いますが、レシーバの関係無い手続きはトップレベルのメソッドでもProcオブジェクトでも可能です。 Procオブジェクトは無名のオブジェクトなので、扱いが柔軟にできるメリットがありますし、curry化も上手く使うと便利なことがあります。 ぜひProcオブジェクトに慣れて効果的に使ってください。

徒然Ruby(17)Procオブジェクト

今回はブロックを一般化したオブジェクトProcを説明します

Procオブジェクトとは

メソッドにはブロックをつけることができます。

[1,2,3].each {|x| print x, "\n"}
# ブロックはdo〜endでも表せる
[1,2,3].each do |x|
  print x, "\n"
end

ブロックはオブジェクトにすることができます。 ブロックは動作を表すので、それがオブジェクトにできるというのは分かりにくいかもしれません。 しかし、考えてみれば「プログラム」はすべて文字列で表されており、文字列はオブジェクトなのですから、ブロックがオブジェクトになることは不思議でもなんでもありません。 ブロックのオブジェクトはProcクラスに属します。

Procオブジェクトの作り方

ProcオブジェクトはProcクラスのオブジェクトですからProc.newインスタンス化できます。 そのインスタンスの内容となるブロックは、Proc.newの後ろにつけます。 そして、そのProcオブジェクトを実行するにはcallメソッドを使います。

b = Proc.new {|x,y| x+y}
p b.call(10,20) #=> 30

Procオブジェクトは他のオブジェクト同様に

  • 変数に代入することができる
  • 式の中で使うことができる
  • メソッドの引数にすることができる

という性質があります。

b = Proc.new {|x,y| x+y}
p b.call(10,20) #=> 30
p 100+b.call(10,20) #=> 130

def abc(x, y, z)
  print x.call(y, z), "\n"
end

abc(b, 15, 25) #=> 40

この例でメソッドabcの引数にProcオブジェクトが代入されたのはすぐには理解が難しいかもしれません。 ですが、これは「慣れ」の問題だと思います。

Proc.newを含め、Procオブジェクトの作り方は3通りあります。

  • Proc.new
  • procメソッド(Kernelモジュールのメソッド)
  • lambdaメソッド(Kernelモジュールのメソッド)。 lambdaメソッドの別形式であるアロー演算子->)を用いることもできる

それぞれについてプログラム例を示します。

b = Proc.new {|x,y| x+y}
p b.call(10,20) #=> 30
b = proc {|x,y| x*y}
p b.call(10,20) #=> 200
b = lambda {|x,y| x-y}
p b.call(10,20) #=> -10
# lambdaの新しい記法。->は「アロー演算子」と呼ばれる。
b = ->(x,y){(x+y)**2}
p b.call(10,20) #=> 900 = (10+20)^2
# [ ]を用いてもProcを呼び出せる
p b[10,20] #=> 900
# b.call(10,20)の糖衣構文
p b.(10,20) #=> 900
  • どのやり方でも、後ろにブロックをつけ、そのブロックをオブジェクトにしたものが返される
  • lamdaの別記法としてアロー演算子がある。 アロー演算子ではブロックのパラメータが波括弧の外に出て、かつ丸括弧で囲まれる。 この演算子は「ラムダ計算」の記法からきている。 ->ギリシャ文字のラムダ( \lambda)の下半分が左にずれた形が由来だということがDavid Flanaganの「The Ruby Programing Language」に書いてあった。 この記法が好きなユーザもいるが、ラムダ計算を意識するのでなければlambdaメソッドを使うのが良いと思う(私の考え)
  • Procオブジェクトを呼び出す別の方法として[]を使う方法もある。 また、糖衣構文で.( )という書き方もある

Proc.newとprocメソッドは同じオブジェクトを作成します。 lambdaメソッドで生成したオブジェクトはそれらと振る舞いが違います。 後の記事で説明しますが、詳細はRubyのドキュメントを参照してください。

今回はProc.newまたはprocメソッドで作成したProcオブジェクトについて説明します。

Procオブジェクトの使用例

ブロックは、何らかのプログラムを実行することから、メソッドに似たものだと思われがちです。 しかし、メソッドとProcメソッドでの大きな違いのひとつは、その名前です。 メソッドが名前(メソッド名)を持ち、メソッド名で呼び出されるのに対し、Procオブジェクトには名前がありません。 変数に代入すれば、変数名で参照できますが、それはオブジェクトの名前ではありません。 しかし、名前でメソッドを呼ぶことに慣れている人は、Procオブジェクトにも名前をつけがちです(その名前は実は変数名ですが)。

sum = proc{|array| array.sum}
p sum.call([1,2,3]) #=> 6

変数名sumが無くても同様のプログラムは作れます。

p proc{|array| array.sum}.call([1,2,3]) #=> 6

このように名前が無いことはメソッドよりも自由度が高いということです。 そして、Procオブジェクトがオブジェクトである以上、他のオブジェクト同様に次のことが可能です。

  • 変数、定数に代入できる
  • 配列、ハッシュの要素になることができる
  • メソッドの引数になれる

これ以外にもオブジェクトを置くことのできるところにはProcオブジェクトを置くことができます。 Procオブジェクトをハッシュの要素にする例を示しましょう。 これは配列の数値データを統計データと見て合計や平均などを表示するプログラムです。

sum = proc{|a| a.sum}
mean = proc{|a| (a.sum.to_f/a.size).round(1)}
var = proc{|a| (mean.call(a.map{|x| (x-mean.call(a))**2}).round(2))}
stdev = proc{|a| (var.call(a)**0.5).round(2)}
max = proc{|a| a.max}
min = proc{|a| a.min}
sort = proc{|a| a.sort}
reverse = proc{|a| a.reverse}
@stat_functions = {sum: sum, mean: mean, var: var, stdev: stdev, max: max, min: min, sort: sort, reverse: reverse}

def report(items, d)
  items.each do |item|
    print "#{item}: #{@stat_functions[item].call(d)}\n"
  end
end

items = [:sum, :mean, :stdev, :max, :min, :sort]
data = [5,3,10,2,6]
report(items, data)

はじめに合計(sum)から逆順並べ替え(reverse)までのProcオブジェクトを変数に代入しています。 実は直接次の@stat_functionsに代入するハッシュのリテラルに書き込みたかったのですが、分散と標準偏差でProcオブジェクトを使うため断念し、このような形にしました。 なお、meanは平均のことで、統計学ではaverageでなくmeanを使います。 すべてのProcオブジェクトのパラメータは、整数または実数を要素とする配列を想定しています。 すべてのProcオブジェクトをひとつのハッシュに組み込んでいます。

メソッドreportは項目とデータ(配列)からそれを表示するものです。 項目は、@stat_functionsのキーとなっているシンボルのうち、表示したいものを集めた配列です。

最後の3行で項目とデータを指定し、そのレポートを表示しています。 実行すると次のように表示されます。

sum: 26
mean: 5.2
stdev: 2.79
max: 10
min: 2
sort: [2, 3, 5, 6, 10]

項目を変えると、表示も変わってきます。

このようにProcオブジェクトは、ハッシュに埋め込んだり、配列に埋め込んだりすることができます。 また名前がないので、名前の衝突を心配する必要もありません。 RubyプログラマはあまりProcオブジェクトを使っていないように見受けられますが、もっと活用できるし、活用すべきだと思います。

Procオブジェクトをブロックとして使う

Procオブジェクトはブロックをオブジェクト化したものだから、ブロックとしても使えます。 そのときには、メソッドの最後の引数に入れ、かつ&記号をつけます。

ブロックを直接書くのとProcオブジェクトで渡すのと両方示しますので比べてください。

show = proc{|x| print x, "\n"}

# ブロックを直接書く
[1,2,3].each{|x| print x, "\n"}

# Procオブジェクトを使う
[1,2,3].each(&show)

両方とも同じもの(1、2、3)が表示されます。 比べてみるとeachメソッドの後ろにブロックが付くか、Procオブジェクトが付くかの違いだけだとわかります。 実は、ブロックはeachメソッドの最後の引数になっているのです。 直接ブロックを書くのとProcオブジェクトを渡すのは同じではありませんが、ほぼ同等に思って構いません。

この&を使う方法はメソッドをオブジェクト化したMethodオブジェクトにも使うことができます。

# Methodオブジェクトをブロックに使う
[1,2,3].each(&method(:p))

methodはObjectクラスのメソッドで、引数の名前(メソッド名はシンボルを使います)のメソッドをMethodオブジェクトに変換します。 このプログラムは

[1,2,3].each{|x| p x}

と同様の働きをします。

このとき&method(:p)の「methodメソッド」を省略して`&:p'とするとエラーになります。 この2つは似ていますが、

  • Methodオブジェクトに&をつけるとブロックに渡される引数がそのままメソッドの引数になる
  • Symbolオブジェクト(そのシンボルはメソッド名)に&をつけるとブロックに渡される第1引数をレシーバとしてメソッドが呼ばれる

例えば

# シンボルに&をつけると第1引数がレシーバになる
p [1,2,3].map(&:to_s)

これを実行すると

["1", "2", "3"]

と表示されます。 つまり、mapの引数は順に1、2、3なので、それをレシーバとして1.to_sから3.to_sが実行されて配列が返されます。

[1,2,3].each(&:p)とすると、1.pを計算しようとしてエラーが起こります。

本題に戻ります。 以上をまとめると、Procオブジェクトは&をつけてメソッドの最後の引数にすると、その部分がブロックとしてメソッドで実行される、ということです。

メソッドとブロックの違い

ローカル変数の違い

メソッド定義の外側で定義されたローカル変数は。メソッド定義内では参照できません。

a = 10

def abc
  p a #=>未定義のaを参照し、エラーが起こる
end

これに対してブロックの外側で定義されたローカル変数はブロック内からも参照できます。

a = 10

[1,2,3].each do |x|
  p a+x
end

このとき、ブロック内でもa=10となるので、

11
12
13

と表示されます。 これはProcオブジェクトの定義でも同じで

a = 10
b = proc {|x| a + x}
p b.call(20) #=> 30

変数aはprocメソッド後のProcオブジェクト呼び出しでも参照できます。

ブロック内で定義されたローカル変数はブロックの終了とともに参照できなくなります。

[1,2,3].each{|x| c = 10+x}
p c #=> cはブロックの外では参照できず、未定義の変数参照としてエラーになる

Procオブジェクトは(procメソッドの前に定義された)ローカル変数を保持し続けます。

注意:procメソッド内で定義されたローカル変数のスコープは(procメソッドの)ブロック内のみですので、procメソッドの終了とともに消えてしまいます。

a = 10
b = proc{ a }
p b.call #=> 10
a = 20
p b.call #=> 20

このようになるのは、変数bの参照するProcオブジェクトが変数aを持ち続けるからです。 もし、このプログラムがメソッド定義の中で行われたとしましょう。 そのメソッドが実行された時にProcオブジェクトが生成されます。 もしこのProcオブジェクトがメソッドから抜け出た後も残っていたら、メソッドのローカル変数aは外部からは使えませんが、 Procオブジェクトのaaの指すオブジェクトはずっと残ることになります。 そうでなければローカル変数はメソッド実行が終了した時点で消え、オブジェクトもガベージコレクションで消えるでしょう。

このようにProcオブジェクトはブロックとしての動作だけでなくローカル変数とオブジェクトを保持することに注意してください。 これもメソッドとの大きな違いです。 このようなオブジェクトを保持するオブジェクトはクロージャーと呼ばれます。

return、next、break
  • returnはメソッドを終了して呼び出し元に返る
  • nextは最も内側のループの現在の回を終了して次の回に移る。 ブロックの場合も同様。 yieldを抜け出すと考えても良い
  • breakは最も内側のループを抜け出す。 ブロックの場合も同様で、ブロックの外に抜け出す

注意しなければならないのは、これらの使い方です。

  • ブロック内でreturnを使うとブロックの外側のメソッド(メソッド定義されているメソッド、実行時のメソッドではない)から抜け出すことになる
  • ブロック内でbreakを使うとブロックが付いているメソッドの外側に出る(そのメソッドが一番内側のループだから)
  • callで呼ばれたProcオブジェクトから抜け出して元に戻るにはnextを使う

要するに、メソッド定義でメソッドを終了して呼び出し元に戻る命令はreturnで、Procオブジェクトではnext。

メソッドとProcオブジェクトは似ているが、このあたりの違いを押さえておかないと悲劇が待っているかも・・・