Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
JavaScript による型クラスの表現

JavaScript による型クラスの表現

この記事は OUCC アドベントカレンダー 2014 12日目の記事です. 岡山大学サイクリング部ではなく, 大阪大学コンピュータクラブです. 昨日は @klta6154 さんによるマスタリングTCP/IP OpenFlow編読みました記でした.

毎度ありがとうございます. (@susisu2413) です.

この記事では JavaScript を使って, Haskell などの言語に存在する "型クラス" のようなものを書いて遊んでみます. ちょくちょく Haskell が登場しますが, Haskell の知識がなくてもある程度読めるように配慮したつもりです. ただ, 流石に JavaScript の説明をしながら型クラスの説明をするのは地獄なので, JavaScript がある程度わかることを前提で書いています.

型クラスとは

型が値の種類を表すとしたら, 型クラスは型の種類を表すようなものです. 例えば, ある型 (オブジェクト指向言語ではクラスなど) に属する値には, その型のメソッドを用いた操作が可能です. 同様に, "ある型クラスに属する型" に属する値には, その型クラスのメソッドを用いた操作を行うことが可能です.

オブジェクト指向言語におけるインターフェースと似ているという説明がされることがあり, 実際その通りなのですが, 型クラスの方がインターフェースに比べてより柔軟にメソッドの定義を行ったりすることが可能です.

用語の定義

ある型が型クラスに属している時, その型は型クラスのインスタンスであるといいます. ちょうど, オブジェクト指向言語でのクラス (型) とインスタンス (値) の関係と (名前が) 似ています. 型が型クラスに属するようにする宣言をインスタンス宣言といいます.

Show

Haskell における (単純化した) Show の定義は以下のようになっています.

class Show a where
    show :: a -> String

日本語で言うと, "ある型 a を 型クラス Show のインスタンスにするには, a の値を String の値に変換する関数 show を定義しなさい" ということです. ここで, 関数 show の型は Show a => a -> String と表されます. => の左側が, 右側の型変数 a が型クラス Show に属していることを表しています. => の右側は単純に aString に変換する関数である, ということですね.

例えば文字列 String, 整数 Int, 真偽値 Bool などは Show のインスタンスとして宣言されていますので (実際にどのようにインスタンス宣言がされているかは面倒なので省きますが), 以下のようにして String に変換が可能です.

show "abc" -- "\"abc\""
show 123   -- "123"
show True  -- "True"

同じ関数 show を, 別の型に属する値に適用している点に注意してください. このような多相 (同じ名前 (この場合 show) が複数の型 (この場合 String -> String, Int -> String 等) に属する値を表す的な) をアドホック多相 (c.f. パラメトリック多相) と言います. この時点では所謂オーバーロードと大差ないですね.

JavaScript の場合, ほとんどすべての (nullundefined を除く) 値は Object を継承したオブジェクト (として扱える) なので, toString メソッドを用いて, 同じような多相が実現が可能です.

"abc".toString(); // "abc"
(123).toString(); // "123"
true.toString();  // "true"

これはまだ Java などのオブジェクト指向言語でのインターフェースでカバーできる範囲内です.

では, Haskell と同じように関数 show を定義して

show("abc"); // "abc"
show(123);   // "123"
show(true);  // "true"

とすることは可能でしょうか. ひとつの方法として, 以下のように show を定義する方法があります.

function show (x) {
    if (typeof x === "string") {
        return x;
    }
    else if (typeof x === "number") {
        // 結局 toString 使ってるじゃんというツッコミは無しで
        // 別の方法で定義してもいいですが, 面倒
        return x.toString();
    }
    else if (typeof x === "boolean") {
        return x ? "true" : "false";
    }
    else {
        throw new Error("error");
    }
}

show("abc"); // "abc"
show(123);   // "123"
show(true);  // "true"

こうした場合, 新たに他の型の値も関数 show を使って文字列 String にしたいときは, show の中身を変更し, 処理を追加する必要があります. でも, それってダサくないですか? どこの TypeScript ですか?

