読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

PrologでTodo・日付の比較と加減算・シンタックス考察

Prolog

ややロジックが面倒な事務処理を書くことになりました。静的型付きな関数型言語などを検討したのですが、どうも問題の性質からPrologが最適な気がします。しかしPrologの経験がほとんど無いので、検討も兼ねてしばらく練習をしたいと思います。

図書館でいくつかPrologの本に目を通しましたが、どれもユニフィケーション・バックトラック・リスト・カット・パズル・終わり!という雰囲気で、いまいち事務処理への応用が掴めません。そういった方面で何かいいドキュメントがあったら教えてください。

Todo

プロジェクト階層を持つTodoを表現してみます。Todoアイテムはtodo/doneの状態を持ち、プロジェクトはTodoアイテム又は他のプロジェクトを入れ子として持ちます。例えば以下のようになります。

  • 掃除(プロジェクト)
    • 水回り(プロジェクト)
      • 洗面所
      • トイレ
    • キッチン
    • リビング(プロジェクト)
      • 机の下
      • 棚の裏

todo/doneの状態は、そのまま事実として定義することにします。例えば机の下を掃除し終わった(done)なら

done(机の下).

と記述します。Todo/プロジェクト名はそのままシンボルです。

プロジェクトは名前のシンボルと、それを達成するために必要なTodoアイテム又はプロジェクトをシンボルのリストとして持たせることにします。上記の例は次のように記述します。

project(掃除, [水回り, キッチン, リビング]).
project(水回り, [洗面所, トイレ]).
project(リビング, [机の下, 棚の裏]).

さて、知りたいのは「何をやればいいか」です。プロジェクトは粒度が大きいのでやることリストには入れたくありません。

  • プロジェクトのやることリストに含まれている
  • それ自体はプロジェクトではない
  • doneではない

ものは、todoであると定義すれば良さそうです。そのまま書き下します。

todo(Todo) :-
    project(_, TodoList), member(Todo, TodoList),
    not(project(Todo, _)),
    not(done(Todo)).

これを使って問い合わせてみます。

?- todo(X).
X = キッチン ;
X = 洗面所 ;
X = トイレ ;
X = 棚の裏.

?- done(X).
X = 机の下.

うまくいきました。プロジェクト(掃除, 水回り, リビング)は含まれていません。それらを達成するために必要なことが列挙されています。

次はプロジェクトの終了を判断します。必要なやることが全て終わったら、プロジェクトも自動的に終了です。この場合は、「プロジェクトのやることリストに書かれてるものが全てdoneなら、プロジェクトもdoneである」と再帰的に定義します。これもそのまま書き下します。

done(Project) :-
    project(Project, TodoList), maplist(done, TodoList).

done(棚の裏)も追加し、リビングの掃除が終わったと判定されるか見てみます。

?- done(X).
X = リビング ;
X = 机の下 ;
X = 棚の裏.

残りのTodoアイテム(キッチン、洗面所、トイレ)もdoneにして、完了したプロジェクトを見てみます。

?- done(X), project(X, _).
X = 掃除 ;
X = 水回り ;
X = リビング .

うまくいきました。

todo/doneの切替時には、assert/retractとプロジェクトか否かの判定を合成すれば良さそうです。他のパラダイムで書いても難しい処理ではありませんが、条件をそのまま書き下せば良いときの楽しさは別格に感じます。

以下は最後の問い合わせ時点でのコードです。

todo(Todo) :-
    project(_, TodoList), member(Todo, TodoList),
    not(project(Todo, _)),
    not(done(Todo)).

done(Project) :-
    project(Project, TodoList), maplist(done, TodoList).

project(掃除, [水回り, キッチン, リビング]).
project(水回り, [洗面所, トイレ]).
project(リビング, [机の下, 棚の裏]).

done(キッチン).
done(机の下).
done(棚の裏).
done(洗面所).
done(トイレ).

日付の比較と加減算

日付の表現をどうしていいかわからず、SWI-Prolog組み込み?のデータ型もやや面倒そうなので意気消沈していましたが、かなりナイスな定義を見つけました。少しアレンジして書きますが

%% get
year( date(Y, _, _), Y).
month(date(_, M, _), M).
day(  date(_, _, D), D).

%% set
year( date(_, M, D), Y, date(Y, M, D)).
month(date(Y, _, D), M, date(Y, M, D)).
day(  date(Y, M, _), D, date(Y, M, D)).

このように定義すると、うまい具合にdate(Y, M, D)を使えます。

?- year(date(2016, 07, 21), Y).
Y = 2016.

?- month(date(2016, 08, 21), 11, D).
D = date(2016, 11, 21).

?- year(D, 2016), month(D, 07), day(D, 21).
D = date(2016, 7, 21).

定義で式を入れ子にできることと、その式自体をマッチできることをどう利用すればいいのかやっとわかりました。

比較をdate(...) > date(...)のように記述したかったのですが、SWI-Prolog組み込みのオペレータはdynamic/1できないようなので断念しました。代わりにeq, gt, leなどのシンボルを使うことにします。

%% compare
comp(date(Y, M, D), date(Y, M, D), eq).

comp(date(Y1, _, _), date(Y2, _, _), gt) :- Y1 > Y2.
comp(date(Y, M1, _), date(Y, M2, _), gt) :- M1 > M2.
comp(date(Y, M, D1), date(Y, M, D2), gt) :- D1 > D2.

comp(D1, D2, ge) :- comp(D1, D2, gt).
comp(D1, D2, ge) :- comp(D1, D2, eq).

