Java : 述語論理

[id:m-hiyama:20060411:1144727533]について
Javaは型付けやらなんやらで、こういう説明をするには向いていないとはいえ、これはあまりにもアレではないですか。もうちょっとJavaを賢く使ってやらないとかわいそうな気がします。
ぱっと見て、

  • static importを使う
  • メソッド名を工夫する
  • staticフィールドを使う

ことでかなり見やすくなるはずです。
では、やってみましょう。まずは、Predicateクラス。staticフィールドとstaticメソッドを必要なだけ列挙しておきます。また、Predicateだけで何をするクラスかすでに分かっているので、メソッド名はできる限り簡潔にします。ここではHaskell風に「$」を使ってみます。

package logic;

public abstract class Predicate<X> {
  public abstract boolean $(X x);
  
  public static final Predicate<Integer>
  positive = new Predicate<Integer>() {
      public boolean $(Integer x) { return x > 0;}
    };
  public static final Predicate<Integer>
  zero = new Predicate<Integer>() {
      public boolean $(Integer x) { return x == 0;}
    };
  public static final Predicate<String>
  empty = new Predicate<String>() {
      public boolean $(String x) { return x.equals("");}
    };
  
  public static<X> Predicate<X>
  and (final Predicate<X> p, final Predicate<X> q) {
    return new Predicate<X>() {
        public boolean $(X x) { return p.$(x) && q.$(x);}
      };
  }
  
  public static<X> Predicate<X>
  or (final Predicate<X> p, final Predicate<X> q) {
    return new Predicate<X>() {
        public boolean $(X x) { return p.$(x) || q.$(x);}
      };
  }
  
  public static<X> Predicate<X>
  not (final Predicate<X> p) {
    return new Predicate<X>() {
        public boolean $(X x) { return ! p.$(x);}
      };
  }
}

次にテスト実行クラス。static importでスタティックフィールドをインポートしておきます。なお、System.out.println()をprintln()メソッドに置き換えるのは、単に短く書きたいだけではなくて、出力先やデコレーションを変更したいときのことを考えてのことです。

import logic.Predicate;
import static logic.Predicate.*;

public class PredicateLogicDemo_01 {
  public static void println(String s) { System.out.println(s);}

  public static void main(String[] args) {
    Predicate<Integer> positiveOrZero = or(positive, zero);
    Predicate<String>  notEmpty       = not(empty);

    int[] intData = {1, 0, -2, 3, -1};
    String[] strData = {"Hello", "", "World"};

    for (int n : intData) {
      boolean truth = positiveOrZero.$(n);
      println("PositiveOrZero " + n + " = " + truth);
    }
    for (String s : strData) {
      boolean truth = notEmpty.$(s);
      println("NotEmpty " + s + " = " + truth);
    }
  }
}

Predicateクラスはまだまだアレですが、テスト実行クラスの方はJavaScriptと比べて全然遜色ないくらい簡潔になっているではないですか。せめてこのくらいのコードを書いてから、「述語論理はJavaScriptを使うべきだった」と言って欲しいです:p

Concurrent Clean : 再帰を繰り返しに : wget

昨日の話([id:lethevert:20060413:p1])は、以前にwgetのようなプログラムをCleanでどのように書くかという話([id:lethevert:20051126:p1]、[id:lethevert:20051124:p1])をした時の話につながります。
実際には動かないCleanプログラムの例として、次のようなものを挙げていました。

// Webはリンクでつながった再帰的な構造をしたデータとして表現される
::Web = Web WebPage [Web]
getWeb page = WebPage page (map getWeb pages)
  where
    pages = getLinkedPages page

// wgetの本体
wget firstpage = saveWeb 0 www
  where
    // Webのデータを、無限に再帰的なデータとして、先に宣言しておく
    www = getWeb firstpage

    // 宣言されたデータを使って、wgetの処理を書く。
    saveWeb MAXDEPTH _             = done
    saveWeb depth (Web page links) # printWebPage page
                                   = map (saveWeb (depth+1)) links

これは、昨日と同型の問題なので、同じような発想が適用できます。

::Web = Web WebPage [(*Socket -> *(Web, *Socket))]

getWeb :: Url *Socket -> (Web, *Socket)
getWeb url socket # (page, socket) = browse url socket
                    urls = getLinkedUrls page
                  = (Web page (map getWeb urls), socket)

という形で、先にデータの取得部分を用意して、

saveWeb :: Int Web (*Socket, *File) -> (*Socket, *File)
saveWeb MAXDEPTH (socket, file)
    = (socket, file)
saveWeb depth (Web page links) (socket, file)
    # file = printWebPage page file
      (linkedWebs, socket) = mapU links socket
    = reelU (map (saveWeb (depth+1)) linkedWebs) (socket, file)

というようにsaveWebの処理を書きます。ここでは、saveWebの部分は再帰的に書きましたが、これも同じような形に展開して、saveWebの中から処理の流れ(制御)を外側に取り出すことも可能です。
このようにすることで、副作用を排除しながら、モジュール化されたプログラムを書くことが可能です。これは、おそらく、もっと汎用的なパターンにまとめられるのではないかと思います。