オブジェクト指向フレームワークとワークフロー(モナド)

 今現在、Javaでフレームワークを設計する仕事をしている以上、今作っているようなものをどのような形にすればF#に持ち込むことができるのか、ということには当然興味の焦点のひとつになってしまいます。
 以前、Javaでやっているライブラリを移植しようとあれこれ考えているうちに、結局F#でもオブジェクト指向に走ってしまい、結局プログラムの枠組みが変わらない、そのまま移植したようなプログラムになってしまったことがあります。オブジェクト指向で作っていると、ある程度は破壊的代入を前提としたクラスを作らざるを得ず、「これならC#でもいいじゃないか」と自分で思ってしまうようなプログラムでした。
 こういう選択肢を取れることもF#(OCaml)の強みなのかも知れませんが、できることならF#なりのプログラムの形を追及してみたいところです。

ポリモルフィズムを考える

 オブジェクト指向フレームワークで使われる手法の代表格、ポリモルフィズム(多態性)ですが、この多態性には使い方ベースで二つの分類があることに気づかれているでしょうか?一つは「外側からの多態性」で、もう一つが「内側からの多態性」です。と言う言葉は僕が勝手に使っているもので、他でどう読んでいるかは実はよく知りません。しかしオブジェクト指向でメシを食っている人は、多かれ少なかれ両方を日常的に使いこなしているはずです。

外側からのポリモルフィズム

 最も分かりやすく、一般的な使われ方の多態性です。もうサンプルなど乗せる必要もないと思いますが、「外側から」がイメージできるように簡単なものを載せておきます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OOPSample
{
    abstract class Animal
    {
        abstract public String Bark();
    }

    class Dog : Animal
    {
        override public String Bark()
        {
            return "Bow-wow!";
        }
    }
    class Coq : Animal
    {
        override public String Bark()
        {
            return "cock-a-doodle-doo!";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Animal[] animals = {new Dog(), new Coq()};
            foreach (Animal animal in animals) {
                Console.WriteLine(animal.Bark());
            }
        }
    }
}
 こうやってC#のコードを貼ると、F#のソースがどれだけ簡潔なものになっているかはっきりと分かりますね。上のコードについては解説するほどのものでもないと思いますが、Mainメソッドの中を見ると、animal.Bark()という風に、仮想関数をオブジェクトの外からCallしているところから、「外からのポリモルフィズム」と呼んでいます。単純明快なので、先に身に付けるポリモルフィズムは普通こちらだと思います。

内側からのポリモルフィズム

 今度のポリモルフィズムは、仮想関数を基底クラスから呼び出すものです。
using System;
using System.Collections.Generic;
using System.Text;

namespace OOPSample
{
    abstract class Animal
    {
        abstract protected String Bark();
        abstract protected int Legs();

        public void BarkOperation() {
            Console.WriteLine(Bark());
            Console.WriteLine("I have {0} legs\n", Legs() .ToString());
        }
    }

    class Dog : Animal
    {
        override protected String Bark()
        {
            return "Bow-wow!";
        }
        override protected int Legs()
        {
            return 4;
        }
    }
    class Coq : Animal
    {
        override protected String Bark()
        {
            return "cock-a-doodle-doo!";
        }
        override protected int Legs()
        {
            return 2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Dog dog = new Dog();
            dog.BarkOperation();
            Coq coq = new Coq();
            coq.BarkOperation();
        }
    }
}
 長っ!こういう括弧の付け方を強要する言語ってあんまり好きになれないなぁ・・・。
 こういうポリモルフィズムは、以下のような状況で使われます。
  1. プログラムの全体の流れは決まっているが、その中の一部の処理だけが処理によって異なる、または決まっていない。
  2. そういうケースで、全体の流れを基底クラスのメソッドで実装し、処理が異なる一部の処理を純粋仮想関数で定義しておく(フレームワーク実装者の仕事)。
  3. 派生クラスで純粋仮想関数を実装することで、プログラム全体の定義を完了する(業務実装者の仕事)。
 MFCみたいな古典的なフレームワークはこんな作りになってることが多いです。
 ちなみに、この基底クラスで実装されているBarkOperationメソッドを別のクラスで実装し、派生クラスのオブジェクトを外から与えるようにしたのがBuilderパターンと言えます。依存性云々の関係でで最近はこっち(Builderパターン)のほうが主流になっているみたいですね。僕は個人的にあんまり好きではないのですが(業務側ソースの絵面がごちゃごちゃするので)。
 どっちにしても、なんだかこれはワークフローで書き換えられそうな気がしてきます。
 上のAnimalクラスをワークフローに書き直してみましょう。