こんな場合の解決策として, 以下のような方法が考えられます.

var show = function (A) {
    if (A.__Show__ && A.__Show__.hasOwnProperty("show")) {
        return A.__Show__.show;
    }
    else {
        throw new Error("No instance declaration for Show");
    }
};

// ネイティブオブジェクト以外でもいいのだけれど, とりあえずこのまま
String.__Show__ = {
    show: function (x) { return x; }
};
Number.__Show__ = {
    show:  function (x) { return x.toString(); }
};
Boolean.__Show__ = {
    show: function (x) { return x ? "true" : "false"; }
};

show(String)("abc"); // "abc"
show(Number)(123);   // "123"
show(Boolean)(true); // "true"

新たに型 Foo の値を showString にしたい場合は, Foo.__Show__.show を定義する (== インスタンス宣言する) だけで済みます. show の後に型を書くのが面倒なのは諦めろ.

この JavaScript 版の show の型について考えてみましょう. show は最初に引数として型クラス Show に属する型 A (今の場合 StringNumber など), すなわち型引数をとり, "A の値を引数にとって, String の値を返す関数" を返します. Show に属する型 A (または, A に対する Show のインスタンス宣言) を Show<A> と書くとすると, Show<A> -> (A -> String) のように書けます (JavaScript に型の記法なんて無いので, どう書いてもいいけれど). これは Haskell の show の型 Show a => a -> String に酷似していますね! これで JavaScript で型クラスが表現できたといえます.

任意の Show のインスタンス型の値に対する関数 (Show という型クラスに属する抽象化された型の値に対する関数) は以下のように定義できます.

var itIs = function (A) {
    return function (x) {
        return "It is "  + show(A)(x) + "!";  
    };
};

itIs(String)("abc"); // "It is abc!"
itIs(Number)(123);   // "It is 123!"
itIs(Boolean)(true); // "It is true!"

とってもとっても, 簡単ですね. 関数の入れ子が若干見た目が悪いですが, これくらいなら JavaScript では日常茶飯事レベルなので大したことはないでしょう.

こんな面倒なことしなくても, toString みたいなメソッドとして定義すればいいじゃない, なんて, そんなことを言っていられるのは今のうちですよ.

Monoid

Monoid は数学におけるモノイドと同じで, Haskell での型クラスの定義は以下のようになっています.

class Monoid a where
    mempty  :: a
    mappend :: a -> a -> a
    mconcat :: [a] -> a
    mconcat = foldr mappend mempty

なんだか新しい概念が登場しました. mempty :: Monoid a => a多相定数と言われる定数で, 文脈 (期待される型) によって表す値が変わる定数です. さらに, mconcat にはデフォルト実装があります. これにより, インスタンス宣言時に memptymappend のみ定義すれば, mconcat は特に定義しなくても使用可能になります (ちなみに [a]a のリストを表します). まあ, よくわからければ, 後で JavaScript で同じことを書くので, そちらを見たほうがよいかもしれません.

最も簡単な Monoid のインスタンスの例として, 真偽値 Bool と論理和 (||) によるモノイドを考えます. この場合, 単位元 memptyFalse, 演算 mappend(||) となります. (本当はモノイド則を満たすかも考えなければならないけれど, ここでは説明を省略します.)

instance Monoid Bool where
    mempty = False
    mappend = (||)

mempty :: Bool                -- False
mappend True mempty           -- True
mappend mempty False          -- False
mappend True True             -- True
mconcat [False, True, False]  -- True
mconcat [False, False, False] -- False

こうすると, memptyFalse, mappend が論理和 (||) となるのは当然 (そう定義したので) ですが, さらに mconcatBool のリストの論理和による総和 (畳み込み) を求める関数として使えることもわかります.

JavaScript でも, Show の場合と同じように定義してみましょう (mconcat にデフォルト実装が含まれていることに注意).

