なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

インターフェース、MixIn、トレイトについて

近年、MixInやトレイトという名称の概念を持つプログラミング言語が増えてきた、が、蓋を開けてみると、実際にはそれはMixInでなかったり、トレイトではなかったりすることが多くあり、MixInやトレイトに関する混乱を招く要因になっていると思う。

そこで今回は、インターフェース、MixIn、トレイトについて、それぞれどういうものなのか、書いていこうと思う。

インターフェース

インターフェースは、Javaが大きく広めたとされる概念である(おそらくその元ネタはオブジェクト指向における「プロトコル」で(参考)機能として導入されたのはObjective-Cが初だと思われる)。

基本的なインターフェースの機能としては、以下のようになっている。

  1. 実装を持たないメソッドを定義できる
  2. クラスに実装(implements)することでメソッドの実装を強制できる
  3. インターフェースは1つのクラスに複数実装できる

特に3.が重要で、Objective-CJavaPHPC#などでは、多重継承が菱形継承問題など様々な問題を抱えていることもあり、クラスの多重継承はサポートされていない。しかし、多重継承を使うことによるメリットがあることも確かで、多重継承すらできないとなると、もし仮に多重継承をしたほうがスッキリしたコードが書ける場合に使えなかったら困るわけである。

そこで、実装を持たない「抽象的な」メソッドの集まりをインターフェースとして定義しそれだけは特例として多重に実装できるようにすることで、多重継承の問題を解決しつつ、多重継承のような実装をできるようアプローチしたわけである。
これにより、たとえ名前衝突による問題が発生したとしても、実装が元々なく、クラス側で実装を持っているわけなので多重継承の様々な問題を難なくクリアしている。

それ以外にも、複数のクラスにある共通のメソッドを1つにまとめることができる、インターフェースを引数に取る関数や返す関数などを作ることで多相性を実現できるなど、インターフェースのメリットはたくさんある。

しかし、JavaC#ではインターフェースの従来の機能だけでは足りなかったのか、後(Java8、C# 8.0)にデフォルトの実装を持てるようになるなど様々な制約が緩和されている
ただしこれらは各言語によって仕様が大きく異なり、JavaC#以外のインターフェースは依然として同様なものであるため、もはや一般的なインターフェースとは別物と考えたほうが良いだろう

そんなわけで今回はJavaC#のインターフェースではなく、PHPでのインターフェースの例を見ていこう。一般的なインターフェースは以下のように定義する。

<?php
/**
 * インターフェースA
 */
interface InterfaceA {
    public function methodA($intvalue); // 定義だけで実装はしない
    public function methodB($str);
}

/**
 * インターフェースB
 */
interface InterfaceB {
    public function methodB($str);
}
?>

以上のように、メソッドの定義はするが、実装しないのがインターフェースである。
そしてクラスへの実装は以下のように行う。

<?php
/**
 * クラスAに実装
 * - 実装が強制され、methodA, methodBがないとエラーになる
 * - メソッドが被っても、実装のほうはクラスがやるので問題ない
 */
final class ClassA implements InterfaceA, interfaceB {
    public methodA($intvalue) {
        return 3 + $intvalue;
    }
    
    public methodB($str) {
        echo "Input arg: ${str}" . PHP_EOL;
        return $str;
    }
}
?>

インターフェースが実装(implements)されたクラスは必ずインターフェースの持つメソッドを実装しなければならない。このようにクラスに対して実装を強制するのがインターフェースの特徴である。
また、多重に実装をしても、あくまで実装するのはクラスなので特に問題はない。

<?php
/**
 * クラスBに実装
 * - methodBは処理そのものは違うが、クラスAと共通したメソッドである
 */
final class ClassB implements InterfaceB {
    public methodB($str) {
        return $str . "ABC";
    }
}

/**
 * インターフェースが実装されたクラスのインスタンスを引数に取る
 * 多相的な関数
 * - PHP5.0から導入された型宣言を使用
 * - PHP7.0からファイルの先頭にdeclare(strict_types=1);を付けることで
 *   そのインターフェースが実装されたクラスであることを強制できる
 */
function apply_methodB(InterfaceB $ib, $arg) {
    return $ib->methodB($arg);
}
?>