F#で書き換えたコード

type OopTransType = Bark of string | Legs of int | Terminate

type OopTransBuilder () =
  member this.Bind (res : OopTransType , cont : unit -> OopTransType) =
    match res with
      | Bark x -> printfn "%s" x
                  cont ()
      | Legs x -> printfn "I have %d legs" x
                  cont ()
      | Terminate -> Terminate
  member this.Return(x : 'a) = Terminate   
  member this.Delay(restOfComputation : unit -> OopTransType) =
    fun () -> restOfComputation () 

let ooplike = OopTransBuilder()

let dog = 
  ooplike {
    do! Bark "bow-wow"
    do! Legs 4
  }

let coq =
  ooplike {
    do! Bark "cock-a-doodle-doo!"
    do! Legs 2
  }

実行結果
> dog ();;
bow-wow
I have 4 legs
val it : OopTransType = Terminate

 構造的に、サブクラスでオーバーライドされていた部分が後半のooplike{}の中にまとめられ、基底クラスのBarkOperationで実装されていたものがBindに移されたような状況です。
 こうしてみると、業務側の記述はオブジェクト指向と比較してずいぶんすっきりしますね。ただ、オブジェクト指向でも抽象化が進んだフレームワークではずいぶん「業務をこのように書くと裏の仕組みがどうなってて動くのか分かりにくい」と言われたものですが、ワークフローではそれに拍車がかかった感じがしないでもありません。ただ、OOPのほうは、仮想関数を実装してもそれがどの順で実行されるのか、基底クラスやDirectorクラスを見ないと分かりませんでしたが、ワークフローではそのあたりが業務側に安心感を感じさせてくれる記述になっている気がします。

プログラムの構造とその変化

 「内側からのポリモルフィズム」にしろ「Builderパターン」にしろ「ワークフロー」にしろ、やろうとしていることの元々の形は以下のものです。
  1. 業務ごとに処理は異なるが、出力の型は共通している処理A(業務処理A)
  2. 業務処理Aの結果Aを受け取り、水面下で行われるべき処理a(共通処理a)
  3. 共通処理aの結果aもしくは業務処理Aの結果がパススルーされたもの)を受け取る(もしくはなにも受け取らずに処理する)、業務ごとに処理は異なるが、出力の型は共通している処理B(業務処理B)
  4. 業務処理Bの結果Bを受け取り、水面下で行われるべき処理b(共通処理b)
 このようなプログラムは、まるで刺繍糸が布の表と裏を行き来しながら縫い目のきれいな表面だけを見せるように、イルカが水面を時折ジャンプしながら泳ぐように、見えているこちら側の美意識を守るため、向こう側の処理をあえて見えないようにしておくような構造を持っています。
 他の人はどうか知りませんが、僕はなんとなく以下のようなイメージでいつもコーディングしています。作るべきもの全体を見渡して、大きな流れで共通するものを探し、どうしても共通化できない部分をモチのようにつかみ出して水面上に出し、その部分だけインターフェースを書いて業務の人に実装をお願いするイメージです。
 オブジェクト指向の場合は上の絵を頭に浮かべてそれでうまくいっていたのですが、ワークフローの場合はこの絵がBindの持つ多重性と全体の再帰的な構造のため奇妙にねじれてしまうのです。まるでクラインの壺のごとく。それのよじれ感をうまく図にしたいのですが。
 構造的な変化は大きく分けて二つあると思います。