var mempty = function (A) {
    if (A.__Monoid__ && A.__Monoid__.hasOwnProperty("mempty")) {
        return A.__Monoid__.mempty;
    }
    else {
        throw new Error("No instance declaration for Monoid");
    }
};
var mappend = function (A) {
    if (A.__Monoid__ && A.__Monoid__.hasOwnProperty("mappend")) {
        return A.__Monoid__.mappend;
    }
    else {
        throw new Error("No instance declaration for Monoid");
    }
};
var mconcat = function (A) {
    if (A.__Monoid__ && A.__Monoid__.hasOwnProperty("mconcat")) {
        return A.__Monoid__.mconcat;
    }
    else {
        // デフォルト実装
        return function (arr) {
            return arr.reduceRight(
                function (y, x) { return mappend(A)(x, y); },
                mempty(A)
            );
        };
    }
};

Monoid のインスタンスの例として, Haskell で定義したものと同じ真偽値 Boolean と論理和 || によるモノイドは以下のように書けます.

Boolean.__Monoid__ = {
    mempty: false,
    mappend: function (x, y) { return x || y; }
};

mempty(Boolean);                          // false
mappend(Boolean)(true, mempty(Boolean));  // true
mappend(Boolean)(mempty(Boolean), false); // false
mappend(Boolean)(true, true);             // true
mconcat(Boolean)([false, true, false]);   // true
mconcat(Boolean)([false, false, false]);  // false

Haskell の Monoid とまったく同じことができています. すばらしー.

MonoidBoolean だけでなく, String, NumberArray (と, それらに対する結合などの演算) に対しても定義できて, 様々な種類のデータを処理 (畳み込み (folding) とか) する方法を効率よく実装できるようになります. 詳細は, なんだか別の話になりそうなのでここでは省略します (興味がある人は調べたり考えたりしてみてください) が, いずれにせよ, Monoid も JavaScript で表現が可能であることがわかりました.

Monad

Haskell の代名詞的な存在のモナド Monad についても JavaScript で表現してみましょう. 型クラスの定義は以下のようになっています. (本当は fail がありますが, 省略. あと, なんか全称量化子 forall とかありますが, どうせ JavaScript では関係ないのでとりあえず無視する方向で.)

class Monad m where
    return :: a -> m a
    (>>=)  :: forall a b. m a -> (a -> m b) -> m b
    (>>)   :: forall a b. m a -> m b -> m b
    m >> k = m >>= \_ -> k

型変数 m が型引数をとっている (例えば m a, m b) ことが, 今までと異なることがわかります.

だからといって別にやることは変わりません. JavaScript による Monad の表現はこんな感じです.

// JavaScript では return は予約語なので, モナドの m を先頭につけておく
var mreturn = function (M) {
    if (M.__Monad__ && M.__Monad__.hasOwnProperty("mreturn")) {
        return M.__Monad__.mreturn;
    }
    else {
        throw new Error("No instance declaration for Monad");
    }
};
// (>>=) は bind と呼ぶっぽい
var mbind = function (M) {
    if (M.__Monad__ && M.__Monad__.hasOwnProperty("mbind")) {
        return M.__Monad__.mbind;
    }
    else {
        throw new Error("No instance declaration for Monad");
    }
};
// (>>) は then としておきます (then という呼び方が一般的かは知りません)
var mthen = function (M) {
    if (M.__Monad__ && M.__Monad__.hasOwnProperty("mthen")) {
        return M.__Monad__.mthen;
    }
    else {
        // デフォルト実装
        return function (m, k) {
            return mbind(M)(m, function () { return k; });
        };
    }
}

もう何も怖くないですね.

Monad が何の役に立つのかは, たぶん私の言語能力では伝わらないと思うので, ここでは詳細な説明はしません. ですが, リスト (配列) 操作, エラー処理, 非同期処理, パーサ等, 割といろいろなところにモナド的なものが見出せます. Monad 型クラスがあれば, これらに統一的な記法や手続きを用意することが出来るようになります.