以上のように、インターフェースはクラスの共通したメソッドをまとめる役割もあり、さらに関数でインターフェースが実装されたクラスのインスタンスを引数に取ったり、返したりすることで多相性を実現することもできる。

MixIn

MixInはクラスにほかのメソッド群を差し込む*1ことで、メソッドを追加する実装手法を示す。インターフェースと違うのは、メソッドの実装を持つことができることである。
MixInの起源は、Lisp Machine LispというLisp方言*2の拡張であるオブジェクト指向システムFlavorsであるとされている。

こちらもインターフェース同様、多重継承の問題を解決するための「仕組み」がある。でも実装があるのにどうやって?と思われるかもしれない。
実はMixInでは多重継承のような形になると、それを単一継承の形に変換する、という特徴がある。これがMixInにおいて多重継承の問題を解決できる「仕組み」の正体である。一般に、多重継承を単一継承の形にできないものはMixInとは呼ばない(たとえばTypeScriptのMixInは、実はMixInではない。あれはただの単一継承である*3)。

たとえばRubymoduleは、立派なMixInである。Rubyでの例を見ていこう。

# モジュールA
module ModuleA
    # MixInではメソッドを実装しても良い
    def method_
        puts "ModuleA"
    end
end

# モジュールB
module ModuleB
    # モジュールAを含む
    include ModuleA
    
    def method_
        print "ModuleB => "
        super   # superで継承元のメソッドが呼べる
    end
end

# モジュールC
module ModuleC
    # モジュールAを含む
    include ModuleA
    
    def method_
        print "ModuleC => "
        super
    end
end

# クラスA
class ClassA
    # モジュールB, モジュールCを含む
    include ModuleB, ModuleC
    
    def run
        print "ClassA => "
        method_
    end
end

今回は多重継承の問題としてよく挙げられる菱形継承的な実装を行っている。図としては以下のようになる。

f:id:opaupafz2:20220122222352p:plain

まずModuleAから始まり、それがModuleBとModuleCに含まれ、最後にはModuleB、ModuleCがClassAに含まれていることがわかるだろう。
これを実行すると、MixInではこうなる。

ClassA.new.run

ClassA => ModuleB => ModuleC => ModuleA

なんと、先ほどの図ではModuleAはModuleBに含まれていたはずなのに、ModuleCがModuleBに含まれていることになっている。これは多重継承の形から単一継承の形に変換された、ということにほかならない

f:id:opaupafz2:20220122231623p:plain

ちなみにどのような順序になるかは各言語で依存しており、どういう実装のときにどういう順序になるのかについてはわからないというのが欠点であり、MixInではメソッドが重なるような設計・実装は基本避けるべきである

トレイト

トレイトはSmalltalk処理系のSqueakで初めて導入されたとされる概念である。

トレイトも、先ほどのMixInとは「クラスに差し込む」という点では同じだが、その挙動は大きく異なる。MixInは継承的関係として表れるのに対し、トレイトはあくまで「クラスの一部品」として実装され、クラスとの直接的な継承的関係は一切生まれない

文章だけではどういうものなのかイメージが付きづらいと思うので、早速PHP5.4以降のトレイトを使って例を見ていく。

<?php
/**
 * トレイトA
 */
trait TraitA {
    private function method() {
        echo "TraitA" . PHP_EOL;
    }
}

/**
 * トレイトB
 */
trait TraitB {
    // トレイトAを使用
    use TraitA;
    
    private function method() {
        echo "TraitB => ";
        parent::method();   // PHPで親のメソッドを呼ぶときはparentを使う
    }
}

/**
 * クラスA
 */
class ClassA {
    // トレイトBを使用
    use TraitB;
    
    public function run() {
        echo "ClassA => ";
        $this->method();
    }
}

// "ClassA => TraitB => TraitA"と出力されることを期待している
(new ClassA())->run();
?>

トレイトを「普通の単一継承」として使ってみる。このような実装はMixInであれば以下のような実行結果になるはずである。

ClassA => TraitB =>TraitA

しかしトレイトではこのような結果にはならない。ではどのような結果になるのかと言うと、実行途中でエラーが発生する。

ClassA => TraitB => PHP Fatal error:  Uncaught Error: Cannot access parent:: when current class scope has no parent in ...(省略)

