Today アクセスカウンター Yesterday アクセスカウンター

はじめてのかたちぇっか

Prolog なら簡単に型チェッカが書ける,とか言ったが,すまん.ありゃ嘘だった.

まぁ,Prolog だと,プログラムを項として扱うと分解 (マッチング) が面倒だし,リストとして扱うとカンマで区切らないといけないから,どっちにしろ TAPL とかで扱っている形とかけ離れてしまうという.

そんなこんなで,いわば S 式で書く Prolog,ET 言語の出番ですな.一番使い慣れてるし.

例題プログラムは,TAPL の let-polymorphism のやつ

let double = λ f:X -> X . λ a . f(f(a)) in
let a = double succ 1 in
let b = double not true in
...

というやつ.

しっかし,パーサを書くのは面倒なので,手抜きさせてください.問題を S 式で与えます.あと,ET では,*var みたいにアスタリスクを付けると変数になるので,識別子に制限はありません (個人的には,乗算やポインタのイメージが付きまとうので,?var とかの方が良かったのではないかと思う.ちなみに ? は,他の全ての変数にマッチしないことが保障される無名変数 (ワイルドカード) として扱われます)


(test) --> (= *F (*X -> *X)), // f:X -> X のつもり.下の式にインラインで書いても良いけど
(= *double (lambda (*F *A) (*F (*F *A)))),
(= *a (*double succ 1)),
(= *b (*double not true)),
(typeInf *a),
(typeInf *b).


あと,推論規則は,テキトーに必要最小限のを,パターンマッチ使いまくりで,IF-THEN ルールをひたすら書いて行きます.


(typeInf1 () ()) -->.

(typeInf1 (*F | *rest) (succ | *rest1))
-->
(= *F (Num -> Num)),
(typeInf1 *rest *rest1).

(typeInf1 (*F | *rest) (not | *rest1))
-->
(= *F (Bool -> Bool)),
(typeInf1 *rest *rest1).

(typeInf1 (*A | *rest) (true | *rest1))
-->
(= *A Bool),
(typeInf1 *rest *rest1).

(typeInf1 (*A | *rest) (*n | *rest1)),
{(number *n)}
-->
(= *A Num),
(typeInf1 *rest *rest1).

(typeInf2 ((Num -> Num) *X) *R)
-->
(typeInf2 *X Num),
(= *R Num).

(typeInf2 ((Bool -> Bool) *X) *R)
-->
(typeInf2 *X Bool),
(= *R Bool).

(typeInf2 Bool *R)
-->
(= *R Bool).

(typeInf2 Num *R)
-->
(= *R Num).


んでまぁ,あとは


// 間違い !!
(typeInf ((lambda *parm *body) | *args))
-->
(typeInf1 *parm *args),
(print (lambda *parm *body)).


とか書きたくなるところですが,これだと *a と *b で型変数が共有されてしまうので,let-polymorphism にならない !!

# *a で *F_a が (Num -> Num) になると,*b での *F_b の型は (Bool -> Bool) だから,*F_a と *F_b が同じ *F として共有されてしまうと全体としては不整合が起きる.

というわけで,変数をコピーして,フレッシュな変数を用意しましょうという話.


(typeInf ((lambda *parm *body) | *args))
-->
// コピーしてフレッシュな変数を使う
(copy (*parm *body) (*parm1 *body1)),
(typeInf1 *parm1 *args),
(print (lambda *parm1 *body1)),
(typeInf2 *body1 *result),
(print (*parm1 => *result)).


ちなみに,定義されてない述語,number/1 と print/1 と =/2 (unification) と copy/2 は組み込みルールです.

実行してみると,こんな感じ.

[D]>load "C:\\Documents and Settings\\aloha\\デスクトップ\\typeInf.eti"

[D]>(test)
-------------------------D execution ---------------------
(lambda ((Num -> Num) Num) ((Num -> Num) ((Num -> Num) Num)))
(((Num -> Num) Num) => Num)
(lambda ((Bool -> Bool) Bool) ((Bool -> Bool) ((Bool -> Bool) Bool)))
(((Bool -> Bool) Bool) => Bool)
----------------------------------------------------------
succeeded.
(test)

