Typescript の Structural Subtyping


概要

  • 最近 Typescript を書いていて、同僚から Structural Subtyping を教えて貰った。
  • どういうのものか簡単にまとめる

Subtyping

  • ある基本型(SuperType)に対して派生した型のことを Subtype という
  • 何をもって派生型と認識するかは各プログラミング言語によって異なる
  • 派生の方式として Nominal SubtypingStructural Subtyping に大別される
  • どちらも当然ながら、派生型は基本型と置換可能であるという 「リスコフの置換原則」 を満たしている

Nominal Subtyping

  • JavaやC# 等の言語では、明示的に interface の実装、class の継承などをする事で、自らの型を公称する。
  • 下記の Pig や BlackPig はいずれも、IPigの派生型として自らを公称している。

pig.java
interface IPig {
  public void bow();
}

// IPigの実装なのでIPigの派生型 
class Pig implements IPig {
   public void bow() { 
      System.out.println("Bow wow wow");
   }
}

// IPigを実装したPigの継承したクラス
class BlackPig extends Pig {
}

  • 下記 callBowメソッドのように IPig を型とした引数を受け取ることができる。
HelloPigs.java
public class HelloPigs { 
   public static void main(String[] args) { 
      HelloPigs.callBow(new Pig()); // => Bow wow wow
      HelloPigs.callBow(new BlackPig()); // => Bow wow wow
   }

   // Pig も BlackPig も IPigの派生なのでOK
   // IPigの派生型でなければコンパイルエラーとなる
   public static void callBow(IPig pig) {
      pig.bow();
   }
}

Structural Subtyping

  • 一方 Structural Subtyping(構造的部分型) は、型の構造に着目して、それが派生型であるかどうか判定する
  • 型自身が、自らの型を公称しなくても構造的に一致するのであれば、それは派生型として認識する

pig.ts
interface IPig {
  bow(): string;
}

class Pig implements IPig {
  bow(): string{
    return "Bow wow wow";
  }
}

// IPigとは何の関係もないが bowメソッドを持っている
// ので callBow は IPigの派生型として認識する
class Dandy {
  bow(): string{
    return "Goood";
  }
  throwCard(): string {
    return "Namerunaaaaa!!";
  }
}

function callBow(pig:IPig) {
   console.log(pig.bow());
}

callBow(new Pig());
callBow(new Dandy());

  • Dandy は IPigと全く関係ないクラスだが、IPigが備えるすべてのメソッドが実装されているので、派生型として認識され、callBow 関数が実行可能になる
  • 静的にチェックできるDuckTyping と捉える事ができる。

Duck Typing とは

  • Duck Typing は動的型付けな言語(Python, Ruby)等における型付けの作法である
  • ある処理に必要なオブジェクト A と同じ振る舞いができるのであれば、継承関係のある/なしにかかわらず、そのオブジェクトはAと同じとみなして良いというもの。

pig.py
class Pig:

    def bow(self):
        print("Bow wow wow")

class Dandy:

    def bow(self):
        print("Goood!")

    def throw_card(self):
        print("Namerunaaaaa!!")


# callBow は bowメソッドを持つオブジェクトであればなんでもいい
# 全て豚とみなす。
def callBow(pig):
    pig.bow()


callBow(Pig())
callBow(Dandy())

  • 動的型付け言語の場合は、callBowを実行するまでは、pigにbowが実装されているかどうかはわからない。
  • 一方で、Structural Subtyping では渡された引数の pigに bow というメソッドがあるか、コンパイル時にチェックし、bowの存在を保証してくれる。(もし存在しなければエラーを吐く)

まとめ

  • Structural Subtyping は 静的にチェックできるダックタイピング と捉える事ができる
  • 豚のような悲鳴をあげれる伊達男のトバルカイン・アルハンブラは豚のように鏖殺されても良い


参考