Structural Subtyping vs Nominal Subtyping 比較して理解する
そもそもSubtyping(派生型)ってなに?
Subtypingとは 型の関係性を表す言葉
データ型S が他のデータ型T とis-a関係にあるとき、S をT の派生型(はせいがた、subtype)であるという。またT はS の基本型(きほんがた、supertype)であるという。
例えばデータ型 Dog と データ型 Animal があった場合 (Dog is a Animal が成り立つ)
Dog は Animal の派生型(Subtype)
AnimalはDogの基本型(Supertype)
である。
この派生型/基本型の関係には、リスコフの置換原則がなりたつ
基本型を引数に取る関数は、派生型も引数として取る事が可能である。
例でいうと、Animal型を引数として取る関数は、その派生型であるDog 型も引数として受け取ることが可能である。
型の派生型チェックの方法は言語によって異なり、以下のような種類がある
Nominal Subtyping
Structural Subtyping
どちらの方法を採用しているかによって、データ型Tとデータ型Sが基本型/派生型の関係になるかが変わってくる
方式は言語仕様によって異なる。
Nominal Subtyping: C++ C# Java Swift
Structural Subtyping: Elm OCaml Haskell TypeScript
Nominal Subtyping
C++ C# Java Swift
派生型関係 の情報は Nominal という名前の通り、明示されたもののみが関係性を持つ
nominal : 名目
Dog型、Cat型どちらも受け取れるようにGreet関数を定義するためには、2つが同一の派生型である事を示せば良い
code: nominal.cs
// IAnimalというinterfaceを用意し、継承することでDog型とCat型を明示的にIAnimalの派生型と宣言する
public interface IAnimal
{
string Name { get; set; }
string Breed { get; set; }
}
public class Dog : IAnimal // IAnimalを明示的に継承
{
public string Name { get; set; }
public string Breed { get; set; }
}
public class Cat : IAnimal // IAnimalを明示的に継承
{
public string Name { get; set; }
public string Breed { get; set; }
}
class Program
{
static void Main(string[] args)
{
var borzoi = new Dog() { Name="Bor", Breed="Borzoi" };
var persian = new Cat() { Name = "Per", Breed="Persian" };
Greet(borzoi);
Greet(persian);
}
// 引数は基本型のIAnimalにすることで、派生型のDog型とCat型どちらも引数として受け取ることが可能になる
public static void Greet(IAnimal args) // ここの引数が重要!!!!引数に IAnimal型を取っている
{
Console.WriteLine($"Hello my name is {args.Name}");
}
}
Interface IAnimal を継承していない場合、派生型を明示的に宣言していないため、エラーが出る
code: nominal.cs
public interface IAnimal
{
string Name { get; set; }
string Breed { get; set; }
}
public class Dog // IAnimalの継承無し
{
public string Name { get; set; }
public string Breed { get; set; }
}
public class Cat // IAnimalの継承無し
{
public string Name { get; set; }
public string Breed { get; set; }
}
class Program
{
static void Main(string[] args)
{
var borzoi = new Dog() { Name="Bor", Breed="Borzoi" };
var persian = new Cat() { Name = "Per", Breed="Persian" };
Greet(borzoi); // エラーが発生する
Greet(persian); // エラーが発生する
}
//Nominal Subtypingにおいて、IAnimal型と、Dog/Cat型は明示的に継承していない場合、Dog/Cat型を引数として取ることはできない
public static void Greet(IAnimal args) // ここの引数が重要!!!!!!引数に IAnimal型を取っている
{
Console.WriteLine($"Hello my name is {args.Name}");
}
}
Structural Subtyping
Elm OCaml Haskell TypeScript
派生型関係 の情報は Structural という名前の通り、型の構造を見て判断される
Structural : 構造的
派生型を明示的に宣言していない場合であっても、Structural Subtypingであれば、型構造を見て派生型かどうかを判断する。
下記のコードの例であれば、Cat型とDog型は Animal型 と同じ型構造をもつため
Cat は Animal の派生型(Subtype)
AnimalはCatの基本型(Supertype)
Dog は Animal の派生型(Subtype)
AnimalはDogの基本型(Supertype)
が成り立つ
code: structural.ts
interface Animal{
name: string;
breed: string;
}
interface Dog{ // 継承は無し
name: string;
breed: string;
}
interface Cat { // 継承は無し
name: string;
breed: string;
}
const borzoi: Dog = {name: "Bor", breed: "Borzoi" };
const persian: Cat = { name: "Per", breed: "Persian" };
function Greet(args: Animal) // ここの引数が重要!!!!!!引数にAnimal型を取っている
{
console.log(Hello my name is ${args.name})
}
Greet(borzoi); // Greetの引数の型はAnimal型だが、型構造を見て、暗黙的に派生型とみなされエラーは出ない
Greet(persian); // 同様
どちらが良いの?
Nominal Subtypingの場合はコードが冗長になりやすく
Structural Subtypingの場合は派生型に関するバグを生みやすい
Nominal Subtypingのメリット
型検査以降も、特に実行時に型情報を活用できる
再帰的な型の取り扱いが容易
型検査、特にsubtypingの検査が自明なまでに簡単
明示的にsubtypingを宣言するため、誤りが生じにくい
それぞれのSubtypingについて、デメリットとなる面を実際のコード付きで例をあげる
Nominal Subtyping
モデルの入れ替え
まったく同一のプロパティを持つ2つの型を持つインスタンスが派生型/基本型の関係がない場合は詰め替えが必要
モデルの入れ替え
code:nominal.cs
public class UserFromDB
{
public int Id { get; set; }
public string Name { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
var userFromDb = new UserFromDB() { Name="Bor", Id=1 };
// Greet(userFromDb); // 型構造は同一だが、型関係を明示していないため、エラーが出る
var user = new User() {
Id = userFromDb.Id,
Name = userFromDb.Name
}; // 詰め替えが必要
Greet(user);
}
public static void Greet(User args)
{
Console.WriteLine($"Hello my name is {args.Name}");
}
}
Structural Subtyping
全く別のものを派生型を見なしてしまう
ユーザー情報をDBに登録する関数を考える。
RegisterUserの引数の型構造が id: string; と name: string;のみの型の場合
その2つを含んでいる型であれば、どんな型でも受け付けてしまう。
code: structural.ts
interface User{ // 継承は無し
id: number;
name: string;
}
interface Car { // 継承は無し
id: number;
name: string;
model: string;
}
const user: User = { id:1, name: "nyaagoo" };
const car: Car = { id: 2, name: "Mercedes‐Benz", model: "A-Class" };
function RegisterUser(args: User) // ここの引数が重要!!!!!!引数にUser型を取っている
{
// DBにユーザーを登録する処理
}
RegisterUser(car); // RegisterUserの引数の型はUser型だが、型構造を見て暗黙的に派生型とみなされCar型でもエラーは出ない
それぞれのSubtypingで短所を補うための方法もある
C#(Nominal Subtyping) でモデルの詰め替えをライブラリに任せる
TypeScript(Structural Subtyping) をNominal Typingを実現しようとしている記事
参考