折角なので, 一つの例として Maybe モナドを実装してみましょう. Maybe モナドは値があるか, 何もないかを表す値に対して定義されるモナドで, 例えば普通なら null に対してメソッドを呼び出してしまいエラーが出るような場面がありますが, Maybe モナドでは値が存在しない場合に処理をしても, エラーを出すことなく処理を続けることができます. (やっぱり, 本当はモナド則を満たしているかを考えなければいけないのですが, 割愛.)

// 配列が空か, そうでないかで Maybe を定義
var Maybe = {
    nothing: [],
    just: function (x) { return [x]; }
};
Maybe.isNothing = function (m) { return m.length === 0; };

Maybe.__Monad__ = {
    mreturn: Maybe.just,
    mbind: function (m, f) {
        if (Maybe.isNothing(m)) {
            return Maybe.nothing;
        }
        else {
            return f(m[0]);
        }
    }
};

mreturn(Maybe)("world"); // ["world"]
// こっそり, 別の型クラス Show を混ぜてみたりして
var sayHello = function (M, A) {
    return function (x) {
        return mreturn(M)("Hello, " + show(A)(x));
    };
};
// just(true) (true という値がある) を sayHello に渡す
mbind(Maybe)(Maybe.just(true), sayHello(Maybe, Boolean)); // ["Hello, true"]
// nothing (値がない) を sayHello に渡す
mbind(Maybe)(Maybe.nothing, sayHello(Maybe, Boolean));    // []

うまく動きましたね.

まとめ

なんだかんだと JavaScript で型クラスを表現してきましたが, まとめるとインターフェース (あるいは抽象クラス ?) と型クラスの違いは以下の表のような感じです.

インターフェース 型クラス
アドホック多相関数
デフォルト実装
多相定数 ×
高階型 ×
既存の型の拡張 ×
(JS での) 使いやすさ

型クラスは, そのための機能が含まれている言語でしか使えないものではなく, JavaScript のような言語でも定義できます (ただし, 少々面倒であることは否めませんが). このような型クラスはインターフェースよりも広い範囲をカバーでき, 抽象化が不十分な部分に型クラスによる抽象化を導入することができれば, よりエレガント (未定義語) なプログラムを書くことが可能になるはずです (たぶん). それでは皆さん, よい抽象化ライフを!

OUCC では明日のアドベントカレンダーの記事を書く人を募集しています.

余談

今やってきたようなことを, 静的な型のある言語で, ちゃんと型を付けて実装しようと思うと, (面倒なので最小限の定義を型だけ書きますが) Monoid の場合は,

class Monoid<A> {
    mempty : A
    mappend : A -> A -> A
}

mempty<A> : Monoid<A> -> A
mappend<A> : Monoid<A> -> (A -> A -> A)

Monad の場合は,

// M<_> は, 型変数 M が 1 つの型引数をとることを表す
class Monad<M<_>> {
    mreturn : A -> M<A>
    mbind : M<A> -> (A -> M<B>) -> M<B>
}

mreturn<M<_>> : Monad<M<_>> -> (A -> M<A>)
mbind<M<_>> : Monad<M<_>> -> (M<A> -> (A -> M<B>) -> M<B>)

みたいな定義になると思います. もちろんこの場合も, 関数を使用する場合は, Monoid<A>Monad<M<_>> の (オブジェクト指向的な意味での) インスタンスを与える必要があります.

この定義はただの擬似コードですが, 型クラスを表現するには, 型引数 (ジェネリクス) と, 高階型変数 (型引数をとる型変数, M<_> のような) を扱えることが必要で, さらに実用的に使おうと思ったら, 型情報から暗黙的に Monoid<A>Monad<M<_>> を与えてくれるようなシステムがあると良いことがわかります.

実はここまで書いた後に調べて気づいたのですが, Scala にはこの両方があって, 実際 Scala での型クラスは大体こんな感じで実装されている, らしいですね (知っている人は記事を読んでいる途中で気づいていたかもしれませんが).

以上, 新規性はないという報告でした. アヘ顔ポンデケージョ☆(ゝω・)v

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.