読者です 読者をやめる 読者になる 読者になる

PHP5.4のトレイトを学んだらRubyのmixinがちょっと変な気がした

Ruby

あ、釣りタイトルっぽい。いちおう、釣りじゃなくて率直に考えたこと、のつもりなので誤解しないでね。

きのうは、PHPのトレイトについてものすごく前倒しで調査しました。で、そこで、Rubyのmix-inに似ているというところまで行ったところで、こんなコメントをいただきました。

Rubyの例は、includeされたモジュールについてもis_a?を使えばいいです。

ええ、たしかにそうなんです。が、この指摘が出るまで、

module SwimmingAnimal
    def swim
        return "swimming"
    end
end

class Penguin < Bird
    include SwimmingAnimal
end

pingu = Penguin.new
puts "pingu"

if pingu.is_a? FlyingAnimal  #moduleを含んでいるとis_aに応答する
    puts "can fly"
end

if pingu.is_a? SwimmingAnimal  #moduleを含んでいるとis_aに応答する
    puts "can swim"
end

と、型情報をヒントにした分岐を、こう書いていました。

if pingu.respond_to? 'fly'
    puts "can fly"
end

if pingu.respond_to? 'swim'
    puts "can swim"
end

型情報を参考にせず、メソッドがあるだけで応答可能と判断する、いわゆる、ダックタイピングスタイルです。

実際は is_a でいいのに、なぜ最初に respond_to で判別しようとしたかというと、「mixin/trait は単一継承OOPにおいて is-a 関係のクラス概念の外にある」という常識感覚がはたらいたからです。で、それは Ruby 的には損してたんですが、一般論として、その遠回りはあながち間違っていないんじゃないか、むしろ、Ruby の mix-in が is_a で true を返すのがおかしいのかもしれないぞ、と感じたお話です。(いいですか、disってないですよ、主観で違和感あるってだけの日記ですよ、これは)

Rubyのmix-inの定義はmoduleキーワードで行われます。クラス定義に似ていますが、クラスと異なり継承ができません。

何かを継承したモジュールはない - rubyco(るびこ)の日記

簡単なケースならいいのですが、ちょっとここでOOP的に変な感じになりました。

module InWaterSwimmingAnimal
    def swim
        return "swimming in water"
    end
end

module OnWaterSurfaceSwimmingAnimal
    def swim
        return "swimming on water surface";
    end
end

class Penguin2 < Bird
    include InWaterSwimmingAnimal
end

class Swan < Bird
    include FlyingAnimal
    include OnWaterSurfaceSwimmingAnimal
end

Penguin2 と Swan は swim できます。ところが、module 構文で InWaterSwimmingAnimal と OnWaterSurfaceSwimmingAnimal が SwimmingAnimal を継承したいけどできないので、それらが SwimmingAnimal の一種になれません。そうすると、Penguin2 と Swan も SwimmingAnimal になりません。結果、以下のコードは何も実行されないということになります。

pingu = Penguin2.new
puts "pingu"
if pingu.is_a? SwimmingAnimal
    puts "can swim" # ここ通らない
end

swan = Swan.new
puts "swan"
if swan.is_a? SwimmingAnimal
    puts "can swim" # ここ通らない
end

困った。これじゃ「低級なオブジェクトはより高級なオブジェクトで置き替えても等価」というリスコフさんとのお約束が果たせない。おっとでも、そもそもこれは単一継承じゃなかったから、is-a で考えたのがだめだったのかも、ということで、respond_to に戻ります。

pingu = Penguin2.new
puts "pingu"
if pingu.respond_to? 'swim'
    puts "can swim" # 通る
end

swan = Swan.new
puts "swan"
if swan.respond_to? 'swim'
    puts "can swim" # 通るぞ
end

こういうことですよね。継承ツリーでもインターフェース実装でもないのに、is_a で考えたのがいけなかった。クラスやインターフェースは継承してより抽象的なものとの is-a が成り立つようにできるけど、mix-in はむしろ act-as だったり has-ability みたいな感じです。また、include せずにメソッドをクラス内部にコピペした場合でも、swim 可能というのは保証できる、としておかないと、そのメソッドだけ特殊な swim にしたいというニーズを満たせません。なので、respond_to を使っておくのが無難なんじゃないかと思うのです。

ただまあ、入出力の型制約がある言語では、そもそもダックタイプを避けて型安全を選びたいので、is_a で縛るほうが適している、なので、Scala は trait でインターフェースと実装を不可分にしてあるんだと思うんですね。

ちょっとここで、もしも PHP5.4 で trait を use するだけで instanceof が true を返してしまうとどうなるか考えました。そうすると、継承できないトレイト(普通はmixinと呼ぶからややこしい)をあてにして、さっきの SwimmingAnimal のバリエーション問題が発生してしまいます。

PHPにはインターフェースがあるので、型システムはインターフェースに任せ、instanceof は、インターフェースの有無(trait の有無とは直交する)だけに反応したほうが安全でしょう。あくまで、継承できないトレイト(普通はmixinと呼ぶからややこしい)は、実装コードの差分に徹する。もしどうしても簡単に trait を使ったかどうか判別したいというのなら、instanceof ではなく、hastraitof みたいな別のもので判別できるようにしたほうがいいですね。

さてさてそんなわけで、Ruby が module を include しているかどうかが is_a? に影響しちゃうのってどうよ。それは本当の trait(継承できるやつ) に進化してからにしたほうがいいんじゃないの? 今はまだ、includes? みたいな別のメソッドを持たせて、それはオブジェクト指向じゃなくてただの実装の事情を返すんだよと示し、正統な継承の系とは分けたほうが良かったんじゃないかな。えーと、ようするに、「継承ないシステムを is_a に混ぜるのはおかしい」みたいなことを思ったわけです。

まあ、現実的には、module/trait は class と明確に区別されるので、is_a/instanceof の後ろに module/trait が来た場合は同じメソッドのモードが切り替わると意識すればいいんでしょうけど。で、意識したうえでできるだけ使わないほうがいい。やっぱり、そもそも型安全があてにできる言語じゃないんだし、現実派は黙って respond_to? とか method_exists とかでダックタイピングでいいじゃないかという結論な気がしました。


disってませんよ、disってませんからね。