BRMSで実験してみました-宣言的プログラミングのすすめ (2)-

Droolsを使って、ルールベース(BRMS)の動きを見るためにちょっと実験してみました。Droolsで、Projectをつくるとサンプルのプログラムをつくってくれますが、それをすこしばかり修正しての実験です。
(ルールベースの動きの基本は、プロダクションシステムとはとか、当ブログのルールベースプログラミングのカテゴリなどを参照ください)

まずは、ルール処理の対象となるファクトを準備します。

SampleFact.java

package com.sample;

public class SampleFact {
    private int num;
    private String name;

    public SampleFact() {
        super();
    }

    public SampleFact(int num, String name) {
        super();
        this.num = num;
        this.name = name;
    }

    /**
     * @return the num
     */
    public int getNum() {
        return num;
    }

    /**
     * @param num the num to set
     */
    public void setNum(int num) {
        this.num = num;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "SampleFact [num=" + num 
                   + ", name=" + name + "]";
    }

}

そして、mainプログラム

DroolsTest.java

package com.sample;

import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;

/**
 * This is a sample class to launch a rule.
 */
public class DroolsTest {
  public static final void main(String[] args) {
    try {
      // load up the knowledge base
      KieServices ks
       = KieServices.Factory.get();
      KieContainer kContainer
       = ks.getKieClasspathContainer();
      KieSession kSession
       = kContainer.newKieSession("ksession-rules");

      // go !
      SampleFact sf1 = new SampleFact(1, "A");
      SampleFact sf2 = new SampleFact(2, "B");
      SampleFact sf3 = new SampleFact(3, "C");
      SampleFact sf4 = new SampleFact(4, "D");
      SampleFact sf5 = new SampleFact(5, "E");
      kSession.insert(sf1);
      kSession.insert(sf2);
      kSession.insert(sf3);
      kSession.insert(sf4);
      kSession.insert(sf5);
      kSession.fireAllRules();
    } catch (Throwable t) {
      t.printStackTrace();
    }
}

上のように5つのファクトをワーキングメモリに追加して、以下のルール

Sample.drl

package com.sample

rule "サンプルルール1"
when
    SampleFact($num: num, $name: name)
then
    System.out.println("num=" + $num
         + ", name=" + $name);
end

を実行しました。
コンソールには、どのように表示されるでしょうか。

まあ、これはワーキングメモリに5つのファクトがあるので、単純に
条件節(when部分)のSampleFactにマッチして、5回ルールのサイクルが回ります。

num=5, name=E
num=4, name=D
num=3, name=C
num=2, name=B
num=1, name=A

つまり条件部にマッチしたファクトが
SampleFact(1, “A”);
SampleFact(2, “B”);
SampleFact(3, “C”);
SampleFact(4, “D”);
SampleFact(5, “E”);
の5つだったということ。

では、上記のサンプルルール1のかわりに次のルールがあった場合は
どうなるでしょうか

Sample.drl(改)

package com.sample

rule "サンプルルール1a"
when
    SampleFact($num: num > 3, $name: name)
then
    System.out.println("num=" + $num
         + ", name=" + $name);
end

このときは条件節に num > 3 という条件が加わったので
num=5, name=E
num=4, name=D
の2つのみが表示されます。

すなわち条件部にマッチしたファクトが
SampleFact(4, “D”);
SampleFact(5, “E”);
の2つということ。

さらに次はどうでしょう。

Sample.drl(改々)

package com.sample

rule "サンプルルール2"
when
    SampleFact($num: num, $name: name)
    SampleFact($num1: num, $name1: name)
then
    System.out.println("num=" + $num
         + ", name=" + $name
         + ", num1=" + $num1
         + ", name1=" + $name1);
end

今度は、条件節のSampleFactのパターンが2つになりました。結果は、
num=1, name=A, num1=1, name1=A
num=1, name=A, num1=2, name1=B
num=1, name=A, num1=3, name1=C
  ・・・(中略)・・・
num=5, name=E, num1=4, name1=D
num=5, name=E, num1=5, name1=E

以上、25行表示されました。

これは、条件節の最初のパターン
SampleFact($num: num, $name: name)
に5つのファクトがマッチして、さらに2番目のパターンにも
SampleFact($num1: num, $name1: name)
5つのファクトがマッチして結局その組合せとして5×5=25(通り)の
組合せが表示されているということを表しています。つまり

条件部にマッチしたファクトの組を
[<最初のパターンにマッチしたファクト>,<2番目のパターンにマッチしたファクト>]
の形であらわすとすると、

[SampleFact(1, “A”), SampleFact(1, “A”)]
[SampleFact(1, “A”), SampleFact(2, “B”)]
[SampleFact(1, “A”), SampleFact(3, “C”)]
  ・・・(中略)・・・
[SampleFact(5, “E”), SampleFact(4, “D”)]
[SampleFact(5, “E”), SampleFact(5, “E”)]

の25個のファクトの組が条件部にマッチしたことになります。

では、こんなルールであったらどうでしょうか。

Sample.drl(もひとつ改)

package com.sample

rule "サンプルルール2a"
when
    SampleFact($num: num, $name: name)
    SampleFact($num1: num > $num, $name1: name)
then
    System.out.println("num=" + $num
         + ", name=" + $name
         + ", num1=" + $num1
         + ", name1=" + $name1);
end

(この項続く)

if-then文とif-thenルール -宣言的プログラミングのすすめ (1)-

この前の更新からずいぶんと間があいてしまいましたが、最近、ビジネスルールというか、「ルール」というものの認識についてどうも気になることがあったので、考え方としては前回と重なる部分も多いかと思いましたが、再び記事を書いてみました。またこれに限らず、ぼちぼちと記事を書いていきたいと思っております。

さて、ちょっと唐突ですがプログラムにおけるif-then文と、一般的なルールとしてのif-thenの違いはどこにあるでしょうか。
(なお、ここで言うルールとしてのif-thenは、実際にBRMSに実装されるif-thenルールではなく、それ以前のまずは仕様(文書)としてまとめられた(ビジネス)ルールとします)

これら if-then文 と if-then ルール、同じ if-then ではあるので、それほど変わらないように感じられるかもしれません。私自身もふだんは明確な使い分けをせずあいまいな書き方をしていたりもしますが、実は大きな違いがあります。

まず、プログラムにおけるif-then文。これは、プログラムの上から下に流れる処理の流れが前提にあって、何もしなければif-thenは1回しか通りません。もう一度通すためには繰り返しを書く必要があります。つまりプログラムのif-then文は処理の流れを意識しないと成り立たず、処理の流れはすべて記載しなければなりません。すなわちプログラムのif-then文は、いわゆる手続き的 (procedural) にしか解釈されません。

一方、ルールにおけるif-thenというものは、どうでしょう。一般にルール(規則)というものは、条件 (if部) が合うのであれば常に適用されるもので、そこに処理の流れというものは存在しません。仮にいくつかif-thenルールがあったとしましょう。原則それぞれのルールは独立してそれぞれ条件が合うかどうかのみで、適用される/されないが判断されます。そこにどの順に処理するかという処理の流れはありません。

たとえば

