Rustのトレイト(以降Rustトレイトとつなげて呼ぶことにする)は一体何なのか、様々な他言語の概念を通して調べていたが、やっと(「やはり」でもあるのだが)結論が出たので書いていこうと思う。
また、Rustトレイトはインターフェースなのか、MixInなのか、はたまたトレイトなのかということについて書き直したかったのでそれもついでに書く。
Rustのトレイトにおける否定
前置き
そもそもの話だが、オブジェクト指向言語でよく見られるインターフェース、MixIn、トレイトはユーザー定義型に対してのみ実装されるものである。そのため、この時点でユーザー定義型だけでなくほぼすべての型に実装できるRustトレイトとは異なるものである。
しかしそれだけだと、「ユーザー定義型以外も実装できる~」と言った表現も可能となるので、そういった点は排除したうえで否定していこうと思う。
Rustのトレイトはインターフェースではない
基本的なインターフェースは実装を持たないので、この時点でRustのトレイトはインターフェースでないとはっきり言える。
基本的なインターフェースだと言えるPHPの例を見ていこう。
<?php /** * インターフェース */ interface Interface_ { public function method_a(); // 定義だけする // これはできない(PHPインターフェースではメソッドの実装ができず、エラー) // public function method_b() { // echo "Called from Interface." . PHP_EOL; // } } /** * インターフェースの実装(implements) */ class Class_ implements Interface_ { // PHPインターフェースはメソッド実装を強制する(実装がないとエラー) public function method_a() { echo "Called from Class." . PHP_EOL; } } ?>
以上からわかるように、PHPインターフェースではメソッドの実装はできず、定義しかできないことがわかる。それでもインターフェースを使うメリットとしては、Rustトレイトと同じように「共通の振る舞いを定義する」ことに加えて、必ずメソッドの実装を強制できるというのがある。
ではRustトレイトはどうなっているのかと言うと、メソッドの定義だけでなく、デフォルトの実装もできる。
/** * Rustトレイト */ trait Trait { fn method_a(&self) -> (); // 定義だけすることができる // デフォルトの実装も持てる fn method_b(&self) -> () { println!("Called from Trait."); } fn method_c(&self) -> () { println!("Default."); } } /** * Rust構造体 * 今回はトレイトの機能を調べるだけなので、マーカー構造体を使用 * マーカー構造体とはフィールド定義のない構造体のことである */ struct Struct; /** * 型にトレイトを実装 * Rustでは構造体定義とトレイト実装は別個に行う */ impl Trait for Struct { // デフォルト実装がない場合は、メソッドの実装を強制される fn method_a(&self) -> () { println!("Called from Struct."); } // デフォルト実装されたメソッドはわざわざ実装する必要はない // fn method_b(&self) -> () { ... } // デフォルト実装は構造体側で再度実装してやることもできる fn method_c(&self) -> () { println!("Overwrote."); } }
Rustトレイトではデフォルト実装されたメソッドはわざわざ型に実装するときに実装しなくても良い。そしてデフォルト実装されたメソッドは実装側で上書きする(あえてオーバーライドとは言わないのは後述する)こともできる。
以上から、Rustトレイトはインターフェースよりも制約が大幅に緩いために、インターフェースとは違うと言うことができる。
・・・と言うと、「じゃあRustトレイトはJava 8やC# 8.0のデフォルト実装付きインターフェースなのでは?」という反論が来ると思うので、これも加えてはっきり言っておこう、Rustトレイトはインターフェースではないと。
RustのトレイトはJavaのインターフェースではない
まずRustトレイトはトレイトごとに個別のメソッド実装を持つことができる*1。Javaインターフェースではそれができず、必ず「新しいメソッドかオーバーライド」になり、継承関係を持つことになる。
ではRustの例を見ていこう。
/** * Rustトレイト(A) */ trait TraitA { // デフォルト実装 fn method(&self) -> () { println!("Called from TraitA."); } } /** * Rustトレイト(B) */ trait TraitB { // デフォルト実装 fn method(&self) -> () { println!("Called from TraitB."); } } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 */ impl TraitA for Struct {} impl TraitB for Struct {}
このコードはビルドしてもエラーにならない。
一方、Javaインターフェースでは以下のコードはエラーになる。
/** * Javaインターフェース(A) */ interface InterfaceA { // デフォルト実装 default void method() { System.out.println("Called from InterfaceA."); } } /** * Javaインターフェース(B) */ interface InterfaceB { // デフォルト実装 default void method() { System.out.println("Called from InterfaceB."); } } /** * インターフェースの実装 * この実装方法はエラーとなる */ //class Class_ implements InterfaceA, InterfaceB {}
これはJavaではクラスにインターフェースを実装するときに継承関係が発生した際、2つの実装を持つことになるからである。2つのインターフェースメソッドが両方とも同名のメソッドであるために名前衝突が発生しているのである。
「あれ、じゃあRustではなぜエラーにならないの?」と疑問に持つ方もいるだろう。Rustでは確かに型にトレイトを実装してもエラーにはならないが、メソッドを実行しようとするとエラーになる。
fn main() { let s = Struct; // 以下のコメントアウトを外すとエラーとなる // s.method(); }
そのエラーの理由もまた名前衝突をしているからである。しかし、Javaインターフェースとは違って、トレイトは確かに実装されている。なぜならメソッドチェーン以外の方法であれば呼び出せるからだ。
Rustのメソッドは関連関数と呼ばれる形式で呼び出すこともできる。
struct Struct; impl Struct { fn method(&self) -> () { println!("Called from Struct."); } } fn main() { let s = Struct; // メソッドを関連関数の形式で呼び出す Struct::method(&s); }
この時点で察しの付いた方もいるだろう。そう、関連関数でどのトレイトのメソッドであるかを明示してやれば名前解決し、エラーを回避することができるのである。
fn main() { let s = Struct; // どのトレイトのメソッドであるかを明示するとエラーにならない // ちなみにTraitA::method(&s)とも書けるが、個人的には推奨しない <Struct as TraitA>::method(&s); }
ここからRustトレイトのメソッドは個別に実装されていることがわかるだろう。なお、Javaインターフェースでも似たようなことはできるが、これはオーバーライドであり、Rustトレイトのそれとは本質的に異なるものである。
/** * インターフェースの実装 * この実装方法はOK */ class Class_ implements InterfaceA, InterfaceB { // メソッドのオーバーライド // インターフェースAのメソッドを呼び出す @Override public void method() { InterfaceA.super.method(); } // インターフェースBのメソッドは別の名前で呼び出すようにする public void method2() { InterfaceB.super.method(); } }
またRustトレイトのメソッドの再実装は「上書き」であるのに対し、Javaインターフェースのメソッドの再実装はオーバーライドである。
/** * Rustトレイト */ trait Trait { fn method(&self) -> () {} } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 */ impl Trait for Struct { // デフォルトメソッドを再実装 // Rustトレイトメソッドの再実装は「上書き」であり、オーバーライドではない fn method(&self) -> () { // WARNING: 以下のコードは再帰呼び出し、危険!!! // 再実装時にデフォルト実装を呼び出す方法はない // <Self as Trait>::method(self); } }
Rustトレイトのメソッド再実装は「上書き」なので、デフォルト実装を呼び出す方法がない。そのため再実装時に「オーバーライドした」と思って元の実装を呼び出そうとするのは危険である。それは単純に再帰呼び出しとなるためスタックオーバーフローが発生する。
/** * Javaインターフェース */ interface Interface { default void method() {} } /** * インターフェースの実装 */ class Class_ implements Interface { // メソッドの再実装(オーバーライド) @Override public void method() { // 再実装時にデフォルト実装を呼び出せる Interface.super.method(); } }
一方、Javaインターフェースではデフォルトメソッドの再実装はオーバーライドであるため、デフォルトメソッドを呼び出すことができる。
RustのトレイトはC#のインターフェースではない
実はRustトレイトはC#インターフェースにかなり近い。なぜならC#インターフェースは実装次第ではRustトレイトと同じような実装ができるからである。
/** * C#インターフェース(A) */ interface InterfaceA { void method() { Console.WriteLine("Called from InterfaceA."); } } /** * C#インターフェース(B) */ interface InterfaceB { void method() { Console.WriteLine("Called from InterfaceB."); } } /** * インターフェースの実装 */ class Class : InterfaceA, InterfaceB {}
以上のコードはエラーにならない。さらに明示アップキャストを行うことで、Rustトレイトと同じように各インターフェースのメソッドを使うことも可能である。
public class Program { public static void Main() { Class c = new Class(); ((InterfaceA)c).method(); } }
Called from InterfaceA.
また以下のように、実装を「上書き」することも可能である。
/** * C#インターフェース */ interface Interface { void method() {} } /** * インターフェースの実装 */ class Class : Interface { void Interface.method() { // WARNING: 以下のコードは再帰呼び出し、危険!!! // 再実装時にデフォルトメソッドを呼び出す方法はない // ((Interface)this).method(); } }
そのためこの実装法をする場合に限っては、RustトレイトはC#インターフェースだと言えるだろう*2。
しかしこれ以外の実装法だと、RustトレイトはC#インターフェースでないと言える余地が十分に出てくる。たとえば単一の形でインターフェース継承をするとき、C#では2つの選択肢がある。
1つは、派生インターフェースで「新しいデフォルト実装」を与える場合。この場合が、Rustトレイトと同じ挙動である。
/** * C#インターフェース(A) */ interface InterfaceA { void method() {} } /** * C#インターフェース(B <: A) */ interface InterfaceB : InterfaceA { // newを加えることで、「新しいデフォルト実装」ができる // これにより基底と派生のメソッドは別個になる new void method() { // 「新しいデフォルト実装」では基底のメソッドも呼べる ((InterfaceA)this).method(); } }
もう1つは、派生インターフェースで基底インターフェースのデフォルトメソッドを上書きする場合。これはRustトレイトでは不可能である。
/** * C#インターフェース(A) */ interface InterfaceA { void method() {} } /** * C#インターフェース(B <: A) */ interface InterfaceB : InterfaceA { // 基底のデフォルトメソッドを上書き void InterfaceA.method() { // WARNING: 以下のコードは再帰呼び出し、危険!!! // 再実装時にデフォルトメソッドを呼び出す方法はない // ((InterfaceA)this).method(); } }
ここからC#インターフェースはRustトレイトよりも幅広い表現ができることがわかるだろう。
また、C#インターフェースのインターフェース継承は継承関係が生まれるため、クラスに実装するのは派生インターフェースだけで良い。
/** * C#インターフェース(A) */ interface InterfaceA {} /** * C#インターフェース(B <: A) */ interface InterfaceB : InterfaceA {} /** * インターフェースの実装 * C#インターフェースでは派生インターフェースだけ実装すれば良い */ class Class : InterfaceB {}
それに対して、Rustトレイトのサブトレイトはあくまで「実装するときにスーパートレイトも実装しなければならない」という制約を与えているに過ぎないため、両方とも実装しなければならない。
/** * Rustトレイト(A) */ trait TraitA {} /** * Rustトレイト(B: A) */ trait TraitB: TraitA {} /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 * サブトレイトを実装する場合、絶対にスーパートレイトを実装しなければならない */ impl TraitA for Struct {} impl TraitB for Struct {}
以上から、RustトレイトとC#インターフェースは性質的に異なることがわかるだろう。
RustのトレイトはMixInではない
RustトレイトがMixInでないことは、菱形継承の形にしてみればわかる(そもそも継承関係ができる時点で、RustトレイトはMixInではないのだが・・・まぁそれは置いておいて)。菱形継承とは以下のような形の継承関係である。
この菱形継承には、「菱形継承問題」と呼ばれる問題がある。菱形継承問題とは、たとえば先ほどの例で言えばAから派生したB, Cを多重継承する場合、DはB, CのどちらからAのメソッドを呼ぶようにするべきか?という問題である。
Rustトレイトもそうだが、MixInはこの菱形継承問題を解決するための手法の一つである。
ではMixInではどのように解決しているのか?MixInでの例を見ていこう。MixInと言えばRubyのモジュールが有名だが、今回使うのはPythonである。
# PythonでMixInをするには、クラスを使う class AMixIn: ''' MixIn(A) ''' def method(self): print('AMixIn') class BMixIn(AMixIn): ''' MixIn(B <: A) ''' def method(self): print('BMixIn <: ', end='') super().method() class CMixIn(AMixIn): ''' MixIn(C <: A) ''' def method(self): print('CMixIn <: ', end='') super().method() class Class(BMixIn, CMixIn): ''' BMixIn, CMixInが含まれる ''' def run(self): print('Class <: ', end='') self.method() Class().run()
Class <: BMixIn <: CMixIn <: AMixIn
実行結果からもわかるように、MixInでは多重継承をした際に単一継承に変換される。これはどれだけ多重に継承していようと同じである。ちなみに変換された際どのような順序になるかはあらかじめルールが決められており、それは処理系に依存する。
以上から、MixInでは多重継承を単一継承に変換することで菱形継承問題を回避していることがわかった。
ではRustトレイトではどうなっているのか。
/** * Rustトレイト(A) */ trait TraitA { fn method(&self) -> () { print!("TraitA => "); } } /** * Rustトレイト(B: A) */ trait TraitB: TraitA { fn method(&self) -> () { <Self as TraitA>::method(self); print!("TraitB => "); } } /** * Rustトレイト(C: A) */ trait TraitC: TraitA { fn method(&self) -> () { <Self as TraitA>::method(self); print!("TraitC => "); } } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 */ impl TraitA for Struct {} impl TraitB for Struct {} impl TraitC for Struct {} /** * 構造体にメソッドを実装する * トレイト実装とは別に実装する必要がある */ impl Struct { fn run(&self) -> () { // よっしゃーメソッド呼び出すぞ・・・あれ? // self.method(); println!("Struct"); } } fn main() { Struct.run(); }
・・・そもそもRustトレイトのメソッドは各トレイトごとに定義されていて、名前解決しないとエラーになるのだった。
ということで、RustトレイトではそもそもMixInのような実装が不可能であることがわかり、ここからRustトレイトはMixInでないことがわかる。
ちなみにこれは名前解決して呼び出してもMixInのような結果にはならないのでどちらにしてもRustトレイトはMixInではないことは明白である。以下のリンクから確認できる。
Rustのトレイトはトレイトではない
まずトレイトという単語は自分が調べた限りでは3つ存在する。
- Traits(Self)
SelfにおけるTraitsは、プロトタイプベースオブジェクト指向言語であるSelfで導入された概念である。実質クラスであると言われているが、Selfはクラスベースに批判的な位置にいるためかクラスという名前は付いていない。
Rustをオブジェクト指向だと仮定しても*3プロトタイプベースではないため、そもそもの本質が異なる。そのため、RustトレイトとTraits(Self)は比べるべきではないだろう。
- Traits(C++テンプレートによるテクニック)
C++テンプレートの特殊化を応用したテクニック。一応型クラス的なこともできるらしいが、Rustトレイトとは大きく違うところが散見されるため、今回はこれとは異なるとして話を進める。
- トレイト(Schärli氏ら提唱)
Schärli氏らの提唱した概念。今回はこれを「トレイト」だとしてRustトレイトと比較する。
Schärli氏らの提唱したトレイトは、こちらもMixInと同じく菱形継承問題の解決策の一つである。だから菱形継承を比較することでもRustトレイトがトレイトでないことは証明できるが、そもそもRustトレイトにない機能がトレイトにはあるのでその時点で証明できる。
なお、ここからは混乱を避けるためにトレイトを「従来のトレイト」と呼ぶこととする。
まず従来のトレイトでは多重に使用するとき名前衝突するとエラーとなる。PHPでは従来のトレイトがサポートされているので、そちらで見てみよう。
<?php /** * トレイト(A) */ trait TraitA { public function method() {} } /** * トレイト(B) */ trait TraitB { public function method() {} } /** * トレイトをクラスに使用 */ class Class_ { // この時点でエラーが発生する use TraitA, TraitB; } ?>
PHP Fatal error: Trait method method has not been applied, because there are collisions with other trait methods on Class_ in ...(省略) (PHP致命的エラー: 他のトレイトメソッドとの衝突があるため、トレイトの`method`メソッドを`Class_`に適用できません。)
Rustトレイトでは先ほどの例からもわかる通り、多重実装時にエラーにはならない。その代わりに呼び出そうとすると名前衝突するので関連関数で名前解決してやる必要があるのだった。
/** * Rustトレイト(A) */ trait TraitA { fn method(&self) -> () { println!("Called from TraitA."); } } /** * Rustトレイト(B) */ trait TraitB { fn method(&self) -> () { println!("Called from TraitB."); } } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 * 多重に実装してもエラーにならない */ impl TraitA for Struct {} impl TraitB for Struct {} fn main() { let s = Struct; // どのトレイトのメソッドであるかを明示するとエラーにならない // ちなみにTraitA::method(&s)とも書けるが、個人的には推奨しない <Struct as TraitA>::method(&s); }
一方、従来のトレイトは多重継承時点で名前衝突するとエラーになるが、それを回避する策として、以下の2つがある。
- メソッドの排除
- メソッドの別名化
従来のトレイトでは、使用時に無名のトレイトを作ることができる。これを利用しメソッドの操作をして新しいトレイトにすることで、それがクラスの一部となるわけである。ちなみにクラスの一部となることからもわかる通り従来のトレイトもRustトレイトと同じく継承関係は生まれない。
<?php class Class_ { // メソッドを操作して新しいトレイトを作る use TraitA, TrairB { // トレイトBのメソッドを排除する TraitA::method insteadof TraitB; // メソッドを排除すると同時に別名化もできる TraitB::method as method2; } } ?>
では、Rustトレイトではメソッドの排除や別名化は?・・・できない。Rustトレイトにはそのような機能はない。
一応、ユーザー定義型のメソッドの実装方法によってはメソッド排除や別名化みたいなことはできたりするのだが、実際にメソッド排除や別名化をしたわけではないため(特にメソッド排除に関してはわかりやすいだろう)、これができるからRustトレイトが従来のトレイトであるとは言えないだろう。
/** * Rustトレイト(A) */ trait TraitA { fn method(&self) -> () { println!("Called from TraitA."); } } /** * Rustトレイト(B) */ trait TraitB { fn method(&self) -> () { println!("Called from TraitB."); } } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装 */ impl TraitA for Struct {} impl TraitB for Struct {} /** * 構造体にメソッドを実装する */ impl Struct { // トレイトAのメソッドを呼び出す(「メソッド排除」的な実装) fn method(&self) -> () { <Self as TraitA>::method(self) } // トレイトBのメソッドを呼び出す(「別名化」的な実装) fn method2(&self) -> () { <Self as TraitB>::method(self) } } fn main() { let s = Struct; // 構造体側で片方のメソッドを呼び出すように実装すればあたかも // メソッド排除したかのような挙動になる s.method(); // 別名化自体は構造体側で新しいメソッドを実装しトレイトメソッドを // 呼び出すようにすることで再現できる s.method2(); // 構造体に実装してもトレイトAのメソッドはいつも通り呼び出せる // ここがトレイトと異なる <Struct as TraitA>::method(&s); // トレイトBのメソッドも普通に呼び出せる // ここもトレイトと異なる <Struct as TraitB>::method(&s); }
以上から、Rustトレイトはトレイトではない。
Rustのトレイトは「高カインド多相のない型クラス」だった
さて、ここからようやく本題である。今までRustトレイトとは何であるのか明確に説明したサイトはごく少数に限られたために、「Rustのトレイトとは、オブジェクト指向におけるインターフェースである」「Rustのトレイトとは、Schärli氏らのトレイトである」などと勘違いしていた人が見受けられた。
しかし、この宣言にてRustトレイトが何であるか、決着をつけようと思う。
Rustのトレイトは「高カインド多相のない型クラス」である。
さて、こう書いておいてなんだが、誤解させないように言っておくと、Rustトレイトは「高カインド多相がない」以外にも通常の型クラスではできないことがある。それは後述する。
根拠1: 型クラスと用法が同じである
ここでやっとRustトレイトの用法について説明する。Rustトレイトの正しい用法としては、「型の分類」となる。特定のメソッドを使う型を分類することで、それぞれの型がどのように扱うのかを記述するのがRustトレイトである。
そして型クラスも特定の関数を使う型を分類することで、それぞれの型がどのように扱うかを記述する。
ちなみに「それぞれの型がどのように扱うかを記述する」だけに着目するとインターフェースも似たようなことをしていると言えるが、違うところは、Rustトレイト・型クラスは「型を分類」する機能であるのに対し、インターフェースは様々なクラスの基底型となる「型」の一種であるため、その本質は大きく異なる。
では早速Haskell型クラスとRustトレイトを比べてみる。まずはHaskellから。
-- 恒等関数を持つ型クラス -- わかりやすさのため「self」としているが、本来「a」などと書くのが一般的 class Id self where id' :: self -> self -- 型クラスのインスタンス -- 型クラスにおけるインスタンスは、「型」である instance Id Int where id' x = x -- main関数 main :: IO () main = print $ id' (123 :: Int)
123
次にRust。
/** * 恒等関数を持つRustトレイト */ trait Id { fn id(&self) -> Self; } /** * 型にトレイトを実装 */ impl Id for i32 { fn id(&self) -> Self { *self } } /** * main関数 */ fn main() { println!("{}", 123i32.id()); }
それぞれの型に定義するための「関数/メソッド」と「インスタンスの定義/型への実装」、そして「関数/メソッド」の評価などすべてにおいて同じであることがおわかりいただけただろうか。
Rustのメソッドは関連関数としても書けることを考慮すると異なるのはHaskellでは純粋関数であり、Rustでは副作用を持つ可能性のある関数であることただ一点のみである。
根拠2: 重複する関数があった際の挙動も同じ
通常、Haskellでは各型クラスの関数が名前空間に含まれているわけではないので、型クラス同士で重複する関数があった場合にコンパイルすると、コンパイルエラーとなる。
したがってこのままではRustトレイトは「高カインド多相のない型クラス」と呼ぶことは困難であろう。
しかしある程度Haskellに慣れている人ならば、こう思うのではないだろうか。
名前空間に含まれていないならば、含ませてあげれば良いではないか
と。
というわけでHaskellのモジュール機能では修飾付きインポートができることを利用して型クラスごとに関数を分類させることで、Rustトレイトと同様となるようにしてみた。
module ClassA where -- 型クラス(A) class ClassA self where f :: self -> String f _ = "Called from ClassA." -- ClassA型クラスのインスタンス instance ClassA ()
module ClassB where -- ClassAモジュールを修飾付きインポート import qualified ClassA -- 型クラス(A self => B self) class ClassA.ClassA self => ClassB self where f :: self -> String f _ = "Called from ClassB." -- ClassB型クラスのインスタンス instance ClassB ()
import qualified ClassA import qualified ClassB -- main関数 main :: IO () main = putStrLn (ClassA.f ()) *> putStrLn (ClassB.f ())
さて、結果はどうなるだろうか。
Called from ClassA. Called from ClassB.
先ほどのHaskellのコードではスーパー型クラスの関数とサブ型クラスの関数を順に呼び出していったのだが、上記の実行結果を見ればわかるように別個の関数として定義されていることがわかる。
Rustトレイトでの結果も、ご想像の通り。
/** * Rustトレイト(A) */ trait TraitA { fn method(&self) -> String { "Called from TraitA.".to_string() } } /** * Rustトレイト(B: A) */ trait TraitB: TraitA { fn method(&self) -> String { "Called from TraitB.".to_string() } } /** * マーカー構造体 */ struct Struct; /** * 型にトレイトを実装する */ impl TraitA for Struct {} impl TraitB for Struct {} /** * main関数 */ fn main() { let s = Struct; println!("{}\n{}", <Struct as TraitA>::method(&s), <Struct as TraitB>::method(&s) ); }
このように、Haskellではもともと各型クラスごとの関数が名前空間に含まれることがないため、Rustのようにするとコンパイルエラーを吐く。しかし名前衝突しないようにしてあげれば、Rustトレイトと同じ挙動となる。
以上から、Rustトレイトは、型クラスであると言えそうである。
しかし、型クラスでできて、Rustトレイトではできないことがある。それこそが「高カインド多相*4」である。
Rustのトレイトでは高カインド多相ができない
そもそも高カインド多相とは何か、についてだが、これをちゃんと理解するためには「カインド」という概念について理解しなければならない。
しかしながらカインドは割とかなり高度な概念であるため、今回カインドに関する説明については省かせていただく*5。
そのうえで高カインド多相について乱暴に説明すると、Rustで言うならば「ジェネリック型を型引数としたジェネリクス」である。
Rustのジェネリック型は、ある任意の型を引数として渡すことで「新しい型を作ること」ができる。たとえばRustのstd
クレート*6に含まれるVec<T>
は型引数T
にi32
を渡すことでVec<i32>
という新しい型を作る。
しかし、ジェネリック型そのものに対しては引数として渡すことはできない。たとえばGeneric<G<T>>
(GもTも型引数である)と言ったものはできない。
一応工夫すればできないわけではないが、Rustでは高カインド多相がサポートされていないというのは間違いないだろう。
そしてHaskell型クラスでは高カインド多相が可能である*7。そのため、「高カインド多相」ができることも型クラスの必要条件に含まれるならば、Rustトレイトは型クラスではないだろう。
それゆえ、今回はRustトレイトに対して「高階カインド多相のない型クラス」という表現を使わせてもらった。
逆に型クラスでできないこともある
逆に、Rustトレイトにできて型クラスにできないこともあって、それは「トレイトオブジェクト、implトレイト構文を使えばあたかも『トレイトを引数にする、戻り値にする』ような関数を作ることができる」と言う点である。これは型クラスではできない。
/** * トレイトオブジェクト、implトレイト構文による例 * `Box<dyn Trait>`がトレイトオブジェクト、`&impl Trait`がimplトレイト構文 */ fn func(t: &impl Trait) -> Box<dyn Trait> { ... }
しかしここで注意しなければならないのが、トレイトは「型ではない」という点である。トレイトオブジェクトやimplトレイト構文はあくまでトレイトから型を作る機能に過ぎない。
そのため「トレイトを引数にする、戻り値にする」という表現は少し不適切な表現であると言える。トレイトオブジェクト型やimplトレイト型*8だと言ったほうが適切だろう。
終わりに
最初は「Rustトレイトは、トレイトなのだろうか?」という疑問から始まったのだが、そこから本当のトレイトやインターフェース、MixInについて知り、さらに「Rustトレイトは、本当に型クラスなのだろうか?」という疑問からHaskellを学習し始め、関数型プログラミングとは何なのか、特に純粋関数型プログラミングとは何なのかを型クラスについて知るついでに学ぶことができたなど、かなり収穫が大きかったように感じる。そういう意味ではRustトレイトには感謝しなければならないだろう。
Rustトレイトに関しては一旦これで区切りとさせていただくが、これからもRustトレイトについて探求し続けるだろうし、その他の概念も探求し続けるだろう。
最後に一言。
「Rust言語の開発者よ、悪く言うつもりはないが、なぜトレイトでないものを『トレイト』と名付けた・・・」
以上。
*1:ここで言うメソッド実装は、トレイト側でデフォルト実装されたか、実装側で実装されたかは問わない
*2:もちろんC#インターフェースがユーザー定義型にしか実装できない点は除く
*3:なぜ「仮定する」のかと言うと、筆者個人はRustをオブジェクト指向だとする意見にかなり懐疑的だからである。詳細は「Rustがオブジェクト指向型言語ではないのとその理由」を参照
*4:高階多相とも呼ばれる
*5:カインドについて理解したければ、「すごいHaskellたのしく学ぼう!」を買ったうえでHaskellについて学習していくのが近道だろう
*6:他言語で言う標準ライブラリのことである
*7:むしろ高カインド多相がなければHaskellにおいて非常に重要な概念となるモナドが作れないに等しかったため純粋関数型プログラミング言語として有名にはなれなかっただろう
*8:implトレイト構文により作った型をどう表現すれば良いのか、非常に悩ましいところである。implトレイト構文自体、トレイト制約と似たようなところがあり一見糖衣構文に見えるが実は糖衣構文でなかったりとややこしい