ビルダーメソッドの重層化

 OOPでは、業務と業務の間に水面下で行われる実装は、先ほどの図のようにビルダーメソッドで仮想関数呼び出しの間に記述されていました。
 しかし、ワークフローのビルダークラスでは、これがBindメソッドのmatchの中に押し込められます。
 この二つの違いは、業務側の実装自由度に現れてきます。OOPでは、業務側の記述がいつどの順番で実行されるかは完全にフレームワークの実装に依存していて、業務側で変更できないようになっていましたが(BuilderパターンにすればDirectorクラスの実装を変えることで対応できますが、水面下の実装はDirecotorクラスに記述されることになるので、このクラスは業務側で書くべきものではないはずです)、ワークフローでは前の関数の戻り値と次の行の引数のつじつまが合えばDSLのようにある程度自由な記述が可能となります。
 例えば上のdogコンピュテーション式は
let dog = 
  ooplike {
    do! Legs 4
    do! Bark "bow-wow"
  }
 のように書き換えても動きますが、「内側からのポリモルフィズム」ではこのような変更を許すことができませんし、BuilderパターンではDirectorクラスの修正が必要になります。
 コンピュテーション式の行と行の間でBindの中のどの処理が実行されるかは、前の行が判別共用体のどの型を返すかによって決まります。上の例では「Legs 4」は「Legs of int」を返すので、
      | Legs x -> printfn "I have %d legs" x
                  cont ()
 にmatchしてこの行が実行されることになります。

Bindメソッドの再帰構造

 あまり意識するようなものでもないのかもしれませんが、OOPでは
業務処理A
共通勝利a
業務処理B
共通勝利b
 のようにベタッと書かれていたものが、ワークフローを使うと
業務処理A
共通勝利a
  業務処理B
  共通勝利b
 のように自動的に入れ子構造に展開されます。しかしまぁこれは、OOPでも入れ子構造に書けないことはないのでたいした違いはないかもしれませんが、入れ子(再帰)構造のコードは決して読みやすいものではないので、ベタッと書けば勝手に入れ子にしてくれるのは、コードの保守性という意味ではありがたいと言えるかもしれません。ただ、Bindが入れ子に展開されるということが実装者に分かっていないと、デバッグのときなどに何が起こっているのかわからない、ということになりかねませんが。
 とにかくこのような構造を先ほどの図に組み込むと、こんな感じになります。図的な理解と合わせて読んでいただけるとより理解しやすいかもしれません。
 この再帰的な構造がイメージしていただけるでしょうか。この図に描ききれていないのですが、一つ重要なことがあります。この図に表れる(青い四角で囲まれた)二つのBindと一つのReturnは、同じオブジェクトのメソッドになります。Bindで呼び出した継続処理の中で、もう一度自分が中から呼ばれているようなちょっと不思議な構造です。業務処理はベタッと縦に並べて書かれていても、実はこのように再帰的な呼ばれ方をしているのです。

まとめ

 少なくとも、オブジェクト指向でBuilderパターンと呼ばれているような構造は、ワークフローで書き直せることが分かりました。しかし、業務側にDSL的な自由度を許す必要がない場合は、普通にBuilderパターンや内側からのポリモルフィズムを利用したほうが、プログラムはずっとわかりやすく、メンテしやすくなるでしょう。
 しかし、業務側が業務内容に応じてfsxスクリプトで書いて実行するような、業務側に高い自由度と安全性が求められる場合には、ワークフローは非常に強力な手段になり得ます。F#ではこういう手法が取れることを常に頭の隅に置いておき、適切なときにこの仕組みを利用できれば、F#使いの面目躍如といったところです。

(文責:片山 功士  2012/01/10)


今日: -
昨日: -
トータル: -
最終更新:2012年02月03日 01:59