インタフェースと型クラス、どちらでもできること・どちらかでしかできないこと

  • 182
    Like
  • 23
    Comment

最近にわかに 型クラス が盛り上がっているようです。しかし、型クラスはインタフェースに似たものだという意見もあればまったく別のものだという意見もあり、混乱する人が多いのではないかと思います。

そのような混乱を招く理由は、 インタフェースと型クラスはどちらも抽象化を実現するためのもの であり、

  1. インタフェースでも型クラスでもできること
  2. インタフェースでしかできないこと
  3. 型クラスでしかできないこと

があるからです。 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 では speciesname をメソッドではなくプロパティにしました。プロパティとは、 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 aa は何かというと、 Animal を満たす具体的な型、 CatDog を表しています。つまり、 name :: a -> String は、 CatDog を受け取ってその名前を 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 を実装する型 CatDog などが満たすべき性質を記述し、その実装を強制することができました

インタフェースでしかできないこと

次は、 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 を抽象的に( CatDog それぞれに個別に実装するのではなく、まとめて)実装することができました。これを使うコードは次のようになります。

// Java
getDiplayName(new Cat("タマ")); // タマ (ネコ)
getDiplayName(new Dog("ポチ")); // ポチ (イヌ)

さっきの static String getDisplayName(Animal animal) の場合と同じように動作していますね。 Animal が型クラスの場合、 Animal 型自体は存在しません。しかし、それでもジェネリクス(パラメータ多相)を使うことでインタフェースと似たような抽象化を実現できました。

アドホック多相

Haskell で namespecies が、 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 を介して namespecies が呼ばれているわけでもありません。↓のコードはポリモーフィズムではなくオーバーロードのように見えると思います。

-- Haskell
species (Cat "タマ") -- "ネコ"
species (Dog "ポチ") -- "イヌ"

displayName の例のようにパラメータ多相とアドホック多相を組み合わせると一気にオブジェクト指向のポリモーフィズムのように見えると思います。

-- Haskell ✅
displayName :: (Animal a) => a -> String
displayName a = name a ++ "(" ++ species a ++ ")"

しかし、このような場合でも a は常に具体的な型( CatDog など)を指し、抽象的な Animal 型の値に対して namespecies が呼ばれているわけではありません。アドホック多相はアドホック多相であり、オブジェクト指向のポリモーフィズムとはまた別のポリモーフィズムです。

なお、 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 ではできないことがあります。それは、↓のように CatDog など、異なる具象型のインスタンスが混ざった 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 型の変数に CatDog を代入した場合には暗黙的に Existential に変換されています。そうでないとメモリレイアウトが異なる(今はたまたま同じですが)値型の CatDog に対して抽象的な 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);
}

一見これでよさそうに見えるかもしれませんが、これではダメです。 CatEquatable を実装させると次のようになります。

// 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` は Haskell における `Equatable` 相当のもの
class  Eq a  where
  (==) :: a -> a -> Bool

Swift のコードでは Self というキーワードが出てきています。これは自分自身の型を表します。たとえば、 CatEquatable にするなら == の引数は Cat に、 StringEquatable にするなら == の引数は String になります。

// Swift
struct Cat: Equatable {
  static func ==(lhs: Cat, rhs: Cat) -> Bool {
    return lhs.name == rhs.name
  }
}

== の引数の型が Self ではなく Cat になっていることに注意して下さい。 Self が意味するのは、そのプロトコルを実装する型自身です。

Haskell のコードもそれと同じことを意味します。 == は二つの a 型の引数をとり Bool を返します。この a には CatString など具体的な型が入るので、 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` を比較する `==` などない

↑のように、 CatString を比較する == はどこにも実装されていません。そのため、そもそも Equatable 型の変数を作れないようになっているのです。 Self を導入することで、オブジェクト指向のサブタイピングによるポリモーフィズムは不可能になります。

このように、型クラスを使えばインタフェースとは違い、 Self のようなより柔軟な制約を記述することができます。しかし、それが実現できるということはインタフェースと違ってサブタイピングによるポリモーフィズムができないことを意味します。

Swift のプロトコルは Self を使わないなどサブタイピングを許容できる場合はインタフェースのように型として使えますが、 Self が記述されたプロトコルは型として使うことができません。型クラスとなり、型ではなくなります2

型クラスでしかできないこと (2)

型クラスではさらに強力な制約を書くことができます。

例として、 map を抽象化することを考えてみましょう。 Java では StreamOptionalmap メソッドを持っています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 が何をやっているのかを説明できなければなりません。 StreamOptional も内部に T 型の値を保持しています。 mapmapper を使ってその値を T 型から R 型に変換します。そしてその変換結果を Stream / Optional で包み直して返します。これが map の挙動です。そのような map を持つものを Functor と呼ぶことにしましょう。

さて、ここでポイントなのは、 Streammap は変換後の値を Stream で包み、 Optionalmap は変換後の値を 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 を実装することができました。この表現力はインタフェースにはないものです。

まとめ

  • インタフェースと型クラスはどちらも抽象化を実現するためのもので、型が満たすべき性質を記述し、それを実装する型に強制することができる
  • インタフェースはそれ自体が型になることができ、サブタイピングによる抽象化が実現できる
  • 型クラスはそれ自体が型になることはできないが、より柔軟な抽象化が実現できる


  1. 継承してオーバーライドするなどすれば挙動が異なりますが、それはここで説明したいことの主旨とは関係がありません。 

  2. Swift の用語ではそれでも型かもしれませんが、ここで言いたいのは変数等の型として利用することができなくなるということです。 

  3. 見やすいようにワイルドカードを取り除いています。 

  4. 次のバージョンの Swift の言語仕様を話し合うオフィシャルな場所。