オブジェクト指向と関数的手続き

オブジェクト指向プログラミングでは処理はメソッドで表わされる。メソッドはなんらかのクラスに属していて、そのインスタンスのデータにアクセスすることができる。つまり、メソッドは常になんらかのインスタンスを環境*1として実行される手続きだ。

しかしあらゆる手続きが、このようなアクセスを要するわけではない。例えば、文字列の配列を連結するメソッド Array#join がそうだ。

[1] pry(main)> ['apples', 'pears', 'mandrills'].join(', ')
=> "apples, pears, mandrills"

興味深いことに、Python は逆にこの手続きを文字列のメソッドにしている。

>>> ', '.join(['apples', 'pears', 'mandrills'])
'apples, pears, mandrills'

実は join は Array クラスや String クラスに属している必要はない。次のように関数(トップレベルのメソッド)として実装することもできる。

def join ary, sep
  result = ""
  ary.each_with_index do |str, i|
    result += sep unless i == 0
    result += str
  end
  result
end

Array と String の公開されたインターフェイスのみを用いて実装できるのだから、join は getter ではないし、当然 setter でもない。この際、手続きがどちらのクラスに属すべきかということは恣意的だ。

言い換えれば、Array を self にすることも、String を self にすることも、そのどちらも self にしないこともできる。ただし、self を無くすことは、オブジェクト指向のモットーに反するので出来無い。

このデータ中心の考え方は、状態を変化させてプログラミングを行うことを前提に置いているように思う。イミュータブルな Integer の times メソッドを見てみよう。特定回数の繰り返しを行う手続きはどれだけ Integer のメソッドである必然性があるだろう? (文法的ぎこちなささえなければ) times は容易に Proc のメソッドにもし得る。

クラスを書いている途中でこういう「関数的」手続きをメソッドにしたいことはよくある。クラスメソッドにすれば呼び出す度にクラス名を書かなければならないし*2、かと言ってインスタンスメソッドにすれば、不必要にオブジェクトの内部状態にアクセスできる場所に置くことになる。当然トップレベルには置きたくないし、かといって引数のクラスを開いて追加するほど一般的な処理でもない……。

こうした「関数的」手続きは、オブジェクト指向パラダイムの時代にあっては自分の居場所を探しつづける運命を宣告されているのかもしれない。

*1:self とも言う。

*2:self.class.func としても呼び出せるが、残念ながら class は予約語だから class.func とはできない。