以前、C#からF#への変換によって、F#に触れるきっかけを作ろうとしました(id:n7shi:20090722, id:n7shi:20101226)。この方法だととりあえずF#で何か作れるようにはなりますが、F#特有の概念の理解にはあまり役に立ちません。
ぜくるさんが判別共用体について議論されているのを見て、判別共用体を使ったことがないのに気付きました。
判別共用体の存在は知っていましたが、どういう時に使えば良いのかピンと来ていませんでした。そこで今度は逆にF#からC#に変換しながら考えてみました。F#ではC#で感じていた手間が軽減されていることに気付き、自分の中で判別共用体の位置付けが少し固まった気がします。
C#との比較
結論から言うと判別共用体は「継承による多態を簡潔に記述」できる構文です。
ぜくるさんの例を改変して引用します。ぜくるさんの本来の議論からは外れますが、アクティブパターンやパイプライン演算子のことまで考えると混乱するので、判別共用体に焦点を絞っています。
type PropLogic = | And of PropLogic * PropLogic | Not of PropLogic | True | False let rec evaluate = function | And(x, y) -> (evaluate x) && (evaluate y) | Not x -> not (evaluate x) | True -> true | False -> false let hoge = And(True, Not(True)) printfn "%b" (evaluate hoge)
これをC#に変換してみます。
using System; abstract class PropLogic { public abstract bool Evaluate(); } class And : PropLogic { public PropLogic x, y; public And(PropLogic x, PropLogic y) { this.x = x; this.y = y; } public override bool Evaluate() { return x.Evaluate() && y.Evaluate(); } } class Not : PropLogic { public PropLogic x; public Not(PropLogic x) { this.x = x; } public override bool Evaluate() { return !x.Evaluate(); } } class True : PropLogic { public override bool Evaluate() { return true; } } class False : PropLogic { public override bool Evaluate() { return false; } } class Test { static void Main() { var hoge = new And(new True(), new Not(new True())); Console.WriteLine("{0}", hoge.Evaluate()); } }
F#とC#を比較すると、C#では継承とvirtualで表現しているため、評価関数の実装が分散して冗長になっています。評価関数をまとめてみます。
using System; abstract class PropLogic {} class And : PropLogic { public PropLogic x, y; public And(PropLogic x, PropLogic y) { this.x = x; this.y = y; } } class Not : PropLogic { public PropLogic x; public Not(PropLogic x) { this.x = x; } } class True : PropLogic {} class False : PropLogic {} class Test { static bool evaluate(PropLogic pl) { if (pl is And) return evaluate(((And)pl).x) && evaluate(((And)pl).y); else if (pl is Not) return !evaluate(((Not)pl).x); else if (pl is True) return true; else if (pl is False) return false; else throw new Exception(); } static void Main() { var hoge = new And(new True(), new Not(new True())); Console.WriteLine("{0}", evaluate(hoge)); } }
記述はF#に近くなりました。評価関数でのキャストが目に付きますが、これはどうしようもありません。コンストラクタを削減するともう少し短くなります。
using System; abstract class PropLogic {} class And : PropLogic { public PropLogic x, y; } class Not : PropLogic { public PropLogic x; } class True : PropLogic {} class False : PropLogic {} class Test { static bool evaluate(PropLogic pl) { if (pl is And) return evaluate(((And)pl).x) && evaluate(((And)pl).y); else if (pl is Not) return !evaluate(((Not)pl).x); else if (pl is True) return true; else if (pl is False) return false; else throw new Exception(); } static void Main() { var hoge = new And { x = new True(), y = new Not { x = new True() } }; Console.WriteLine("{0}", evaluate(hoge)); } }
かなり頑張りましたが、利用側での記述があまりに冗長です。ぜくるさんのマルチパラダイムの例のように、インスタンス生成関数を提供するべきでしょう。
C#では継承とvirtualで書くことが多く、別の箇所でまとめることは少ないように思います。敢えてまとめて書くと、アスペクト指向的に実装を横断しているという印象を受けます。
いずれにしてもF#の書き方は簡潔です。F#を単にC#からの直訳で使っていても、構文が簡潔だということに多大なメリットを感じていましたが、このように特化した構文ではそれが顕著です。
AST
今回の例ではASTを組み立てて値を評価しています。そういえばコンパイラを作ったとき(id:n7shi:20110111)、コンパイル時に値を評価するため似たようなことを書いたのを思い出しました。
これはF#がハマるパターンなので、C#の例はわざと直訳で複雑になっているような印象を与えるかもしれません。しかし実際に私がC#でコンパイラを作ったとき、何の疑問もなくそういう複雑な書き方をしていました。
コンパイラを作るとき「まずは動かす」ことだけを考えていたので、複雑なことをすっきり書ける言語仕様にまで頭が回りませんでした。何が目的で何が手段なのかよくわからない状態ですが、あまり深く考えていないので悪しからず。