pyてよn日記

一寸先は闇が人生

ポリモーフィズムとは

最近,気分転換に Haskell の勉強を始めた.筆者は Python が親なので,Python をある程度書けるようになったら他の言語を学ぼう,という思いで勉強していたが,マンネリを感じて他の言語に手を出そうと思い立ったのがきっかけ(つい一昨日 2020/06/14).Haskell の勉強のために読んでいるのは,「すごい Haskell たのしく学ぼう」という Haskell 入門本で有名な本である.今のところ新鮮ですごく楽しい.

上記の本を読んでいる最中に「多相的関数(ポリモーフィズムな関数)」とか「ジェネリクス」などの言葉が出てきた.これまで何度か調べたことがあるものの,理解が不十分でもやもやしたため,本記事ではそれらを調べて用語の意味と実装例を簡単にまとめてみた.

Summary

本記事のざっくりとしたまとめ.

  • ポリモーフィズム
    • ある関数が,引数・返り値の数・データ型が異なる実装を持ち,呼び出し時に使い分けることができるような性質を持つとき,その関数を「ポリモーフィズムな関数」,「多相的関数」という.
    • ポリモーフィズムの 3 つの分類
      • アドホック多相
        • 関数が異なる引数の数・型に対してそれぞれ異なる実装を持つ.
      • パラメータ多相
        • 関数の引数・返り値の型情報の一部をパラメータ化して外部から与えることができる.
      • サブタイピング多相
        • 関数が共通の上位型をもつ複数の型(部分型,サブタイプともいう)を引数や戻り値に取ることができる.
    • ポリモーフィズムの目的は「型の抽象化
  • ジェネリクス

調べる前の理解:ポリモーフィズムジェネリクス

自分の中では両者をちゃんと言葉で説明できなかった(だからこの記事を書いてる).

まず,「ポリモーフィズム」,「ジェネリクス」をほぼ同じものだと思ってしまっており,用語の区別ができていない.何も見ずに現時点での自分の理解を言葉に起こすと,

  • C++ の template みたいに汎用的な型を使って関数実行時に型の違いを吸収してくれる仕組み
  • 例えば,整数型,浮動小数点数型などの数値型を同じ関数で処理したい時に汎用的な型 T でどっちにも対応してくれるやつ
  • Java だと『ジェネリクス』って言われているらしい

というかなり雑な説明になってしまう.本記事では,「用語で『ポリモーフィズム』,『ジェネリクス』の説明をできるようになる」ことを目標とする.

ポリモーフィズム

ポリモーフィズム基本

プログラミング言語の型システムの性質の 1 つで,プログラミング言語の各要素(定数,変数,式,オブジェクト,関数,メソッドなど)について,それらが複数の型に属することを許す性質.引数,返り値などの「型の抽象化」を行うことが「ポリモーフィズム」の目的である.

もう少し簡単な説明を示す.ある関数(またはメソッド)が「引数・返り値の数・データ型が異なる実装を持ち(つまり,同じ名前の関数なのに引数・返り値が異なる),呼び出し時に使い分けることができる」ような性質を持つとき,その関数を「ポリモーフィズムな関数」,「多相的関数」という.

ポリモーフィズムの分類

ポリモーフィズムはいくつかの種類に分類できる.ここでは,各分類の説明と実装を示す.

ポリモーフィズムの分類:説明

  • アドホック多相 ad hoc polymorphism
    • ある関数が,異なる引数の数・型に対してそれぞれ異なる実装を持つとき,「アドホック多相な関数」という.多くのプログラミング言語で「関数の多重定義」としてサポートされている.
    • (ざっくり)呼び出し側に記述された引数の数・型の違いに応じて,処理系が自動で合致する実装を選んで呼び出してくれる関数の性質.
    • C++ での「関数の多重定義」.
  • パラメータ多相 parametric polymorphism(パラメトリック多相)
    • ある関数の引数・返り値の型情報の一部をパラメータ化し外部から与えることができるとき,その関数を「パラメータ多相な関数」という.パラメータ化された型を「型パラメータ」や「型変数」と呼ぶ.普通は型に対して抽象化された 1 つの実装を持つ.
    • 型パラメータは基本的には任意の型を取ることが出来るが,その反面「抽象化された 1 つの実装」の中でパラメータ化された値に対して具体性のある処理をしたいときに困る.そのため,型パラメータに型制約を付与することができる言語がある.
      • Java では <T extends SomeType> のようにサブタイプを指定できる.
      • Haskell では型クラスで型変数の型制約を付与できる.
    • (ざっくり)呼び出し時に型をパラメータとして明示的に指定できる関数の性質.
    • オブジェクト指向言語では「ジェネリクス generics」や「ジェネリックプログラミング」として知られる.
    • Haskell 等の関数型言語の分野では,パラメータ多相のことを指して単に「多相性」と呼ぶ場合がある.
    • プログラミング言語で「パラメータ多相な関数」(Haskell では「多相的関数」という)を実装する方法
  • サブタイピング多相 subtyping polymorphism(部分型多相 subtype polymorphism,包含多相 inclusion polymorphism,部分型付け)
    • ある関数が共通の上位型をもつ複数の型(上位型の部分型,サブタイプ,派生型ともいう)を引数や戻り値に取ることができるとき,その関数を「サブタイピング多相な関数」という.一般に,型に応じて異なる実装に処理が振り分けられる.
    • C++Java などのオプジェクト指向言語の分野では,ポリモーフィズムと言えば「サブタイピング多相」指すことが多い
    • (ざっくり)2 つのデータ型が基本型と派生型の関係にあるとき,基本型を引数に取る関数を定義することにより,派生型でも同じように動作させるようにすることができる.この関数のことを「サブタイピング多相な関数」という.
    • オブジェクト指向プログラミング言語では,親クラスから派生(継承)した子クラスがメソッドの内容を上書き(オーバーライド)したり,インターフェースで定義されたメソッドを実装することによりこれを実現している.