まぁ,S 式とパターンマッチによるルール記述とユニフィケーションと論理変数があると,定理証明器とか型チェッカはすごく簡単に (いや,もちろん,複雑な型体系になったら,それなりに大変だけど,他の言語とかに比べたら相対的に) 書けますよと.

パターンマッチは劇的にプログラムを宣言的にします.
あと,型推論の本質は記号によって組み立てられた構造のユニフィケーションなので.

今回使ったのは,ET の D (eterministic) ルールというサブセットのみで,他にもまぁいろいろ機能はあります.面倒だから説明しないけど(駄目)

たとえば論理変数だけだと表現力に乏しいので,情報 (制約) 付き変数とか.

D ルールのみだと,基本的に Erlang とほとんど同じだと思います.構文とかもかなり似ているような.ただ,値への簡約ではなく,論理変数経由で計算結果を伝えていく点は違いますけど.

コメント

Secret

細かいことですが,前から気になっていたのでコメントします.

> たとえば論理変数だけだと表現力に乏しいので,情報 (制約) 付き変数とか.

で,「情報 (制約) 付き変数」

というのは論理プログラミングの世界では,
「Attributed Variable」
参考:
http://gollem.science.uva.nl/SWI-Prolog/Manual/extvar.html

と呼ばれているものと(たぶん)同じものだと思うので,
できれば同じように属性付き変数呼ぶほうがいいと思います.

この機能の目的は,論理変数に=による単一化(単一代入値)
以外の値を変数に関連付けるということでした.

この属性とコルーチン制御(freezeとか)を使って述語の遅延実行と,
エルブラン項以外のユニフィケーション(つまりは制約処理)を実装しているわけです.
制約論理言語やCHRなどの実装の多くはこの機能でProlog上に実装しているのでした.

私もこの辺りの実装は良くわかっていませんが,
http://www.cs.kuleuven.ac.be/~toms/Research/CHR/
辺りを参照.

しかし,この方法は問題点も多いと思います.

基本が逐次処理の中で,並行処理を実装する

のではなく,

基本が並行処理の中で,部分的に逐次処理を指定したり,自動逐次最適化ができる

方が,理想的だと思うのですが.
しかし,現実世界のプログラムは逐次処理を考える場面の方が多いし,
また,現在のアーキテクチャで細かい制御をしようとする場合や,
命令・関数プログラムなどに慣れていることを考えると逆に分かり辛いということもあったりして,
現実的ではないのかなとも思います.

う〜ん,CHR だったか制約論理プログラミングの方では,制約付き変数と呼んでいたような気がします (ソースは失念してしまいましたが,おそらく 「制約論理プログラミング」

http://www.amazon.co.jp/dp/4320024699

http://alohakun.blog7.fc2.com/blog-date-20051010.html

freeze と melt を使用した遅延実行 (データ駆動計算) の実装なども載っていたはずです.僕も昔,卒論書いてる時に ET で 「情報付き変数」 を使って実装しました).

http://alohakun.blog7.fc2.com/blog-entry-128.html

それが望ましいかどうかはともかく,関数型言語の方でも,ひとつの概念を (微妙なニュアンスや文脈,学派閥の違いなどを込める場合も) 複数の呼びかたで呼ぶ場合がありますよね ? 例えば,非正格評価/遅延評価/怠惰評価/必要呼び/正規順序簡約/最左最外簡約などなど.

特にこの場合は,まだそれほど標準的な用語があるとも思えませんし,論理プログラミングの用語に無理に合わせる必要は無いと,個人的には思います.

