プログラムの停止性とRanking関数
大学・大学院時代の研究テーマは、「プログラムの停止性の自動検証」 ー つまり任意の入力に対しプログラムが停止するかを自動で検証する手法についてだったのですが、プログラムの停止性の特徴付けとして有名なものにRanking関数(Ranking function)があります。
Ranking関数とはつまり、関係が定義された構造(Σ, →)から何らかのwell-ordered structure(W, >)への準同型 f のことで、つまり
a,b∈Σ, a→b ⇒ f(a) > f(b)
が成り立つような写像fのことを言います。大体はwell-ordered setとして(N, >)が使われます。
プログラムの停止性とRanking関数の関連について、詳しくはByron Cookの資料(サマースクールのテキスト?)が簡潔にまとまっていていい感じです。適当にググれば出てきます。
http://research.microsoft.com/en-us/um/cambridge/projects/terminator/principles.pdf
関連部分だけ軽く説明すると、プログラムを
- S: プログラムの状態
- R: プログラムの遷移関係
- I: 初期状態
の3つ組(S,R,I)で表したとき、プログラムの停止性は「IからRで到達可能な状態(R*(I)と書く)とRからなる構造(R*(I), R)がwell-founded structureである」ことと定義されます。
さて、(R*(I),R)にRanking関数があれば、(S,R,I)が停止することが言えます。なので、停止性検証の主流な手法の一つとして、そのプログラムに対するRanking関数を見つけてやる、という方法があります。
で、前々からちょくちょく考えてたのが「逆は成り立つのか?」ということで、つまりプログラムが停止するとき、そのプログラムに対するRanking関数は必ず存在するか?ということです。well-foundedだがwell-orderedでない集合なんていくらでもある訳で(例えば偶奇が異なる数同士は比較できないように制限した自然数の集合とか)実際どうなんだろうなぁと思っていたのですが、どうやら選択公理を仮定すればwell-founded relationからwell-orderedへの拡張は可能なようで:
[1503.06514] On the Well Extension of Partial Well Orderings
ということはつまりプログラムが停止するならばRを拡張して適当なwell orderingにできるということで、停止するプログラムには必ずRanking関数が存在する、ということになります。
あと、一般のwell-founded relationからwell-orderingへの拡張には整列可能定理が要りますが、 要はプログラムの状態の集合に適当な整列順序を一つ定義できればいいので、状態集合が加算なら選択公理も必要ないような気がします。
と、いうようなことを悶々と考えていたりしました。なお未だに以下の点が疑問です。
- このことに明確に言及した文献はないのか。(絶対にあると思う)
- computableな(つまりプログラムとして書き下せる)Ranking関数が見つかるための条件は何か。
誰か何か知ってたら教えて下さい。
Haskellのレコード更新構文を詳しく
スタック・オーバーフローにて
haskell - Text.Parser.Token.StyleのemptyIdentsの使い方について - スタック・オーバーフロー
といった質問をしたところ、このような回答を頂き、レコード更新構文についての詳細を知る必要を感じたので私的まとめ。
といってもほとんどHaskell Language Reportを見ていろいろ実験しただけなんですが。
レコード更新構文の解釈
3.15.3 Updates Using Field Labelsより。
をコンストラクタ、を束縛の列、を値として、を次のように定義します:
「コンストラクタの番目のフィールドの名前がであり、かつ束縛の列にに関する束縛が含まれるならばである。そうでないならばである。」
データ型Tを
data T = { :: ... :: }
| ...
| { :: ... :: }
とすると、T型の式に対するレコード更新構文 {}は
{} = case of
... -> ...
...
... -> ...
に変換されます。
多相的な値と組合せるときの注意点
変換された式を見ると、要するにただのパターンマッチになっていることがわかります。
マッチされる式 e の型が多相的で、かつユーザが型シグネチャを供給していないならば e の型は精密化されません。
となると、例えば e が多相的な型を持ち、かつ e {bs} に単相的な型をユーザが与えていた場合、 e の値によって e {bs} の値が一意に決まらなかった場合などは困るわけで、このような場合曖昧性エラーが生じます。
具体的には以下のようなコードで曖昧性エラーが発生します。
data T a = T {element :: a, typeStr :: String} class F a where mes :: a -> String instance F Int where mes _ = "Int" instance F Double where mes _ = "Double" mkDefaultT :: F a => a -> T a mkDefaultT x = T x (mes x) defaultT :: (F a, Num a) => T a defaultT = mkDefaultT 0 -- error! modifiedT :: T Double modifiedT = defaultT {element = 0.0}
defaultT は T Int 型のとき typeStr = "Int" で、 T Double 型のとき typeStr = "Double" です。なので、上の modifiedT の定義では defaultT {element = 0.0} の typeStr フィールドが "Int" なのか "Double" なのかが決定できす、曖昧性エラーとなります。
GHCはどこまで賢くやってくれるのか、という問題
上のような事情があるにはあるのですが、例えば以下のようなコードはコンパイルを通ります。
data T a = T {element :: String, typeStr :: String} defaultT :: T a defaultT = T "" "a" -- error! modifiedT :: T Double modifiedT = defaultT {element = ""}
このケースでは defaultT にはいかなる制約もかかっていない forall a . T a という型がついており、このような場合は a がどんな型に具体化されようと e の値は変わらず T "" "a" です。
なので、 defaultT の型が曖昧でも modifiedT の定義は上のままでうまくいきます。
では、「型変数がどの型に具体化されても値が同じ」場合につねにうまくいくかというとそういうわけにもいかず、以下のコードはエラーになります。
data T a = T {element :: String, typeStr :: String} class F a where mes :: a -> String instance F Int where mes _ = "Int" instance F Double where mes _ = "Double" defaultT :: F a => T a defaultT = T "" "a" -- error! modifiedT :: T Double modifiedT = defaultT {element = ""}
単に defaultT の型に制約を増やしただけですが、これはエラーになります。
これはおそらく「型変数がどの型に具体化されても値が同じ」かどうかは型によってのみ判断されるからです。
forall a . F a => T a の型の値は「型変数 a が何に具体化されても同じ値」とは限らず、例えば
polyT :: F a => T a polyT = mkPoly undefined where mkPoly :: F a => a -> T a mkPoly x = T "" (mes x)
のような値がありえます。
同じフィールド名に対するレコード更新構文
知ってた人には「今更」って感じなんでしょうが、
data User = Registered {uid :: Int, name :: String} | Guest {name :: String} deriving (Show) updateName :: User -> String -> User updateName u n = u {name = n} -- 動作: -- *Main> updateName (Registered {uid=0, name="Bob"}) "Alice" -- Registered {uid = 0, name = "Alice"} -- *Main> updateName (Guest {name="Alice"}) "Bob" -- Guest {name = "Bob"}
みたいなの書けたんですね。知りませんでした。*1
*1:というかそもそも同じデータ型の定義中でならフィールド名被っててもいいのだということを初めて知りました。
Egisonで国士無双の判定(及び「速い」Egisonコードの書き方について少し)
RubyKaigiの実況TL (Egison at RubyKaigi 2014 - Togetterまとめ)にて、
Egisonで麻雀の役判定する話になったとき「国士無双は?」みたいな声を2、3見掛けたので、今更書いてみる。
;; tileの定義は公式のサンプルコードと同じ。 ;; 么九牌の判定 (define $yaochu (pattern-function [] (| <hnr _> <num _ ,1> <num _ ,9> ))) ;; 全13種の么九牌のリスト (define $all-yaochu-list {<Hnr <Haku>> <Hnr <Hatsu>> <Hnr <Chun>> <Hnr <Ton>> <Hnr <Nan>> <Hnr <Sha>> <Hnr <Pe>> <Num <Wan> 1> <Num <Wan> 9> <Num <Pin> 1> <Num <Pin> 9> <Num <Sou> 1> <Num <Sou> 9>}) ;; 国士無双の判定 (define $kokushi-musou (pattern-function [] <cons (yaochu) ,all-yaochu-list>))
つまり、「アタマの么九牌」+「13種類の么九牌」で正しく判定できる。
できるんですが、実は速度的に若干の問題を抱えているというか、このコードだと
「アタマを選んでから残りがall-yaochu-listとマッチするかどうかを判定する」という挙動になるので、
例えば手牌全部么九牌だけど国士無双じゃないみたいなコードは一々全部の牌をアタマにしたケースを探索してしまいます。
なので、こう書いたほうが速いはず。
(define $thirteen-orphans-fast (pattern-function [] <join ,all-yaochu-list <cons (yaochu) <nil>>>))
multisetにはsnocがないのでjoinを使って、先に「13種類全ての么九牌がある」かどうかを判定して、残った一牌がアタマとして正しいかどうかを判定するパターンにしています。こうすれば、13種類の么九牌がなかった時点、或いはアタマが不適切(つまり么九牌じゃない)だった場合に即座に失敗してくれます。
しかしなんというか、こんな風にマッチャーの挙動が計算量に割とクリティカルに作用*1してくるケースが生じてくるのは、Egisonプログラミングの難点の一つなのかなぁと思いました。要は、Egisonの処理系の速い遅いの問題はともかく、コーディングする側が「速度」を意識してパターンマッチを書くのが結構難しいんですよね。麻雀の役判定ぐらい複雑なパターンマッチになると、その辺はどうしても無視できなくはるはずです。
たとえば、サンプルコードで七対子の判定パターンがありますが、
あれもたとえば手牌に3対子(aa, bb, cc)しかない状態で判定を走らせると、
- th_1 = aa, th_2 = bb, th_3 = cc
- th_1 = aa, th_2 = cc, th_3 = bb
- th_1 = bb, th_2 = cc, th_3 = aa
- th_1 = bb, th_2 = aa, th_3 = cc
- th_1 = cc, th_2 = aa, th_3 = bb
- th_1 = cc, th_2 = bb, th_3 = aa
の6通りの失敗パターンを通るわけですが、一つ目失敗した時点でもう全体のマッチ失敗が確定するはずなんで、探索を切っていいはずですよね。
そういえば、昔のEgison*2にはcut patternってのがあって、
(twin !$th_1 (twin !$th_2 ...
みたいな感じで、th_1, th_2にマッチしたら、その一つ目の結果でダメなら探索を切るということができました。
要はつまりPrologのカット節と同じ感じで「バックトラックを行わない」ことを指定できたんですが、もう復活はしないのかな。
Haskellの多相関数と自然変換(というか射の族)について
(Kan拡張のとこ以外)読み終えた。随伴がモナドを伴う、というのは知ってたけど具体的にHaskellコードに落とせるというのは面白い。 / Haskellと随伴 by @myuon_myon on @Qiita http://t.co/AiN2UsRJVX
— xenophobia (@xenophobia__) 2014, 12月 5
そんなことより、多相関数を与えることとHask -> Haskな関手間の自然変換を与えることとがほぼ対応する、という事実に気付かされたので意義深かった。 > アドベントカレンダー4日目記事
— xenophobia (@xenophobia__) 2014, 12月 5
これなんですが、以前も聞いた気がするし*1、忘れないように備忘録をば。
たとえば、HaskellでInt -> Double型の関数を1つ与えるというのは、Hask圏において対象Intから対象Doubleへの射を1つ与えることに対応していると思ってよい。
そういう視点で今度はforall a . a -> [a]型の多相関数を考えてみる。
aにある具体的な型、つまりHaskの対象Xを代入すると一つの具体化された型 X -> [X]が決まり、それに対応してHask圏上の(Xから[X]への)射が1つ与えられることになる。
つまり、forall a . a -> [a]型の多相関数は、Hask圏上の射の族と対応していると考えることができるはず。
射の族が表せるなら、自然変換も表せるという道理になる。*2
つまり、
nat :: (Functor f, Functor g) => f a -> g a nat = ...
という多相関数は、関手から関手への自然変換と対応していると考えることができるし、
nat_l :: Functor f => f a -> a nat_l = ... nat_r :: Functor g => a -> g a nat_r = ...
はそれぞれ自然変換及び自然変換と対応していると考えることができる。
そういう視点でMonadのreturn, joinの型を見ると、
class Monad m where return :: a -> m a join :: m (m a) -> m a
これらはそのまま圏論のモナドの2つの自然変換の成分とに対応しているというのがはっきりわかる。
今までHaskellでのMonadの定義と圏論におけるモナドの定義はなんとなく一致してるなーぐらいの感覚で見ていたんですが、今回この多相関数と自然変換との関係を再確認することでより理解が深まった感じがします。よかったよかった。
ところで、Functorのfmap :: (a -> b) -> f a -> f bも多相関数ですが、あれは関手の射関数を表しています。
したがって、fmapはHask圏の射の族ではなく、射を射に写す関数の族を表していると言うべきでしょう。
Haskellの(->)はHask圏の射であり、かつ関数でもある*3ので、どちらと取ればいいかには常に気をつけていく必要がありそうですね。
*1:http://d.hatena.ne.jp/hiratara/?of=15 を見るにつけおそらく圏論勉強会で既に学んでいるはず。
*2:自然性をコードで表現できると言っている訳ではない。
*3:あと、A -> B(A,Bは具体的な型)はそれ自体が型なのでHask圏の対象でもありうる。
【随時追記予定】読書メモ:関数プログラミング 珠玉のアルゴリズムデザイン
「関数プログラミング 珠玉のアルゴリズムデザイン」を買いました。
「関数プログラミング 珠玉のアルゴリズムデザイン」買った。二章まで読んだけど読み応えあってよい感じ。 pic.twitter.com/tGSm0ceW0E
— xenophobia (@xenophobia__) 2014, 11月 14
読みながらツイートしてたら
珠玉のアルゴリズムデザイン、おれが買う頃には @xenophobia__ さんがわかりにくいと思ったところを記事にまとめてくれてて、それを見ればスムーズに読み進められるんだろうなー!
— とむらっきー (@masquerade0324) 2014, 11月 14
というフリを受けたので、理解を深めるためにもメモを書くことにします。
随時更新していきますが、とりあえず3章まで。
(11/19追記)サポートサイトがあるようです。なんとこの記事にリンクを貼っていただきました。ありがとうございます。
関数プログラミング 珠玉のアルゴリズムデザイン
【更新履歴】
2014/11/19: 6章まで更新。あと見た目を多少改善
2015/5/4: 7章を更新。
1章「最小自由数」:
最初の章だけあって平和だった。
強いていえばaccumArray関数の説明がわかりにくかったことぐらい。
なんかこんな感じに挙動の例があれば一瞬で理解できたと思う:
accumArray (+) 0 (1,5) [] = array (1,5) [(1,0),(2,0),(3,0),(4,0),(5,0)] accumArray (+) 0 (1,5) [(1, 3), (5, 6), (1, 2)] = array (1,5) [(1,0+3+2),(2,0),(3,0),(4,0),(5,0+6)] = array (1,5) [(1,5),(2,0),(3,0),(4,0),(5,6)]
2章「上位者問題」:
運算を追っていたらウッとなりました。一部の行間がデカいです。
まぁ手を動かせばいい話なんですが。
[(z, scount z zs) | z:zs <- map (++ys) (tails xs) ++ tails ys] = {- <- を++上で分配 -} [(z, scount z (zs ++ ys)) | z:zs <- tails xs] ++ [(z, scount z zs) | z:zs <- tails ys]
これはつまり
[(z, scount z zs) | z:zs <- map (++ys) (tails xs) ++ tails ys] = {- <- を++上で分配 -} [(z, scount z zs) | z:zs <- map (++ys) (tails xs)] ++ [(z, scount z zs) | z:zs <- tails ys] = [(z, scount z zs') | z:zs <- tails xs, let zs' = zs ++ ys] ++ [(z, scount z zs) | z:zs <- tails ys] = [(z, scount z (zs ++ ys)) | z:zs <- tails xs] ++ [(z, scount z zs) | z:zs <- tails ys]
ということ。
今後もこれぐらいは瞬時に脳内補完できるような運算力が求められてくるのだろうか。
3章「鞍型探索の改良」:
p12. Theoの発言
最悪の場合でをおよそ回計算し, 最良の場合では回になります.またはのどちらかはよりも十分小さくなりうるので, たとえば, であれば最悪の場合でステップしかかからないアルゴリズムになりますね
これは全く何を言っているのかわからなかったし、今でも理解できない。
理解できないというのは、「またはのどちらかはよりも十分小さい」からの話の流れとして意味がわからない、ということ。の場合はももに比べて十分小さいので最悪ケースでもステップという結論自体は正しいと思います。
p14. Anneによるinvert関数の計算量の下界の解析
ここでというのは、二引数狭義単調増加関数の値をにする(、つまりとなる)ようなのリストとしてありうる全ての組み合わせの数。
(つまりたとえばなら、問題「をみたすようなのリストを求めよ」の答えとしては
の6通りが考えられるから、)
を見積もるのはやさしいことです. かつの範囲でとなる対のそれぞれのリストが, の長方形を左上隅から右上隅へいく階段形状と一対一で対応しています. この階段形状内の角部分にが現れます.
ここは難しかった。
まず、この章のテーマでもある鞍型探索アルゴリズムが解を見つけていく挙動をみてもわかるように、をみたすは長方形上に左上から右下に
(をみたすを赤で表示している)
のように並ぶということがわかる。
すると、のリスト一つに対し、その各点が角になるような階段
を対応付けることができる。これは一対一の対応である。
4章「選択問題」:
以下、タイトルに「」が付いていた場合は引用であることを意味するものとします。
「順序付き型の要素の……」
型に順序が付いてる……?とか一瞬思ったけど要はOrd型クラスのインスタンスになるような型のことを言っているらしい。
abideな演算子
縦横不問(abide)というのは初めて聞いたが、要するに
xs U ys ++ ++ ys U vs
と並べたときに
縦、横の順で[xs ++ ys] ∪ [us ++ vs]と計算しても、
横、縦の順で[xs ∪ us] ++ [ys ∪ vs]と計算しても結果は同じ、というイメージで理解した。
「の場合は双対になる」
smallest k (xs ++ [a] ++ ys) (us ++ [b] ++ vs)の結果は(xs ++ [a] ++ ys)と(us ++ [b] ++ vs)とを取り替えても変わらないので、
適当に取り替えることでを仮定できるという意味。
の場合は?
今、unionする集合同士は互いに素であることを仮定している。
5章「組和の整列」:
「この行列は標準Young盤の一例である」
xs,ysが昇順ソート済みなら、[[x y | y <- ys] | x <- xs]の各行・各列は明らかに昇順だから標準Young盤(ヤング図形 - Wikipedia)になっている。ここでは、xs及びysは集合(なので要素に重複がない)という前提が使われている。(重複がもしあれば半標準(semi-standard)というべき)
比較回数の下界
はソートしたい組和が並ぶ行列の各要素にインデックスを割り振る方法の数なので、ソートの出力は通りありうる。
あとは一般的な比較ソートの下界の議論と同じ。
tableの意味
xs、ysを順序集合の要素のリストとする。
table xs ys :: [(Int, Int, Int)]の中の(1, i, j) < (2, k, l)という2つの要素を見ることで得られる情報は、
(xsのi番目の要素)(xsのj番目の要素) (ysのk番目の要素)(ysのl番目の要素)
つまり
(xsのi番目の要素)(ysのk番目の要素) (xsのj番目の要素)(ysのl番目の要素) ((5.2)より)
である。
したがって、2つの組差とを比較するにはtable xs ysの対応する要素の大小を参照すればよく、配列に変換してしまえばこれは定数時間でできる。
ここで重要なのは、目的は上の比較回数を減らすことであるということ。当然、tableを使ってもソートにはかかるが、その際の比較演算は自然数上の比較演算なので、
必要な上の要素の比較はtableを構築する際に必要な分だけということになる。
6章「小町算」:
正格関数
divergence: を適用した結果が発散する関数を正格関数という。
Haskellなら「undefinedを適用したときに*** Exception: Prelude.undefinedを吐かない」と言いかえてもいいはず。
filter f: [a] -> [b]はまずリストをパターンマッチして次の挙動を決めるのだから、自明に正格。
(6.3)は(iii)の具体化
(6.3)が
filter (ok . value) . extend x = filter (ok . value) . extend x . filter (ok . value)
f = filter (ok . value), g = extend, h = extend' = filter (ok . value) . extendを代入して
f . (g x) = h x . f
両辺にyを適用
f (g x y) = h x (f y)
これが条件(iii)に一致。
(6.10)の導出
なんでこれだけ導出載ってないんだろう。「自明」ってことだろうか。
filter (p . snd) . map (fork (f, g)) = {- filter p . map f = map f . filter (p . f) より -} map (fork (f, g)) . filter (p . snd . fork (f, g)) = {- (6.5) snd . fork (f, g) = g より -} map (fork (f, g)) . filter (p . g)
glue(一つめの定義)の意味
glue 1 "23×4+56" = "123×4+56" ++ "1×23×4+56" ++ "1+23×4+56"
のような「式の一番前に数字を一つ加える」挙動が、
glue 1 [[[2,3],[4]],[[5,6]]] = [[[1,2,3],[4]],[[5,6]]] ++ [[[1], [2,3],[4]],[[5,6]]] ++ [[[1]], [[2,3],[4]],[[5,6]]]
とリスト上の操作で実現されている。
7章「最小高さ木の構築」:
長らく更新がなかったのは普通にこの章が難しいからです。
誤りがあるかもしれませんのでこの章に関するツッコミは特に歓迎です。
この章を難しくしている最大の要因は、『木(Treeデータ型)』に対して定義されている関数と『森(Forestデータ型)*1』に対して定義されている関数が同名で、
かつそれらが文脈によってフレキシブルに入れかわるという点だと思います。後述するようにそれらは実質同じものなのでそうすることで議論の本質を外してしまうことはありませんが、
どちらがどちらなのかを注意して読まないとどっかで置いていかれることもあるかと思います。
prefixes関数(Tree版)
prefixes x tで「tの一番左の葉としてxを挿入した場合の木のリスト」を返すとあるが、
これは「周縁の最左要素がxとなるようにtにxを挿入した場合の木のリスト」という意味である。
つまりは木の周縁(fringe)を返す関数fringeを
fringe :: Tree -> [Int] fringe (Leaf x) = [x] fringe (Fork u v) = fringe u ++ fringe v
と定義したとき、以下の性質
t' ∈ prefixes x t ⇒ fringe t' === x ++ (fringe t)
を満たすということ。
left spine(木の左背骨)
二分木の定義
data Tree = Leaf Int | Fork Tree Tree
より、二分木は必ずFork (Fork (...(Fork (Leaf x) )... ) という形をしている。
このとき[Leaf x]を元の木の左背骨という。これは森である。
後に、左背骨を求める関数spineが登場する。
この左背骨をrollup = foldl1 Forkで一つの木にすると元の木に戻る。この操作を巻き上げという。
木の左背骨は一意に決まるので、これをもって左背骨(=最左要素が葉であるような森)と木とに一対一対応がつけられる。
prefixesの再定義
これまで木に対して各関数を考えてきたが、左背骨(:: Forest)と木の間に一対一対応がつけられることがわかった。
また、森の各木の周縁を同じ順で連結すると、その森を巻き上げた木の周縁になる、つまり
concatMap fringe forest === fringe $ rollup forest
成り立つ。
したがって、prefixes x tsを「(巻き上げた後にできる木の)周縁の最左要素がxとなるように森tsにxを挿入した場合の森のリスト」と再定義できる。
非決定的な(nondeterministic)関数
ここでいう非決定的とは、プログラム中に非決定性(nondeterminism)が含まれているという意味ではなく、
仕様だけでは出力が一意に決まらないという意味。
これとは別に、定義に乱数やインプットが含まれているようなプログラムをnondeterministicと呼ぶケースもある。
*2
この先の議論にはminBy costの定義がしっかりと与えられている必要はなく、
あくまで「costが最小の木を返す」という仕様が満たされていればよいので、定義を一部曖昧なままにした、と捉えたほうがいいかもしれない。
精緻化関係 上の運算
非決定的な関数にもプログラム運算が適用できるように精緻化関係上の運算が導入される。
つまり、精緻化関係の右辺が出力しうる値は左辺でも出力されうることが保証されている。
たとえば、anyOf :: [Int] -> Intを
anyOf [n1, n2, ..., nm] ∈ {n1, n2, ..., nm} {- n1, ..., nm のどれかをランダムに(非決定的に)出力 -}
と定義すると、anyOf (l1 ++ l2) anyOf l1 のような関係が成り立つ。
たまにを指してと書かれていることがあるっぽい?*3
融合変換の章
個人的にはここの話の流れが掴みづらい。
全体として「cost関数で順序付けをすると融合条件等を考えづらいので、cost'を使う」みたいな話になっている。
色々なことが省略され、行間に消えてしまっているので、補いつつ読むしかない。
cost', costsについて
costは木のコストで、cost'は木の左背骨に沿ってコスト情報を拾うことで得られるリストである。
cost' tの最左要素はcost tなので、cost'で最小の要素はcostで最小の要素でもある。
またcosts = map cost . reverseは、左背骨のコストを定義していると思ってよい。
cost' = costs . spineなので、costs = cost' . rollupである。つまり、costs ts < costs ts'ならばtsを左背骨にもつ木はts'を左背骨にもつ木よりコストが小さい。
insertについて
p40の最下部にある運算では、insertの規定が
minBy costs . prefixes x insert x
になっている。元々示されていたinsertの規定はminBy cost . prefixes x insert xなので、型がInt -> Tree -> TreeからInt -> Forest -> Forestに変わっている。
つまり、insert xは「森に対し、コストが最小になるようにxを挿入した森(のうちのどれか一つ)を返す」という関数。実際には一般の森ではなく左背骨に対して挿入が行われる。
以下、insertはInt -> Forest -> Forestの版として議論します。
以下、議論に必要なのでInt -> Tree -> Tree版のinsertをinsert'で表します。
insertとinsert'の間には次のような関係が成り立ちます。
(insert-rel): insert' x (rollup ts) = rollup (insert x ts)
p40最下部の運算で行われている「融合変換(fusion)」について
minBy costs . foldrn (concatMap . prefixes) (wrap . wrap . Leaf) foldrn insert (wrap . Leaf)
が単に「minBy costs . prefixes x insert xを用いて融合変換すると……」と書かれている。
これも要するに全てのxsについて
minBy costs (foldrn (concatMap . prefixes) (wrap . wrap . Leaf) xs) foldrn insert (wrap . Leaf) xs
ということだと思うのでそのつもりで解説する。
foldrnの融合変換を上の形で使うには、以下の2つが成り立たなければならない。
- 任意のxについて minBy costs ((wrap . wrap . Leaf) x) (wrap . Leaf) x
- (7.1'): 任意のx, fs, ts'について minBy costs fs ts' ならば minBy costs (concatMap (prefixes x) fs) insert x ts'
前者は要素が一つしかないので自明。後者(7.1')についてはp39に倣っていろいろ言う必要がある。
ここが完全に省略されているのは多分「前のページと同じようにできるので」的な意味合いなのだと思う。
まず、以下に示す(7.2')が成り立てば(7.1')が成り立つ。(★)
(7.2') minBy costs fs ts minBy costs (map (insert x) fs) insert x ts
(★)を示すには、p39の最下段にある運算と全く同じことをすればよいです。
{- concatMapを展開 -}
= minBy costs (concat (map (prefixes x) fs))
= (minBy costs . concat) (map (prefixes x) fs)
{- minBy costs . concat = minBy costs . map (minBy costs) -}
= (minBy costs . map (minBy costs)) (map (prefixes x) fs)
= minBy costs . (map (minBy costs) (map (prefixes x) fs))
{- map f (map g xs) = map (f.g) xs -}
= minBy costs . (map (minBy costs . prefixes x) fs)
{- f f' ならば map f map f' かつ g.f g.f' -}
minBy costs . (map (insert x) fs)
で、(7.2')が言えるためには単調性条件が言えて欲しいが、(7.2)とは違い、ここでは(7.4)が成り立つならば以下の単調性条件(7.3')が成り立つことが言える。
(7.3'): costs us costs vs costs (insert x us) costs (insert x vs)
なぜならば、costs us costs vs cost' (rollup us) cost' (rollup vs)であり、
また、これと(insert-rel)より
costs (insert x us) costs (insert x vs)
cost' (rollup (insert x us)) cost' (rollup (insert x vs))
cost' (insert' (rollup us)) cost' (insert' (rollup vs))
なので、(7.3')と(7.4)は同値であるからである。
すなわち、(7.4)が言えれば(7.2')、(7.1')が言えるから融合変換が可能になる。
結局、insert :: Int -> Forest -> Forestが実装でき、(7.4)が}成り立てば融合変換が可能ということになり、p41冒頭の主張に繋がる。
単調性条件について
なぜ(7.3)ならば(7.2)が成り立つのか、について。
(7.3)は関数 insert x が順序関係に関して単調であるという条件であり、
これが成り立つならばtsでに関して最小の要素はt_min = [insert x t | t<-ts]でに関して最小の要素はinsert x t_minとなるので、(7.2)が成り立つこととなる。
上述した(7.2')⇒(7.3')も同じ議論で導ける。
「(7.4)から……が導けることをこのあと示す」
ここが全くわからない。どう見てもこの後に導出があるようには見えないし、そもそもこの先の議論で必要な事実ではない。
訳出ミスでもなさそう。情報求む。
j+1 k nの範囲で
「観察によって確かめよ」とあるが、以下の事実に着目するとわかりやすい。
, の定義より、がより真に増加するのは、
がより真に増加し、かつその値がより大きいとき、つまり
のときである。
つまり、ならばは増加しないことになり、当然kがj+1より大きい範囲では増加しようがない。
最適なinsertの方針
p42冒頭では、「jが(7.5)を満たし、かつijなるiも(7.5)を満たすなら、(下から数えて)i番目に挿入したほうがj番目に挿入するよりコストが低くなる」
という事を述べている。これはつまり、コスト最小を目指すなら(7.5)を満たす範囲でなるべく低い位置に挿入すべきだということを示している。
これで、最適な挿入を実装する方針が固まったことになる。
Egisonポーカー再考
Egisonコードの例としてよく紹介される(?)例としてポーカーの役判定があります。
http://www.egison.org/demonstrations/poker-hands.html
- コレクションをMultisetとして扱うことにより手札の順序を考慮しなくてよい
- non-linear patternを扱える(value patternを使える)おかげでペアやスリーカード系の判定が直接的に書ける
- カードの数部分をintegerではなくmod 13でマッチすることで、ストレートにおける10-11-12-13-Aのようなケースをうまく扱える(後述するように、実際はこれだけだと11-12-13-A-2とかにマッチしてしまってマズいですが)
など、いろいろな面倒をマッチャーに任せて楽ができるという主旨のものです。
これを題材にいろいろやってみました。
Jokerを加える
Jokerを所謂ワイルドカードとして加えるルールがあります。これをカードに加えたポーカーを考えたい、つまり
(poker-hands {<Card <Club> 12>
<Card <Club> 10>
<Joker>
<Card <Club> 1>
<Card <Club> 11>})
のようにJokerを含んだ手を役判定したいとき、考えられる手段としては
- poker-handのパターンを修正する
- cardマッチャーを修正する
前者は、要は例えばスリーカードのパターン
<cons <card _ $n>
<cons <card _ ,n>
<cons <card _ ,n>
<cons _
<cons _
<nil>>>>>>
を
<cons <card _ $n>
<cons (| <card _ ,n> ,<Joker>)
<cons (| <card _ ,n> ,<Joker>)
<cons _
<cons _
<nil>>>>>>
とか書き直せばいいという話ですが、全パターンにこれをやろうとすると場合分けやらがひどいことになって多分死にます。あと、折角直感的にパターンを書けていたのに煩雑になってしまいます。
後者は<Joker>がワイルドカード、つまり存在する全てのカードとしてマッチするようにマッチャーを書き直すというものです。一から全部書き直してもいいですが、algebraic-data-matcherになんとかしてもらえそうなところは全面的に委託する感じで書いてみます。
このようにすると<Joker>がワイルドカードとしてのふるまいをするようになり、poker-handsは何も変えなくてよいということになります。
役判定アルゴリズムのパターン記述部とカードの実装を分離できている
ので、個人的にはこっちの解決策のほうが好きです。
ストレート(及びストレートフラッシュ)の修正
これは普通に間違ってるなーと前からちょっと思っていて、要はストレート(ストレートフラッシュ)のパターンって微妙に間違っていて、12->13->A->2->3みたいなのはストレートにはならんわけです。
これを修正するのは一瞬で、
でOK。一番小さい数字が11未満であることを保証してやれば、12->13->A->2->3はちゃんと弾かれるわけですね。
個人的にはEgisonやりたてのころはpredicate patternは慣れないと使い方わからないようなところあったので、ちょっと複雑にはなるけど例として入れといたら入門的な意味でいいんじゃねーのと思ったりしました。ANDパターンの使用例にもなりますし。
Type Level Ifについて
前記事 http://xenophobia.hatenablog.com/entry/2014/05/07/004251 書いてるときにふと、型レベルのIf使ったほうがいいのかなと一瞬思ったりしました。
singletonsパッケージのData.Singletons.Types(https://hackage.haskell.org/package/singletons-0.10.0/docs/Data-Singletons-Types.html)とかに型レベルのIfが置いてありますが、Type familyの「評価」ってそもそもLazyなんですかね。LazyだったらちゃんとIfとして機能しますが。というわけでやってみました。
結果、ループにハマりました。つまりこのコードにおいては引数部分が先に評価されている。
Line 10: 1 error(s), 0 warning(s) Context reduction stack overflow; size = 201 Use -fcontext-stack=N to increase stack size to N Loop Int ~ uf0 In the expression: undefined :: If True Int (Loop Int) In an equation for `a': a = undefined :: If True Int (Loop Int) In the expression: do { let a = undefined :: If True Int (Loop Int); print "Hello" }
ということは式とは違って評価はStrictっぽい?ちょっと意外。