 if 赤信号 then 止まる

というルールですが、これを手続き的に解釈して処理の手順を考え、先ほど赤信号を処理したので、次の赤信号は処理できず結果的に無視してしまうということはないでしょう。どんな状況でも、条件さえ合えば常に適用されるというのがルールです。

無理やり手続き的に書こうとすると、「if 赤信号 then 止まる」という if-then文 をループで囲って、1回赤信号を処理しても、次の赤信号も処理できるように待っているといった書き方になります。

また、個々のデータが○か×の値をとる配列があり、

 if × then エラー出力

などといったルールでも同じです。ルールそのものは上記で書けても、手続きを明示しないといけないので、if-then文 を配列全体にわたってループする・・・という追加の記述が必要になります。

まとめますと、仕様としての if-then ルールを手続き的に書こうとすると、ループなどの処理手順も明示する必要があり、それが複雑になると本来の仕様が処理手順に埋もれてしまうということになります。たとえば前回の記事などのようにデータに親子関係があり、複数の子の間での制約など if-then ルールとして記述すれば比較的わかりやすいけれども、手続き的に書こうとすると面倒というケースです。従来の手続き的なプログラミングの問題点の一つはそんなところにもあるのではないでしょうか。

    ***

さて、みなさまは「宣言的」(あるいは宣言型)プログラミングという言葉をお聞きになったことがありますか?

Wikipediaの宣言型プログラミングという項には、

「宣言型プログラミング(英: Declarative programming)は、プログラミングパラダイムの名称だが、2種類の意味がある。第一の意味は、処理方法ではなく対象の性質などを宣言することでプログラミングするパラダイムを意味する。第2の意味は、純粋関数型プログラミング、論理プログラミング、制約プログラミングの総称である。」

とあります。

上にあげた第2の意味は、現在一般的に認知されているプログラミングパラダイムの中で、第1の意味での宣言型プログラミングを具現化しているものとも解釈できるかと思います。そういった視点で第2の意味に加えるとしたら、プログラミングパラダイムとして一般的に必ずしも認知されているわけではないけれども、たとえばSQLなどはあげられるでしょう。実は、前回あげた ルールベース(プロダクションシステム)のプログラミングも宣言的なプログラミングの仲間に入れていいかと思います。

宣言型プログラミングの第1の意味では、処理方法ではなく対象の性質などを宣言することでプログラミングするパラダイムを言いますが、「対象の性質」と言っても、なかなかわかりにくいのでもう少しかみ砕いた記述を見てみましょう(宣言型プログラミングの可能性と限界より)。

「宣言型プログラミングが記述するものは、問題の定義、すなわち解くべき問題の性質や、その際に満たすべき制約の記述です。」

「対象の性質」をかみ砕くと「問題の定義、すなわち解くべき問題の性質や、その際に満たすべき制約」ということになるでしょうが、これをルールベースによる宣言的プログラミングに置き換えると、if-then ルールを使って、問題の性質や制約を記述することになりましょう。このことは何を意味しているのでしょうか。