エラーを読んでみると、「現在のクラスに親がいない場合、parentにアクセスできない」と言っている。
なぜこんなことが起こるのか?それに対する答えこそが、「トレイトが『クラスの一部品』であり、クラスとは直接的な継承的関係にない」である。

賢明な人ならば「ちょっと待って!じゃあ、トレイトは多重継承の問題をどうやって解決するの?名前衝突してしまうじゃない」と疑問が湧いたことだろう。

そうだ。実はトレイトでは名前衝突によるエラーが起こる

<?php
/**
 * トレイトA
 */
trait TraitA {
    private function method() {
        // 処理
    }
}

/**
 * トレイトB
 */
trait TraitB {
    // トレイトAのメソッドとまったく同じ名前のメソッドを定義する
    private function method() {
        // 処理
    }
}

/**
 * クラスA
 */
class ClassA {
    // トレイトA, トレイトBを使用
    use TraitA, TraitB;
}
?>

PHP Fatal error:  Trait method method has not been applied, because there are collisions with other trait methods on ClassA in ...(省略)

エラー内容を読んでみると、「他のトレイトのメソッドと名前衝突してしまうため、トレイトの`method`メソッドがClassAに適用されない」と言っていることからも、トレイトではトレイト同士のメソッドが重複したらエラーとなってしまうことがわかるだろう。

これによりMixInの「基本的にメソッドが重なるような設計・実装してはいけない」ということがトレイトではやりやすくなる。メソッドの重複をエラーとして検出してくれるので「メソッドが重なるような設計・実装になっている」ということがすぐにわかるからである。

さらにトレイトの凄いところはここだけではない。

実はトレイトではメソッドが名前衝突した際にメソッドをどのように実装するのか制御できるのである。
先ほどの名前衝突した例のクラスを見てみよう。

<?php
/**
 * クラスA
 */
class ClassA {
    // トレイトA, トレイトBを使用
    use TraitA, TraitB;
}
?>

ここで、TraitAのメソッドを使いたいとしよう。そういう場合には、使わないほうのメソッドを排除することができる

<?php
/**
 * クラスA
 */
class ClassA {
    // トレイトA, トレイトBを使用
    use TraitA, TraitB {
        TraitA::method insteadof TraitB;    // トレイトAのメソッドを使用
    }
}
?>

また、TraitA、TraitBのメソッドを両方とも使いたいとしよう。そういう場合には、メソッドに別の名前を与えることができる

<?php
/**
 * クラスA
 */
class ClassA {
    // トレイトA, トレイトBを使用
    use TraitA, TraitB {
        TraitA::method insteadof TraitB;    // 別名化の際も排除は必須
        TraitB::method as method2;          // トレイトBのメソッドを別名化
    }
}
?>

さらにTraitA、TraitB両方ともに使わずクラスで独自に定義し直したい場合は、普通にクラスで定義してやれば良い

<?php
/**
 * クラスA
 */
class ClassA {
    // トレイトA, トレイトBを使用
    use TraitA, TraitB;
    
    // クラスで定義し直す
    public function method() {
        // 処理
    }
}
?>

このように「ちょっとリッチすぎるんじゃないの?」と思うほど高度な機能を持つのがトレイトである。

余談だが、「え、じゃあScalaやRustのトレイトはトレイトではないのでは・・・」と思ったScalalianやRustaceanもいると思うので言っておくと、ScalaやRustのトレイトはトレイトではない(Rustに関しては何度も書いているのでしつこいようで申し訳ないが)。
Scalaのトレイトは実はMixInであることはこの方が書いてくれているし、何を隠そう筆者自身も「Rustのトレイトはトレイトではない」という主張を書いている(ちなみに近日中に「Rustのトレイト」に関する最新情報を書いていこうと思っている)。

*1:この「差し込む」は、継承であったり何か別の機能だったりしても構わない

*2:方言と言うのは、基本的な文法・機能は同じだが、振る舞いなどの細かいところで違うプログラミング言語のことを言う。規格化が当たり前となる前のプログラミング言語にはこのような「方言」がたくさんあった

*3:まぁ、その機能を実現するために単一継承を利用しているので、仕方ないところもあるが・・・