最近にわかに 型クラス が盛り上がっているようです。しかし、型クラスはインタフェースに似たものだという意見もあればまったく別のものだという意見もあり、混乱する人が多いのではないかと思います。
そのような混乱を招く理由は、 インタフェースと型クラスはどちらも抽象化を実現するためのもの であり、
- インタフェースでも型クラスでもできること
- インタフェースでしかできないこと
- 型クラスでしかできないこと
があるからです。 1 に着目した人は似ていると語り、 2 や 3 に着目した人はまったく違うものだと言います。
本投稿では、 Java / Kotlin のインタフェース、 Haskell の型クラス、 Swift のプロトコルを比較し、上記の 3 点を整理します。 Swift のプロトコルを加えるのは、 Swift のプロトコルがインタフェースと型クラスの両方の性質を備えたものなので、比較対象としておもしろいからです。
比較対象言語に盛り上がりの発端となった Scala が含まれていないのは、僕が Scala に詳しくないからです。もしかしたら @kmizu さんあたりがコメントで Scala についても補足・比較してくれるかもしれません🙂
なお、本投稿では言語間の用語の差異を Java に寄せて書いてあります。同じ概念でも言語によって呼び方が違ったりしますが、すべての言語にとって正確な表現で書こうとすると文章が冗長になってしまいます。四つの言語の中では Java が最も広く知られていると思うのと、型クラスがある言語を使いこなしている人は大抵インタフェースについても知っているので、本投稿の想定読者ではないからです。
インタフェースでも型クラスでもできること
動物を表す Animal
インタフェースおよび型クラスを作ってみましょう。 Animal
は「種」を表す species
と、その個体の名前を表す name
を持っているものとしましょう。たとえば、猫のタマであれば species
は "ネコ"
で、 name
は "タマ"
です。
// Java ✅
interface Animal {
String getSpecies(); // 種の名称
String getName();
}
// Kotlin ✅
interface Animal {
val species: String
val name: String
}
// Swift ✅
protocol Animal {
var species: String { get }
var name: String { get }
}
-- Haskell ✅
class Animal a where
species :: a -> String
name :: a -> String
Kotlin と Swift では species
と name
をメソッドではなくプロパティにしました。プロパティとは、 Java でいうフィールドのように見えるけれども、実体としてはメソッドのようなものです。 foo.bar
と書くと foo.getBar()
が呼ばれるようなものと思って下さい。
Haskell にはメソッドがないので代わりに関数を使います。メソッドは animal.getName()
のような形でコールしますが、関数の場合は animal
を引数に渡して getName(animal)
のようにコールします。 Java で表すと次のような違いです。
// Java
class Cat {
private String name;
// メソッド
public String getName() {
return this.name;
}
// 関数(がやっているのと同じようなこと)
public static String getName(Animal animal) {
return animal.name;
}
...
}
どちらの場合でも同じようなこと1が実現できるのがわかると思います。
前述の Haskell のコードで a -> String
と書いてあるのは、引数として a
型の値を受け取って String
を返すという意味です。 Animal a
の a
は何かというと、 Animal
を満たす具体的な型、 Cat
や Dog
を表しています。つまり、 name :: a -> String
は、 Cat
や Dog
を受け取ってその名前を String
で返す関数 name
の宣言です。
では、 Animal
を満たす具象型 Cat
を実装しましょう。
// Java ✅
class Cat implements Animal {
private final String name;
Cat(String name) {
this.name = name;
}
@Override
String getSpecies() {
return "ネコ"
}
@Override
String getName() {
return name;
}
}
// Kotlin ✅
class Cat(override val name: String) : Animal {
val species: String
get() = "ネコ"
}
// Swift ✅
struct Cat: Animal {
var species: String {
return "ネコ"
}
let name: String
}
-- Haskell ✅
data Cat = Cat String
instance Animal Cat where
species _ = "ネコ"
name (Cat n) = n
species
は型そのものに紐付いているので( Cat
なら "ネコ"
)フィールドで保持する必要はありません。そのため、 Cat
はただ一つの String
型のフィールド name
を持ちます。同じように Dog
型を作ることもできます。
このように、 Animal
がインタフェースでも型クラスでも、 Animal
を実装する型 Cat
や Dog
などが満たすべき性質を記述し、その実装を強制することができました。
インタフェースでしかできないこと
次は、 Animal
を受け取って "タマ (ネコ)"
のように、名前と種を結合した文字列を返す関数/メソッド getDisplayName
/ displayName
を作ってみましょう。
// Java ✅
static String getDisplayName(Animal animal) {
return animal.getName() + " (" + animal.getSpecies() + ")";
}
// Kotlin ✅
fun displayName(animal: Animal): String {
return "${animal.name} (${animal.species})"
}
// Swift ✅
func displayName(animal: Animal) -> String {
return "\(animal.name) (\(animal.species))"
}
-- Haskell ⛔
-- できない
これは型クラスではできません。インタフェースは型ですが、型クラスは型ではありません。そのため、抽象的な Animal
型というものは存在しません。当然、 Animal
型の値を引数として受け取るようなこともできません。
インタフェースでも型クラスでもできること (2)
Java の感覚では、 Animal
を型として使えないと困るように思います。しかし、 Animal
型がなくてもこれを解決する方法があります。ジェネリックメソッド/関数です( Haskell では パラメータ多相( Parametric polymorphism ) と呼びます)。
getDisplayName
はジェネリクスを使って次のように実装することもできます。
// Java ✅
static <A extends Animal> String getDisplayName(A animal) {
return animal.getName() + " (" + animal.getSpecies() + ")";
}
// Kotlin ✅
fun <A : Animal> displayName(animal: A): String {
return "${animal.name} (${animal.species})"
}
// Swift ✅
func displayName<A: Animal>(animal: A) -> String {
return "\(animal.name) (\(animal.species))"
}
-- Haskell ✅
displayName :: (Animal a) => a -> String
displayName a = name a ++ "(" ++ species a ++ ")"
getDisplayName
をこのように実装すれば、 Animal
自体を型として使わなくても getDisplayName
を抽象的に( Cat
や Dog
それぞれに個別に実装するのではなく、まとめて)実装することができました。これを使うコードは次のようになります。
// Java
getDiplayName(new Cat("タマ")); // タマ (ネコ)
getDiplayName(new Dog("ポチ")); // ポチ (イヌ)
さっきの static String getDisplayName(Animal animal)
の場合と同じように動作していますね。 Animal
が型クラスの場合、 Animal
型自体は存在しません。しかし、それでもジェネリクス(パラメータ多相)を使うことでインタフェースと似たような抽象化を実現できました。
アドホック多相
Haskell で name
や species
が、 Cat
に対して呼ばれた場合と Dog
に対して呼ばれた場合でどの実装が呼ばれるか切り替わる仕組みを アドホック多相( Ad hoc polymorphism ) と言います。
-- Haskell
species (Cat "タマ") -- "ネコ"
species (Dog "ポチ") -- "イヌ"
Java の視点でみるとただのオーバーロードに見えるかもしれません。
// Java
static String getSpecies(Cat cat) {
return "ネコ";
}
static String getSpecies(Dog dog) {
return "イヌ";
}
getSpecies(new Cat("タマ")); // "ネコ"
getSpecies(new Dog("イヌ")); // "イヌ"
しかし、これをジェネリクス(パラメータ多相)と組み合わせてみるとどうでしょう。 Java では Haskell と同じことができません。
// Java
static <A extends Animal> String getDisplayName(A animal) {
return getName(animal) + " (" + getSpecies(animal) + ")"; // コンパイルエラー
}
一方で、オブジェクト指向言語におけるポリモーフィズム( Subtype polymorphism と呼ばれることもある)のようにも見えるかもしれません。しかし、アドホック多相では抽象的な型 Animal
を介して name
や species
が呼ばれているわけでもありません。↓のコードはポリモーフィズムではなくオーバーロードのように見えると思います。
-- Haskell
species (Cat "タマ") -- "ネコ"
species (Dog "ポチ") -- "イヌ"
`displayName` の例のようにパラメータ多相とアドホック多相を組み合わせると一気にオブジェクト指向のポリモーフィズムのように見えると思います。
> ```haskell
-- Haskell ✅
displayName :: (Animal a) => a -> String
displayName a = name a ++ "(" ++ species a ++ ")"
しかし、このような場合でも a
は常に具体的な型( Cat
や Dog
など)を指し、抽象的な Animal
型の値に対して name
や species
が呼ばれているわけではありません。アドホック多相はアドホック多相であり、オブジェクト指向のポリモーフィズムとはまた別のポリモーフィズムです。
なお、 Swift のプロトコルはインタフェースのように使えると書きましたが、実はそのような使い方をすることはそれほど多くありません。標準ライブラリもパラメータ多相とアドホック多相を組み合わせた方法で実装されていることが多いです。
インタフェースでしかできないこと (2)
↑の方法も万能ではありません。
例として、 List<Animal>
を受け取ってそれら動物の名前のリスト List<String>
に変換する関数/メソッド getNames
/ names
を考えてみましょう。先程と同じようにジェネリクス(パラメータ多相)で実装してみます。
// Java ✅
static <A extends Animal> List<String> getNames(List<A> animals) {
return animals.stream().map(x -> x.getName());
}
// Kotlin ✅
fun <A : Animal> names(animals: List<A>): List<String> {
return animals.map { it.name }
}
// Swift ✅
func names<A: Animal>(of animals: [A]) -> [String] {
return animals.map { $0.name }
}
-- Haskell 🤔
names :: (Animal a) => [a] -> [String]
names animals = map name animals
一見どれでもうまく実装できたように見えます。しかし、↑のコードで Haskell 以外ではできて Haskell ではできないことがあります。それは、↓のように Cat
や Dog
など、異なる具象型のインスタンスが混ざった animals
を渡すことです。
// Java ✅
getNames(Arrays.asList(new Cat("タマ"), new Dog("ポチ")));
// Kotlin ✅
names(listOf(Cat("タマ"), Dog("ポチ")))
// Swift ✅
names(of: [Cat("タマ"), Dog("ポチ")])
-- Haskell ⛔
-- できない
names [Cat "タマ", Dog "ポチ"] -- コンパイルエラー
Haskell では Animal
は型クラスであって型ではないので、いくらジェネリクス(パラメータ多相)を使って引数を受けるようにしたところで List<Animal>
相当の型が作れるわけではありません。 あくまで List<Cat>
も List<Dog>
もどちらでも受け取れる関数が作れるだけです。抽象的な Animal
のリストを受け取れるわけではないのです。
(追記) @cohei@github さんにコメントいただいたように Haskell でも Existential を使えば Animal
のリストのようなことができます。実は、 Swift でも Animal
型の変数に Cat
や Dog
を代入した場合には暗黙的に Existential に変換されています。そうでないとメモリレイアウトが異なる(今はたまたま同じですが)値型の Cat
や Dog
に対して抽象的な Animal
型などというものを考えることはできません。そういう意味でも Swift でプロトコル型変数を使ってサブタイピングするのはちょっと特殊で、 Swift のプロトコルは型クラス的です。
型クラスでしかできないこと
次は逆に、型クラスならできるけどインタフェースではできないことを見てみましょう。
Java や Kotlin では equals
/ ==
を使ってインスタンスが同値であるかを判定します。 equals
や ==
はルートクラス( Java では Object
、 Kotlin では Any
)で定義されているため、どんなクラスのインスタンスでも比較をすることが可能です。
equals
を実装するときは↓のような感じです。
// Java
class Cat implements Animal {
...
@Override
boolean equals(Object object) {
if (!(object instanceof Cat)) {
return false;
}
Cat cat = (Cat)object;
return getName().equals(cat.getName());
}
}
equals
の最初の行で、渡されたオブジェクトが Cat
のインスタンスかどうかを判定しています。しかし、本当に Cat
以外のあらゆる型のインスタンスとの同値性をチェックしたいでしょうか。 Cat
同士の比較さえできればよいのであれば、↓のようにシンプルに書けます。
// Java
class Cat implements Animal {
...
boolean equals(Cat cat) {
return getName().equals(cat.getName());
}
}
しかし、このような equals
を継承やインタフェースによって強制することはできません。試しに、そのような制約を表すインタフェース Equatable
を考えてみましょう。
// Java
interface Equatable {
boolean equals(Equatable object);
}
一見これでよさそうに見えるかもしれませんが、これではダメです。 Cat
に Equatable
を実装させると次のようになります。
// Java
class Cat implements Animal, Equatable {
...
@Override
boolean equals(Equatable object) {
if (!(object instanceof Cat)) {
return false;
}
Cat cat = (Cat)object;
return getName().equals(cat.getName());
}
}
equals
の引数の型が Object
から Equatable
に変わっただけです。引数の型を Cat
にできるわけではありません。
次のようにしたらどうでしょうか?
// Java
interface Equatable<T> {
boolean equals(T object);
}
class Cat implements Animal, Equatable<Cat> {
...
@Override
boolean equals(Cat cat) {
return getName().equals(cat.getName());
}
}
equals
の引数の型を Cat
にすることができました。しかし、これは class Cat implements ..., Equatable<Cat>
としたからです。もし class Cat implements ..., Equatable<String>
としたら equals
の引数の型は String
になってしまいます。 A implements Equatable<A>
の二つの A
を必ずそろえて使うことを強制することはできません。
Haskell や Swift などの言語ではそのような制約を記述することができます。
// Java ⛔
// できない
// Kotlin ⛔
// できない
// Swift ✅
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
-- Haskell ✅
-- ↓の `Eq` は Swift における `Equatable` 相当のもの
class Eq a where
(==) :: a -> a -> Bool
Swift のコードでは Self
というキーワードが出てきています。これは自分自身の型を表します。たとえば、 Cat
を Equatable
にするなら ==
の引数は Cat
に、 String
を Equatable
にするなら ==
の引数は String
になります。
// Swift
struct Cat: Equatable {
static func ==(lhs: Cat, rhs: Cat) -> Bool {
return lhs.name == rhs.name
}
}
==
の引数の型が Self
ではなく Cat
になっていることに注意して下さい。 Self
が意味するのは、そのプロトコルを実装する型自身です。
Haskell のコードもそれと同じことを意味します。 ==
は二つの a
型の引数をとり Bool
を返します。この a
には Cat
や String
など具体的な型が入るので、 Cat
に ==
を使うと Cat
同士を、 String
に ==
を使うと String
同士を比較することになります。
Swift ではプロトコルをインタフェースのように使い、抽象的なプロトコル型の変数を作ることができました。これを Equatable
でやってみるとどうなるでしょうか。
// Swift
let equatable: Equatable = Cat(name: "タマ") // コンパイルエラー
コンパイルエラーになりました。なぜでしょう?
Cat
の ==
は Cat
に対してしか使えません。同じように、 String
の ==
は String
にしか使えません。
// Swift
Cat(name: "タマ") == Cat(name: "タマ") // OK
"abc" == "abc" // OK
もし Equatable
型の変数が作れるとして、その ==
は何型と何型を比較するのでしょう?
// Swift
let equatable: Equatable = Cat(name: "タマ") // コンパイルエラー
equatable == "abc" // `Cat` と `String` を比較する `==` などない
↑のように、 Cat
と String
を比較する ==
はどこにも実装されていません。そのため、そもそも Equatable
型の変数を作れないようになっているのです。 Self
を導入することで、オブジェクト指向のサブタイピングによるポリモーフィズムは不可能になります。
このように、型クラスを使えばインタフェースとは違い、 Self
のようなより柔軟な制約を記述することができます。しかし、それが実現できるということはインタフェースと違ってサブタイピングによるポリモーフィズムができないことを意味します。
Swift のプロトコルは Self
を使わないなどサブタイピングを許容できる場合はインタフェースのように型として使えますが、 Self
が記述されたプロトコルは型として使うことができません。型クラスとなり、型ではなくなります2。
型クラスでしかできないこと (2)
型クラスではさらに強力な制約を書くことができます。
例として、 map
を抽象化することを考えてみましょう。 Java では Stream
も Optional
も map
メソッドを持っています3。
// Java
interface Stream<T> {
<R> Stream<R> map(Function<T, R> mapper);
...
}
class Optional<T> {
<R> Optional<R> map(Function<T, R> mapper) { ... }
...
}
どちらも同じようなシグネチャです。似たようなものは抽象化することで実装の重複を避けることができます。この map
も抽象化できるとうれしいはずです。
map
を抽象化するということは、抽象的に map
が何をやっているのかを説明できなければなりません。 Stream
も Optional
も内部に T
型の値を保持しています。 map
は mapper
を使ってその値を T
型から R
型に変換します。そしてその変換結果を Stream
/ Optional
で包み直して返します。これが map
の挙動です。そのような map
を持つものを Functor
と呼ぶことにしましょう。
さて、ここでポイントなのは、 Stream
の map
は変換後の値を Stream
で包み、 Optional
の map
は変換後の値を Optional
で包んで返すという点です。 Stream<T>
と Stream<R>
は異なる型なので、( Swift で言うところの) Self
で表すこともできません。
残念ながら Swift には( Swift 3.1 時点では) Self
以上の表現力がないので、そのような制約を記述することはできません。 Swift Evolution 4 では何度か話題に挙がっていますが、他にやるべきことがたくさんあり、将来的に導入されることはあり得るが現時点の優先度は低いと判断されています。
// Java ⛔
// できない
// Kotlin ⛔
// できない
// Swift ⛔
// できない
-- Haskell ✅
-- ↓の `fmap` は Java の `map` と同じ
class Functor f where
fmap :: (a -> b) -> f a -> f b
当然、型クラス Functor
を抽象的な Functor
型として扱うことはできません。しかし、パラメータ多相とアドホック多相を使えば、 Functor
を満たす型に対して抽象的に処理を実装することができます。
たとえば、 Functor
の中身の値が数値だった場合に、その中身を二乗する関数 square
を実装してみましょう。
-- Haskell
square :: (Functor f, Num a) => f a -> f a
square = fmap (\x -> x * x)
そうすれば、リストも Maybe
( Java や Swift の Optional
の相当)も fmap
を持つ Functor
なので、 square
はリストに対しても Maybe
に対しても使うことができます。
-- Haskell
square [2, 3, 5] -- [4, 9, 25]
square (Just 3) -- Just 9
Just 3
は Java で言えば Optional.of(3)
の意味です。
このように、型クラス Functor
を使って抽象的に map
を利用する square
を実装することができました。この表現力はインタフェースにはないものです。
まとめ
- インタフェースと型クラスはどちらも抽象化を実現するためのもので、型が満たすべき性質を記述し、それを実装する型に強制することができる
- インタフェースはそれ自体が型になることができ、サブタイピングによる抽象化が実現できる
- 型クラスはそれ自体が型になることはできないが、より柔軟な抽象化が実現できる