comp(D1, D2, lt) :- not(comp(D1, D2, ge)).
comp(D1, D2, le) :- comp(D1, D2, lt).
comp(D1, D2, le) :- comp(D1, D2, eq).

次のように比較を試したり、関係を調べたりできます。

?- comp(date(2016, 07, 21), date(2016, 07, 21), eq).
true .

?- comp(date(2016, 07, 21), date(2016, 08, 01), X).
X = lt ;
X = le .

比較関係を取り回せるので案外使い道があるかもしれません。

最後に日の加減算を書いたので載せておきます。先に使用例です。

?- add(date(2016, 12, 28), 3, D).
D = date(2016, 12, 31).

?- add(date(2016, 12, 28), 4, D).
D = date(2017, 1, 1).

?- add(date(2016, 01, 10), -10, D).
D = date(2015, 12, 31).

?- add(date(2016, 01, 10), -9, D).
D = date(2016, 1, 1).

?- add(date(2016, 01, 10), -380, D).
D = date(2014, 12, 26).

実装です。もっと上手い書き方がありそうです……。(numberは後々日付の差を出すために入れています)

%% 月の日数
last(_, 01, 31).

last(Y, 02, 29) :- 0 is Y mod 400, !.
last(Y, 02, 28) :- 0 is Y mod 100, !.
last(Y, 02, 29) :- 0 is Y mod 4, !.
last(_, 02, 28).

last(_, 03, 31).
last(_, 04, 30).
last(_, 05, 31).
last(_, 06, 30).
last(_, 07, 31).
last(_, 08, 31).
last(_, 09, 30).
last(_, 10, 31).
last(_, 11, 30).
last(_, 12, 31).

nextMonth(Y1, 12, Y2, 01) :- Y2 is Y1 + 1.
nextMonth(Y,  M1, Y,  M2) :- M2 is M1 + 1.
prevMonth(Y1, 01, Y2, 12) :- Y2 is Y1 - 1.
prevMonth(Y,  M1, Y,  M2) :- M2 is M1 - 1.

%% 別の月に行く
add(date(Y1, M1, D1), N, Date2) :-
    number(N), N >= 0,
    N1 is D1 + N,

    last(Y1, M1, Last),
    N1 > Last,
    Diff is N1 - Last,

    nextMonth(Y1, M1, Yn, Mn),

    add(date(Yn, Mn, 0), Diff, Date2),
    !.

add(date(Y1, M1, D1), N, Date2) :-
    number(N), N < 0,
    N1 is D1 + N,

    N1 < 1,

    prevMonth(Y1, M1, Yn, Mn),
    last(Yn, Mn, Last),
    add(date(Yn, Mn, Last), N1, Date2),
    !.

%% 月の中
add(date(Y, M, D1), N, date(Y, M, D2)) :-
    number(N),
    D2 is D1 + N,
    !.

シンタックス考察

新しくプログラミング言語をしっかり触ろうとすると、毎回何故かシンタックスを変えたくなります。そしてもう少し後から振り返ると、何も知らなかったと反省するはめになります。LispでもForthでもSmalltalkでもそうでした。しかし、中には二転三転した後ずっと暖め続けられるアイディアもあるので、一応書いておきます。全く同じものがあったりしたら教えてください。

Prologの論理式ですが、括弧とカンマは無くても認識できそうに思えます。foo(a, b)ならばfoo a bに、入れ子になってはじめて括弧を使ってもいいのではないでしょうか。例えば上の比較を次のように書きます。

%% 元の例
comp(date(Y1, _, _), date(Y2, _, _), gt) :- Y1 > Y2.
comp(date(Y, M1, _), date(Y, M2, _), gt) :- M1 > M2.
comp(date(Y, M, D1), date(Y, M, D2), gt) :- D1 > D2.
%% 括弧とカンマを取り払った例
comp (date Y1  _  _) (date Y2 _   _) gt :- Y1 > Y2.
comp (date Y  M1  _) (date Y  M2  _) gt :- M1 > M2.
comp (date Y  M  D1) (date Y  M  D2) gt :- D1 > D2.

更に、定義中に現れるのは殆ど変数です。引数はデフォルトで変数とし、シンボルは'aとクォートで、述語を渡す際は同じものか#predのような表記でもいいのでは無いでしょうか。次のように書けると思います。

%% 元の例
done(Project) :-
    project(Project, TodoList), maplist(done, TodoList).

project(掃除, [水回り, キッチン, リビング]).
project(水回り, [洗面所, トイレ]).
project(リビング, [机の下, 棚の裏]).

add(date(Y1, M1, D1), N, Date2) :-
    number(N), N >= 0,
    N1 is D1 + N,

    last(Y1, M1, Last),
    N1 > Last,
    Diff is N1 - Last,

    nextMonth(Y1, M1, Yn, Mn),

    add(date(Yn, Mn, 0), Diff, Date2),
    !.
%% 括弧とカンマを取り払い、デフォルトで変数とした例
done proj :-
    project proj todolist, maplist #done todolist.

project '掃除     ['水回り 'キッチン 'リビング].
project '水回り   ['洗面所 'トイレ].
project 'リビング ['机の下 '棚の裏].

add (date y1 m1 d1) n date2 :-
    number n, n >= 0,
    n1 is d1 + n,

    last y1 m1 l,
    n1 > l,
    diff is n1 - l,

    nextMonth y1 m1 yn mn,

    add (date yn mn 0) diff date2,
    !.

オペレータ表にアクセスできるIDEならばシンタックスハイライトも可能だと思います。何よりシフトを抑える左手小指への負担が減ります。Prologをしっかり使うことになったら、ちょっと真剣に開発環境の作成を検討したいところです。

広告を非表示にする