ポリモーフィズムの分類:実装

ポリモーフィズムの各分類の実装例を示す.

アドホック多相
  • アドホック多相 ad hoc polymorphism
    • ある関数が,異なる引数の数・型に対してそれぞれ異なる実装を持つとき,「アドホック多相な関数」という.多くのプログラミング言語で「関数の多重定義」としてサポートされている.
    • (ざっくり)呼び出し側に記述された引数の数・型の違いに応じて,処理系が自動で合致する実装を選んで呼び出してくれる関数の性質.
    • C++ での「関数の多重定義」.

C++ では,同じスコープ内で名前 func を持つ関数を複数宣言することにより,関数名 func を多重定義できる.

func の宣言は,型,または引数の数,またはその両方で相互に異なっていなければならないfunc という名前の多重定義された関数を呼び出すとき,関数呼び出しの引数リストを名前 func を持つ多重定義された候補関数のそれぞれのパラメータ・リストと比較することによって,正しい関数が選択される.「候補関数」とは,多重定義関数名の呼び出しのコンテキストに基づいて呼び出すことのできる関数のことである.

異なる型を標準出力するための print という関数を多重定義した.「関数の多重定義(C++ のみ)」より実装を引用している.実行時に,処理系によって引数の型に合致した print が呼び出される.

#include <iostream>
using namespace std;

void print(int i) {
  cout << " Here is int " << i << endl;
}
void print(double  f) {
  cout << " Here is float " << f << endl;
}
void print(char* c) {
  cout << " Here is char* " << c << endl;
}

int main() {
  print(10);
  print(10.10);
  print("ten");
}

出力

Here is int 10
Here is float 10.1
Here is char* ten
パラメータ多相
  • パラメータ多相 parametric polymorphism(パラメトリック多相)
    • ある関数の引数・返り値の型情報の一部をパラメータ化し外部から与えることができるとき,その関数を「パラメータ多相な関数」という.パラメータ化された型を「型パラメータ」や「型変数」と呼ぶ.普通は型に対して抽象化された 1 つの実装を持つ.型パラメータは基本的には任意の型を取るが,任意の型しか取れない場合,「抽象化された 1 つの実装」の中でパラメータ化された値に対して具体性のある処理をしたいときに困る.そのため,型パラメータに型制約を付与することができる言語がある.
      • Java では <T extends SomeType> のようにサブタイプを指定できる.
      • Haskell では型クラスで型変数の型制約を付与できる.
    • (ざっくり)呼び出し時に型をパラメータとして明示的に指定できる関数の性質.
    • オブジェクト指向言語では「ジェネリクス generics」や「ジェネリックプログラミング」として知られる.
    • Haskell 等の関数型言語の分野では,パラメータ多相のことを指して単に「多相性」と呼ぶ場合がある.
    • プログラミング言語で「パラメータ多相な関数」(Haskell では「多相的関数」という)を実装する方法

C++Java でパラメータ多相な関数の実装例を示す.

#include <iostream>

template<typename T>
void print(T c) {
    std::cout << " Here is char* " << c << "\n";
}

int main() {
    print(10);
    print(10.10);
    print("ten");
}
public class Main {
    public static void main() {
        print(10);
        print(10.10);
        print("ten");
    }