また,「ET の理論体系内の用語」 という側面を強調するために,わざと既存の用語とは異なる用語を用いている,という側面もあると思います (そもそも,私が名付けたわけでは無いので (^-^;) ET の理論は,論理プログラミング (これもまたバズワードですが) の限界を越えるために,一から再構築された体系なので.

後半の方の言及は,私も同感です.現在のハードウェアにせよプログラミング言語にせよ,あまりにも強く逐次実行を意識し過ぎていると感じます.この流れが今後も続く限りは,並列計算の未来は暗いでしょう.

しかし,ほんの 10 年前 20 年前を振り返ってみると,ハードウェアは驚く程激変してきました.それを考えると,今後も現在のようなハードウェアの流れが永遠に続くとは限らないとも思います.

個人的には,逐次処理は並列処理の特殊な場合に過ぎず,デフォルトは並列分散処理,という形態の方が自然で美しいと思います.単に現在のパラダイムが古い習慣を引きずっているだけで.

> 論理プログラミングの用語に無理に合わせる必要は無いと,個人的には思います.

たしかに,用語の使い方というのはあいまいで,
合わせようとしたらきりがないので仕方がないですね.

まず,「情報付き」と呼ぶのは全く問題ないと思います.
問題なのは「制約付き」と呼ぶことです.

たしかに制約という用語もとてもあいまいだと思います.
他にもUML,SQLとか,経営学分野(TOC:制約理論)とかでも,
「制約」という用語がいたるところに出てきます.

なので,以下は,制約プログラミングにおいてという話で,
Attributeを制約と呼ぶことがまずい理由を書いておきます.

まず,参考文献に挙げていた本ですが,
これは結構古いもので,制約プログラミングの概念
が十分に定式化される以前の内容だと思います.

制約プログラミングにおける「制約」はAttributeのように

「一つの変数」に対してつく(例: in(X, [2,4]) 変数Xを2から4までの範囲に制約)

のではなく,

「複数の変数」に対してつく(例: all_different(X, Y, Z) X,Y,Zは互いに異なる)

ケースがあるので,制約表現としての幅が狭いのです.

参考:
Artificial Intelligence(a Modern Aproach の制約充足問題の定義の章(サンプルですがここだけは読めます):
http://aima.cs.berkeley.edu/newchap05.pdf
このサイトの資料も参考になる:
http://ktiml.mff.cuni.cz/~bartak/constraints/consistent.html

ここの定義では,一つの変数に対する制約の解消をNode-Consistency,
2つの変数に対する制約の解消をArc-Consistency,
そこからまだ広げていくとPath-Consistency, Hyper-arc Consistency
などとなり,変数への制約の仕方で分類していっているようです.


もちろん,Attributeに変数を含んだデータを入れれば,
複数の変数への制約もエンコードできますし,
実装レベルなのだとして区別すれば問題ないのかもしれないのですが.

しかし,これでは,2項関係制約を超えると定義は辛いと思います.
そのために,CHRのような制約処理系用の言語が提案されているといえます.


以下話題は変わって.並行が基本にならない理由の追加.

Prolog(KL1やETも)や,π計算の式などを見ても分かるように,
静的スコープとかで実行状態遷移の記述がネストに書かれるのではなく,
すべてフラットに記述されていることがあるかもしれません.

なので,コード記述が構造化されない,情報の流れが把握しづらい,
読みづらい,保守性が低い,流行らないということです.

もちろん,実行状態が木構造より複雑な場合はあまり問題にならないのでしょうが.
しかし,アルゴリズム記述は木構造だし,
プログラムテキスト自体も木構造的に見える方が望ましいわけです.

いや,これは実は単なるシンタクスシュガーの問題なのかも知れませんが.
Oz/Mozart,Mercuryとかは色々と関数プログラムに近い
記述ができるようになっているようでした.
あと,関数型言語のコンパイルでもクロージャ変換とかで
最終的にはフラットで環境渡しなコードになっているようですし.

しかし,宣言性・並行性は,静的スコープ(とくに,プラス副作用時)
とかと非常に相性が悪い気がします.
この辺りの解析技術については知識がまったくなので
詳しくはなんともいえないのですが.
コードが環境依存するとモジュラー化(ルール化)できないような.
たぶんモナドの方法はこの辺りの解決に関わる.

なるほど,安易に 「いきなり ET の独自用語を出すよりはわかりやすくなるかな〜」 と思って,あえて (制約付き) とか書いていたのですが,かえってその方面に明るい方にとっては混乱の元になる,ということですね.参考になりました.コメントありがとうございます.

後半の,記述がフラット過ぎる,というのは,まさしく私も問題だと感じていた部分でした (明確に言語化して認識していたわけではありませんが…)

Prolog の宣言性に関わる問題 (情報の流れが把握しにくいなど) は,まさしくその通りだと思います.

そのため ET パラダイムでは,宣言的記述から並列性などを抽出して,明確に手続き的なルールを生成する,というアプローチを取っているのだと思います.

# 具体的な技術よりも,この体系的なアプローチこそが,ET パラダイムの価値,だと思います.

つまり,ET や KL1 のルールは,ある種のアセンブリ言語なんですよね.だから,フラットで高速に機械で処理できる (可能性がある) 半面,人間にとっては読みづらいし,保守しにくいと思います.

# 具体的な解析技術などは,私はほとんど知識がありませんし,(うちの研究室でも) それ以外にやることが山積みで忙殺され,あまり研究が進んでない感じですが.なんせ地味で人気が無い研究室なので.みんなカツカツです.

モナドが,そのような解析に対してフレームワークを与える,というならば,非常に興味深いところですが…

私の偏見に過ぎませんが,今の計算機科学における圏論の導入は,まだ荒削り過ぎるというか… 確かに便利なのでしょうが,オーバーキル過ぎるというか.一部の専門家しか扱えないような気がします (関数畑の人達には常識なのかもしれませんが).

いや,単に私の勉強不足のせい,と言われれば,何も反論はできませんが.もう少しシロートにも扱いやすいものになって欲しいなぁと.そして,そうならない限り,一般には広まらないし基本にもならないと思います.

すみません.解析技術云々のあとにモナドという事柄を出したのは,
混乱してしまう書き方でした.いや,私もよく分かってないのですが.

以下,不正確なので読み流してください.

たぶんモナドというのは「解析技術」からは遠くて,
型定義の延長の「関数データの抽象化技術」だと思います.

プログラムの機能的には,
----
1. データへの型タグづけ
2. データを入力する(出力する)関数の制限

3. 関数ネストのシンタクスシュガー
例:
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m >> k = m >>= \_ -> k
なので,

p1 >> p2 >> p3

let x = p1
let y = p2 x
let z = p3 y
z

----
を型クラス(cf. テンプレート,インターフェイス定義)
で定義することで,

----
1. 逐次操作指定( a;b;c ⇔ a >> b >> c)

2. データ参照の一意性保証
例:(コードは適当なScheme風)

(let ((fs openFile("foo"))
(begin
(f fs)
(g fs)))

IOデータは'>>'でつないで使わねばならないので,
上みたいなコードはfsを2回使用していて不可.
次のように使うことを強制している

(let ((fs openFile("foo"))
(let ((fs1 (f fs))) ; f 前状態fs -> 次状態fs1
(g fs1)))

----

を多相型システムの枠内で実現できるようにすることだと思います.

----
それで,具体的にコードから"解析する"ための技術としては,
線形型(一意性)システムの方がまだ近いと思います.

私も読めていませんが,下の論文はMercuryに対して
線形性まで含めてモード/型解析しているようです.

Precise and Expressive Mode Systems for Typed Logic Programming Languages (2003):
http://citeseer.ist.psu.edu/overton03precise.html

いずれにしろ,使う側にとっては
単一代入変数の更新を連ねてデータを扱うということは変わりません
実際どちらもKL1のストリームによるものと同じようなプログラム記述になると思います.
変数でつなぐか,関数でつなぐかの違いはあれ,
フラットな状態渡しプログラムに強制しているのでした.

そうやって記述させないと非決定的になるのでたぶんこれは仕方がないです.
如何にして非決定性を防ぐ(抽象化して扱う)かということを
考えるのではなく,非決定もありきで実行状態グラフ(オートマトン)
を追って解析・検証しようというアプローチのモデル検査技術もあるみたいですが…….
プロフィール
  • Author:あろは (alohakun)
  • 京都のデバッガベンダーに勤めるアラサー会社員。

    本ブログの内容は,あくまでも個人的な感想や意見であり,会社の意見を代表するものでは一切ありません.

    連絡先 : alohakun ___at___ gmail.com
    mixi : http://mixi.jp/show_friend.pl?id=182927
    twitter : http://twitter.com/alohakun













    あわせて読みたい


    この日記のはてなブックマーク数


    スカウター : ホワット・ア・ワンダフル・ワールド


    Map
FC2カウンター
ブロとも申請フォーム

この人とブロともになる

最近のコメント
リンク
最近のトラックバック
人生の残り日数
日本人男性の平均寿命は 28700日.
RSSフィード
カテゴリー
  1. RSSリーダー