    ***

仕様としての if-then ルール、これは問題の定義-問題の性質や制約を記述-を記述したものなので、実は「宣言的プログラミング」の意味での「宣言的」であるといえます。

これを実装する場合、従来のプログラミングでは if-then ルール を手続き的に直して書くだけだったので、本来の仕様以外に処理手順なども記述する必要がありました。

手続き的

これにより本質がぼやけてしまいプログラムの可読性が落ちることもありましたが、これはすなわち本来宣言的に表しているものを手続き的に表そうとすることで仕様と実装との間でのミスマッチ(注0)が起こっていたということなのです。

もし宣言的に表されている仕様を、そのまま宣言的に表せるプログラム手法があるのであれば、それに越したことはなく、プログラムの可読性を落とすことなく実装を行うことができます。

宣言的

ここであらわれたのがルールベースのプログラミング(プロダクションシステム)でした。もともとルールベースの技術は人工知能の応用としてエキスパートシステムの構築のために生まれてきたわけですが、このルールベースにおける if-then ルールの動きというものが、条件 (if部) さえ合うのであれば常に適用されるという、まさに仕様としての if-then ルールの動きと一致したわけです(注1)。

すでに前回にも書きましたが、ルールベースのプログラミングでは、if-then 文とループを使って処理手順を記述するのではなく、if-then ルールのみの仕様を記述することでプログラミングを行います。

で、if-then ルールを書けば、あとはルールのエンジンが、いい塩梅で実行してくれるというわけ。

たとえて言えば、手続き型のプログラミングは、処理の手順を一から教えなければならない新入社員のようなものである一方、ルールベースによる宣言的プログラミングは、「これこれをやっておいて(問題の性質や制約 ! )」と伝えておけば、それなりにいいようにやってくれる、常識的な処理手順はわかっている入社1~2年目の社員といったところでしょうか。

    ***

もっとも、このルールベースでのプログラミング、他の宣言的なプログラミング手法と同様、実際に実行するという視点から言えば良いことばかりではなくて、エキスパートシステム構築ツールの時代からメモリなどのコンピュータリソースを食うデメリットがありました。

ただ、昔のスーパーコンピュータの性能が ipad2と同じくらいであるといわれるようになった今日、プログラムの開発・メンテナンスのコストに対し、コンピュータリソースのコストは相対的に飛躍的に減少しているので、それほど気にすることはないでしょう。

    ***

まあ、そうは言っても実際のところ単項目のエラーチェックなどで、if-then文とif-thenルールとの違いがあまり表面化しない場合(単純なデータ構造で1回 if-then のチェックをすればよい場合など)も多々あるので、if-thenルールを記述できるBRMSを使うまでもないということもあるでしょう。

そんなときには、ちょっと手軽で安価な、手続き的に if-then文 を処理するBRMS (たとえばOpenRulesのsequential rule engine) を使ってもよいかと思います。

ただルールを手続き的な if 文で書こうとすることは、本質的にはJava(注3)やCobolなどなど従来の手続き型プログラミングをなぞっているという域を出るものではなく、本来のルールの宣言性を生かした宣言型のプログラムにはならないということは、きちんと認識しておいた方がよいかと思います。

手続き的な処理方法のBRMSで多少複雑な処理を行おうとすると、本来のBRMSのメリットである可読性が失われてしまい、メンテナンスの視点からは、結局従来のプログラムと何ら変わらないことになってしまうでしょう(注2)。要するに限界は限界として認識した上で、向き、不向きを判断して使うことが重要かと思います。