    private static <T> void print(T arg) {
      System.out.println(arg);
    }
}
サブタイピング多相
  • サブタイピング多相 subtyping polymorphism(部分型多相 subtype polymorphism,包含多相 inclusion polymorphism,部分型付け)
    • ある関数が共通の上位型をもつ複数の型(上位型の部分型,サブタイプ,派生型ともいう)を引数や戻り値に取ることができるとき,その関数を「サブタイピング多相な関数」という.一般に,型に応じて異なる実装に処理が振り分けられる.
    • C++Java などのオプジェクト指向言語の分野では,ポリモーフィズムと言えば「サブタイピング多相」指すことが多い
    • (ざっくり)2 つのデータ型が基本型と派生型の関係にあるとき,基本型を引数に取る関数を定義することにより,派生型でも同じように動作させるようにすることができる.この関数のことを「サブタイピング多相な関数」という.
    • オブジェクト指向プログラミング言語では,親クラスから派生(継承)した子クラスがメソッドの内容を上書き(オーバーライド)したり,インターフェースで定義されたメソッドを実装することによりこれを実現している.

C++ でサブタイピング多相な関数の実装例を示す.

親クラス Moster,子クラス SlimeGhost を定義した.関数 printStatusMoster 型の引数を受け取るが,そのサブタイプである SlimeGhost 型の引数を渡しても問題なく動作する.

#include <iostream>
#include <string>

// 親クラス
class Monster {
    private:
        string name;
        int hp;
    public:
        Monster(string name, int hp);
        void showStatus();
};

Moster::Monster(string n, int h) {
    name = n;
    hp = h;
    std::cout << "Create new monster!" << "\n";
}
void Monster::showStatus() {
    cout << "name: " << name << "\n";
    cout << "HP: " << hp << "\n";
}

// 子クラス
class Slime : public Monster {
    public:
        Slime(int hp);
}
Slime::Slime(string n = "slime", int h = 10) : Monster(n, h) {
    std::cout << "Create new slime!" << "\n";
}

class Ghost : public Monster {
    public:
        Ghost(int hp);
}
Ghost::Ghost(string n = "ghost", int h = 30) : Monster(n, h) {
    std::cout << "Create new ghost!" << "\n";
}

void printStatus(Moster m) {
    m.showStatus();
}

int main() {
    Slime slime = new Slime();
    Ghost ghost = new Ghost();

    printStatus(slime);
    printStatus(ghost);

    return 0;
}

ジェネリクス

色んな言語でポリモーフィズムな関数を実装する

色んな静的型付け言語でポリモーフィズムな関数を実装してみる.TypeScript,C++HaskellJava を用い,「パラメータ多相な関数」の実装例を示す.

このジェネリクスに関する記事の例を拝借し,「1 つの引数を受け取り,その引数を要素を 3 つ持つのタプルを返す」関数を実装してみる(3 を受け取ったら[3, 3, 3] を返す関数).

TypeScript

const triple = <T>(arg: T): [T, T, T] => [arg, arg, arg];

triplet = triple(3);

C++

#include <tuple>

template<typename T>
std::tuple<T, T, T> T triple(arg T) {
    return std::make_tuple(arg, arg, arg);
}

int main() {
    std::tuple triplet = triple(3);
    // auto [a, b, c] = triple(3);  C++ 17 以降だと戻り値をこんな感じで展開できる
}

Haskell

triple :: a -> (a, a, a)
triple arg = (arg, arg, arg)

triplet = triple 3

Java

ボリューミーになってしまった.Java 自体初めて書いたので実装の仕方が分からなかった(雰囲気で書いてるので,「一般的にはこう書くよ」って書き方があればご指摘ください).

class Triplet<X, Y, Z> {
    private X x;
    private Y y;
    private Z z;

    public Triplet(X x, Y y, Z z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public X first() {
        return this.x;
    }

    public Y second() {
        return this.y;
    }

    public Z third() {
        return this.z;
    }
}

public class Main {
    public static void main() {
        Triplet<T> triplet = new Triplet(3);
    }

    private static <T> Triplet<T, T, T> triple(T arg) {
      return new Triplet<T, T, T>(arg, arg, arg)
    }
}

残った疑問点

  • ポリモーフィズム,特にサブタイピング多相とダックタイピングって関係ある?
    • 「ダックタイピング」についての理解が浅い.調べ切れていない.

終わりに

ポリモーフィズムの概要について理解できた.特に,実装してみることでこういうことを言いたいんだなということが理解できた.ただ,ポリモーフィズムについて体系的な勉強をしたいなら「型システム」関連の本とか読まないといけないんだろうな.まあそれは置いといて,とりあえず Haskell をのんびり学んでいくぞ.

最後まで読んでくださりありがとうございました.