この記事は OUCC アドベントカレンダー 2014 12日目の記事です. 岡山大学サイクリング部ではなく, 大阪大学コンピュータクラブです. 昨日は @klta6154 さんによるマスタリングTCP/IP OpenFlow編読みました記でした.
毎度ありがとうございます. (@susisu2413) です.
この記事では JavaScript を使って, Haskell などの言語に存在する "型クラス" のようなものを書いて遊んでみます. ちょくちょく Haskell が登場しますが, Haskell の知識がなくてもある程度読めるように配慮したつもりです. ただ, 流石に JavaScript の説明をしながら型クラスの説明をするのは地獄なので, JavaScript がある程度わかることを前提で書いています.
型が値の種類を表すとしたら, 型クラスは型の種類を表すようなものです. 例えば, ある型 (オブジェクト指向言語ではクラスなど) に属する値には, その型のメソッドを用いた操作が可能です. 同様に, "ある型クラスに属する型" に属する値には, その型クラスのメソッドを用いた操作を行うことが可能です.
オブジェクト指向言語におけるインターフェースと似ているという説明がされることがあり, 実際その通りなのですが, 型クラスの方がインターフェースに比べてより柔軟にメソッドの定義を行ったりすることが可能です.
ある型が型クラスに属している時, その型は型クラスのインスタンスであるといいます. ちょうど, オブジェクト指向言語でのクラス (型) とインスタンス (値) の関係と (名前が) 似ています. 型が型クラスに属するようにする宣言をインスタンス宣言といいます.
Haskell における (単純化した) Show
の定義は以下のようになっています.
class Show a where
show :: a -> String
日本語で言うと, "ある型 a
を 型クラス Show
のインスタンスにするには, a
の値を String
の値に変換する関数 show
を定義しなさい" ということです.
ここで, 関数 show
の型は Show a => a -> String
と表されます.
=>
の左側が, 右側の型変数 a
が型クラス Show
に属していることを表しています.
=>
の右側は単純に a
を String
に変換する関数である, ということですね.
例えば文字列 String
, 整数 Int
, 真偽値 Bool
などは Show
のインスタンスとして宣言されていますので
(実際にどのようにインスタンス宣言がされているかは面倒なので省きますが),
以下のようにして String
に変換が可能です.
show "abc" -- "\"abc\""
show 123 -- "123"
show True -- "True"
同じ関数 show
を, 別の型に属する値に適用している点に注意してください.
このような多相 (同じ名前 (この場合 show
) が複数の型 (この場合 String -> String
, Int -> String
等) に属する値を表す的な)
をアドホック多相 (c.f. パラメトリック多相) と言います.
この時点では所謂オーバーロードと大差ないですね.
JavaScript の場合, ほとんどすべての (null
と undefined
を除く) 値は 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
の値を show
で String
にしたい場合は, Foo.__Show__.show
を定義する (== インスタンス宣言する) だけで済みます.
show
の後に型を書くのが面倒なのは諦めろ.
この JavaScript 版の show
の型について考えてみましょう.
show
は最初に引数として型クラス Show
に属する型 A
(今の場合 String
や Number
など), すなわち型引数をとり,
"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 は数学におけるモノイドと同じで, Haskell での型クラスの定義は以下のようになっています.
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
mconcat = foldr mappend mempty
なんだか新しい概念が登場しました.
mempty :: Monoid a => a
は多相定数と言われる定数で, 文脈 (期待される型) によって表す値が変わる定数です.
さらに, mconcat
にはデフォルト実装があります.
これにより, インスタンス宣言時に mempty
と mappend
のみ定義すれば, mconcat
は特に定義しなくても使用可能になります
(ちなみに [a]
は a
のリストを表します).
まあ, よくわからければ, 後で JavaScript で同じことを書くので, そちらを見たほうがよいかもしれません.
最も簡単な Monoid
のインスタンスの例として, 真偽値 Bool
と論理和 (||)
によるモノイドを考えます.
この場合, 単位元 mempty
は False
, 演算 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
こうすると, mempty
が False
, mappend
が論理和 (||)
となるのは当然 (そう定義したので) ですが,
さらに mconcat
が Bool
のリストの論理和による総和 (畳み込み) を求める関数として使えることもわかります.
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
とまったく同じことができています. すばらしー.
Monoid
は Boolean
だけでなく, String
, Number
や Array
(と, それらに対する結合などの演算) に対しても定義できて,
様々な種類のデータを処理 (畳み込み (folding) とか) する方法を効率よく実装できるようになります.
詳細は, なんだか別の話になりそうなのでここでは省略します (興味がある人は調べたり考えたりしてみてください) が,
いずれにせよ, Monoid
も JavaScript で表現が可能であることがわかりました.
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