   ***

今回は、概念的な宣言的プログラミングの説明で終わってしまいましたが、どこかで宣言的プログラミングの例をもう少し書いてみたいと思います。たとえばビジネスの例ではありませんが、ポーカーの役の判定などは、手続き的に書くとするとループを回したりする必要がありますが、ルールベースで書くと役の定義に素直に、比較的直感的に書けるので、良い例になりそうだと思っています。

(注0) ミスマッチという言葉を使ったのは、ここを書いていて、一昔前によく言われていたオブジェクト指向言語からRDBを呼び出す際に、それぞれの拠って立つモデルが違うことから実装が複雑になる「インピーダンス・ミスマッチ」という言葉が頭に浮かんできたので・・・。

(注1) ちなみに、これが、現在の一般的なBRMSの多くが、エキスパートシステム構築ツールをルーツに持っているという所以です。たとえば、IBMのOperational Decision Manager、FICOのBlaze Advisor、RedHatのJBoss Rules、Oracle Business Rulesは、いずれもReteアルゴリズムベースのエキスパートシステム構築ツールをルーツに持ちます。またSAPのルールエンジンはReteベースのようですし、ProgressのCorticonは、DeTIという独自のアルゴリズムですが、動き的にはReteと同等です。

(注2) なお、OpenRulesにはそのために制約プログラミングをベースとした Rule Solver というもうひとつのルールエンジンがあり、上記にあげたルールベース的な動きではありませんが、宣言的なプログラミングができるようになっています。

(注3) 余談ですが、最近では Java も流行の関数型のパラダイム (上にも書きましたが宣言型のパラダイムの一つ) を取り入れるようになってきています。ルールベース的な宣言とは、ちょっと違うのですが、個人的には、関数型は関数型で結構好きだったりもします。

プログラミングパラダイムとしてのルールベース

サグラダファミリアBRMSの多くが依拠しているルールを用いたプログラミング(ルールベースプログラミング)について、最近はなかなか正面からの解説が少ないように思います。これは、最近のBRMSの適用例の多くが入力チェックや、査定、不正検知など、あまりルールベースのプログラミングのプログラミング上の特徴が表れてこない例が多く、あえてルールベースプログラミングを強調することもないということだからでしょうか。

(なお、ここで言う「ルール」を用いたプログラミングとは、通常のプログラミング言語で言うところの if 文を手続き的に整理した「ルール」ではなく、いわゆるプロダクションシステムにおける「ルール」、すなわちアルゴリズム的にはRete(およびその改良版)、DeTI、(最近では)Phreakなどを採用しているルールエンジンの「ルール」、を用いたプログラミングのことになります)

とはいえ、やはり通常のプログラミング言語 -手続き型- の延長としてルールベースプログラミングを理解してしまうと、時によってルールベースプログラミングの良さを引き出せないままBRMSを使ってしまったり…ということがありそうな例を (かなり前ですが) 見聞きしたのでちょっとここで紹介したいと思います。

例題
ある入力画面で、氏名の入力欄(たとえば保険金受取人)が
10段並んでいる。入力チェックとして、違う欄に同じ氏名が
入力されていたらエラーとしたい。

これ、通常のプログラミング言語で書こうとすると、二重の for 文 を回して同じ氏名が入力されていないかチェックするということになると思います。ところが、ルールベースプログラミングですと 「違う欄に同じ氏名が入力されていたらエラー」 ということをそのまま一つのルールとして実装するだけでプログラミングが完了します。

rule "重複チェックルール"
    when
        c1 : Column( id1 : id_No , name1 : name != "");
           // 名前が空白のものは対象外にする
        c2 : Column( id2 : id_No > id1, name == name1 );
           // 欄は違う (id_Noが違う)  けれども 名前は同じ。
    then
        System.out.println("重複がありました。名前は" + name1
                             + "id=" + c1.getId_No()
                             + " と id=" + c2.getId_No()
                             + "です。");
end

なお、ここで名前欄を表す

public class Column {
    private int id_No;
    private String name;
    // getter, setter  ほか
}

というクラスを用いています。

このルールを以下のデータ

Column [id_No=1, name=野中]
Column [id_No=2, name=伊丹]
Column [id_No=3, name=伊丹]
Column [id_No=4, name=大前]
Column [id_No=5, name=嶋口]
Column [id_No=6, name=野中]
Column [id_No=7, name=]
Column [id_No=8, name=野中]
Column [id_No=9, name=]
Column [id_No=10, name=]

に適用してみると、

重複がありました。名前は野中id=1 と id=6です。
重複がありました。名前は野中id=1 と id=8です。
重複がありました。名前は伊丹id=2 と id=3です。
重複がありました。名前は野中id=6 と id=8です。

といった結果になります。

このようにルールベースのプログラミングでは、繰り返しを明示的に書くことは多くの場合必要ありません。もちろん、ルールベースでもカウンタにあたる属性あるいはFactなどを作って繰り返しを表現することは可能なのですが、概して繰り返しを書かずとも望む結果が得られる場合が多いです。なので、もし繰り返しが必要になりそうなときは本当にその繰り返しが必要かどうか自問自答してみるべきです。

この繰り返しを書く必要がないというところは、次のルールベースプログラムの実行原則のひとつに拠っています。

条件に合うものは「全部」実行する。実行するものがなくなったら終了。

ここの「全部」というところは、通常の手続き型のプログラミング言語では for や while などや、さらにたとえば iterator を使って繰り返しも含めたプログラミングすることになるでしょうが、この繰り返しは、ルールベースでは上記にあげた実行原則に見るように、すでに実行の機構として言語に含まれてしまっています。したがってルールベースプログラミングでは、「何をするか」というルールの記述のみを行えばよいことになります。

翻って、人間の職場の作業依頼/指示について考えてみましょう。たとえば、書類の束を渡して、「XXのチェックしておいて」とか「チェックして印鑑お願いします」とか…「何を」行うかのみの依頼/指示だけで、手続き的に「上から順に」チェックするとか、「全部」チェックするとかの指示はしないのが普通でしょう。

ルールベースプログラミングの基本は、「どのように」処理を実行するかの記述を省き、単刀直入に「何をするか」ということをルールで記述していくところにあります。これがまさにルールベースプログラミングが宣言的プログラミングであるといわれる所以であり、また、ルールベースのプログラムが人間の感覚に近いと言われる所以でもあります。

上の例のルールを見てみましょう。処理そのものを追ってみると、

条件部の1行目(c1にあたるところ)は、Column( というところだけを見ると、データとしてあげた10個のColumnファクトすべてがマッチする可能性がありますが、name != “” というところまで見ていくと、10個のファクトのうち id_No=7,9 のファクトは対象外となります。残りの8個のファクトが c1 にマッチするということになります。
次に2行目(c2)。id_Noがc1のid_Noより大きく(違う != という条件でもよいのですが、同じ結果の組が2つ出てきてしまう-たとえばid=1とid=6の組、id=6とid=1の組の2つ-ので不等号の条件にしています…念のため)、名前が同じという条件にすると、c1とc2の組み合わせで、上記に示したような結果が得られるというわけ。

ですが、ルールの条件部を先入観を持たずに見てみると、単純に例題の「違う欄に同じ氏名が入力されていたら」を記述しただけ (注1)ということがわかると思います。

以上、例題を通して、ルールベースプログラミングの特徴 (の一つ) を見てきました。最初にも書いたようにBRMSの最近の適用例はあまりルールベースプログラミングの特徴が表れにくいものが多いのですが、例題としてもあげたように、ところどころルールベース的に書くべきところも散見されるので今回、とりあげてみました。

さて、これを言ってしまうと、場合によってはルールベースの敷居の高さを感じてしまう方もおられるかもしれませんが、そもそもルールベースプログラミングは、通常の手続き型のプログラミングとはパラダイムが違います。ただ、いわゆる関数型のパラダイムの(Haskellの)モナドとか、論理型で言えばPrologのカットオペレータとか、わかりにくい概念というものはなく(注2)、「if-thenルールの条件にマッチするものを実行する」という人間の素朴な直感にマッチしたパラダイムなのでそれほど身構える必要はありません。むしろ手続き型のように、「どのように」実行するかを書いていく必要がない分、ルールベースでは単刀直入に「何を行うか」を書けるのでわかりやすいとも言えましょう。もっとも、だからこそBRMSが流行っているのだとは思いますが。

(注1) 名前が空白でないという追加の条件は加えています。
(注2) 強いて言えば競合解消戦略がわかりにくいかと思いますが、最近はあまり競合解消戦略に依存するようなルールの書き方はしないので、あまり問題にはならないでしょう。