Thinking Functionally with Haskell勉強メモ: 第5章2 確定したマスの情報をベースにフィルタ
非常に非効率だが問題文に誠実に宣言的に書いているので正しさが(ほぼ)自明なプログラムに対して、ここから成り立つと証明できる法則を使って、結果が同一なことを担保しつつ効率が上がるような変換を行っていく。
前回の数独ソルバーは981-kものGrid
を一つ一つ作成してはルールに従ってフィルタしていくものだった。Grid
作成過程でどうにかして作る数を減らさなければ、実際的な時間内で計算が終わらない。
今回はもっとも簡単なアイディアである「確定したマスの行、列、箱の他のマスが、確定した値を含まないようフィルタする」を実装し、それがもとのプログラムと同一の結果を返すことを証明する。
prune :: Matrix [Digit] -> Matrix [Digit] filter valid . expand . choices = filter valid . expand . prune . choices
となるようなprune
関数を求めたい。
Matrix Digit
型の問題文をchoices
でMatrix [Digit]
型の「各マスに入る可能性のあるDigitのリストのMatrix」に変換したわけだが、その「入る可能性のあるDigit」をexpand
する前に減らしておく、という戦略である。
pruneRow
関数
まずRow
に対してフィルタするpruneRow
を考えてみる。
pruneRow :: Row [Digit] -> Row [Digit] pruneRow row = map (remove fixed) row where fixed = [d | [d] <- row]
この定義、とくにfixed
の部分が面白い。[d | [d] <- row]
でrow
の中身に対してパターンマッチを行っていて、要素1のリストだけをフィルタしてその要素を摘出している。too cleverな気もするけど・・・
remove :: [Digit] -> [Digit] remove _ [x] = [x] remove ds xs = filter (`notElem` ds) xs
[x]
な(つまり確定している)マス以外は、確定した値を含まないようフィルタする。remove _ [x] = [x]
がないと確定したマスが空になってしまう。
とここで
The function pruneRow satisfies the equation
filter nodups . cp = filter nodups . cp . pruneRow
とさらっと書いてあって、「自明か?自明なのか?」と数学書でおなじみのアレになる。いや、やっていることを考えれば成り立つとは思うんだけど証明しなくていいのか?
これから使う等式
さて、ここでいくつか他にも重要な等式を列挙しておこう。
rows, cols, boxs
は対合
rows . rows = id cols . cols = id boxs . boxs = id
boxs
ケースの証明は意外と簡単。cols
つまりtranspose
は自明っぽいけど(transposeしてtransposeしたらもとに戻る)証明はTFwHのここまでの説明だけでは無理らしい。
group . ungroup = id --すでにgroupされているMatrixに対して ungroup . group = id
まあ自明。これを使ってboxs
の対合性を証明できる。
expand
とrows...
の関係
map rows . expand = expand . rows map cols . expand = expand . cols map boxs . expand = expand . boxs
box
の定義上9x9のMatrix
が引数な場合に限り、だと思う(本ではN2xN2となっていたがその場合box
の定義も変える必要があるはず)
cp
の特性
map (map f) . cp = cp . map (map f) filter (all p) . cp = cp . map (filter p)
最初の式は自然変換。cp
は要素に対してなにも働きかけないので、f
を各要素に適用するのがcp
の前か後かで結果は変わらない。
二番目の式はちょっとややこしい。
- 左辺は直積をとってできた
[[a]]
から、[a]
の全要素がp
を満たしているリストのみを摘出している - 右辺は直積をとる前にそもそも
[a]
からp
を満たさない要素を抜いている
その結果が等しいということは、直積する前にフィルタしてしまって効率化を図れるということ。
対合とfilter/map
-- f . f = idの場合 filter (p . f) = map f . filter p . map f filter (p . f) . map f = map f . filter p
まあ自明。第4章ですべてのp, f
において
filter p . map f = map f . filter (p . f)
を証明したのを使えば楽に証明できる。
pruneRow
の等価性の証明
以上の等式を使って、filter valid . expand . completions = filter valid . expand . prune . completions
が成り立つようなprune
をpruneRow
を使って書けることを証明する。
まずfilter valid
を、valid
の定義から以下のように書き直す:
filter valid = filter (all nodups . rows) . filter (all nodups . cols) . filter (all nodups . boxs)
この変換も自明として扱われている。
p x = p1 x && p2 x filter p = filter p1 . filter p2 = filter p2 . filter p1
が成り立つのは自明だろうか?直感的にはわかるが・・・
さて、これで
filter valid . expand = filter (all nodups . rows) . filter (all nodups . cols) . filter (all nodups . boxs) . expand
になるので
filter (all nodups . boxs) . expand filter (all nodups . cols) . expand filter (all nodups . rows) . expand
を一つ一つみていく(というのは嘘で最初のやつを解いたらまったく同じロジックであとの二つも解ける)。
filter (all nodups . boxs) . expand = {filter (p . f) = map f . filter p . map f -- f . f = id} map boxs . filter (all nodups) . map boxs . expand = {map boxs . expand = expand . boxs} map boxs . filter (all nodups) . expand . boxs = {expand = cp . map cp} map boxs . filter (all nodups) . cp . map cp . boxs = {filter (all p) . cp = cp . map (filter p)} map boxs . cp . map (filter nodups) . map cp . boxs = {functor law of map} map boxs . cp . map (filter nodups . cp) . boxs = {filter nodups . cp = filter nodups . cp . pruneRow} map boxs . cp . map (filter nodups . cp . pruneRow) . boxs
ここまででpruneRow
を等価な変換で登場させることに成功した。
あとは左側にfilter (all nodups . boxs) . expand
が戻ってくるように逆変換していく:
map boxs . cp . map (filter nodups . cp . pruneRow) . boxs = {functor law of map} map boxs . cp . map (filter nodups) . map cp . map pruneRow . boxs = {filter (all f) . cp = cp . map (filter f)} map boxs . filter (all nodups) . cp . map cp . map pruneRow . boxs = {expand = cp . map cp} map boxs . filter (all nodups) . expand . map pruneRow . boxs = {filter (p . f) . map f = map f . filter p -- f . f = id} filter (all nodups . boxs) . map boxs . expand . map pruneRow . boxs = {map boxs . expand = expand . boxs} filter (all nodups . boxs) . expand . boxs . map pruneRow . boxs
これで
filter (all nodups . boxs) . expand = filter (all nodups . boxs) . expand . boxs . map pruneRow . boxs
という等式が成り立つことが証明できた。まったく同じ理屈で
filter (all nodups . rows) . expand = filter (all nodups . rows) . expand . rows . map pruneRow . rows filter (all nodups . cols) . expand = filter (all nodups . cols) . expand . cols . map pruneRow . cols
も成り立つ。
boxs . map pruneRow . boxs
は、Matrix
をまず3x3の区切りの集まりに変換し、各集まりに対して確定したマスの数を取り除くpruneRow
を適用し、またbox
を適用する。box
を適用したMatrix
にbox
を再度適用すると形はもとに戻るので、最終的には「3x3の区切りをただしくpruneしただけ」という結果が残る。
なのでpruneBy f = f . map pruneRow . f
を定義して
pruneBy boxs
はMatrix
の形を変えずにboxs
単位で確定値をフィルタpruneBy cols
はMatrix
の形を変えずにcols
単位で確定値をフィルタpruneBy rows
はMatrix
の形を変えずにrows
単位で確定値をフィルタ
とみなすことができる。
filter (all nodups . boxs) . expand = filter (all nodups . boxs) . expand . pruneBy boxs filter (all nodups . rows) . expand = filter (all nodups . rows) . expand . pruneBy rows filter (all nodups . cols) . expand = filter (all nodups . cols) . expand . pruneBy cols
とより簡単に表現できるようになる。
話をfilter valid . expand
に戻すと:
filter valid . expand = {filter validの書き換え} filter (all nodups . rows) . filter (all nodups . cols) . filter (all nodups . boxs) . expand = {filter (all nodups . f) . expand = filter (all nodups . f) . expand . pruneBy f -- f in (rows, cols, boxs)} filter (all nodups . rows) . filter (all nodups . cols) . filter (all nodups . boxs) . expand . pruneBy boxs = {filter A . filter B = filter B . filter A} filter (all nodups . rows) . filter (all nodups . boxs) . filter (all nodups . cols) . expand . pruneBy boxs = {filter (all nodups . f) . expand = filter (all nodups . f) . expand . pruneBy f -- f in (rows, cols, boxs)} filter (all nodups . rows) . filter (all nodups . boxs) . filter (all nodups . cols) . expand . pruneBy cols . pruneBy boxs = {filter A . filter B = filter B . filter A} filter (all nodups . cols) . filter (all nodups . boxs) . filter (all nodups . rows) . expand . pruneBy cols . pruneBy boxs = {filter (all nodups . f) . expand = filter (all nodups . f) . expand . pruneBy f -- f in (rows, cols, boxs)} filter (all nodups . cols) . filter (all nodups . boxs) . filter (all nodups . rows) . expand . pruneBy rows . pruneBy cols . pruneBy boxs = {filter validの書き換え} filter valid . expand . pruneBy rows . pruneBy cols . pruneBy boxs
と、filter valid . expand
の後ろにrows/cols/boxs
すべてのpruneBy f
をつけても等価であることが証明できるので:
prune = pruneBy rows . pruneBy cols . pruneBy boxs
とprune
をpruneBy
を使って定義すれば:
filter valid . expand = filter valid . expand . prune filter valid . expand . completions = filter valid . expand . prune . completions
を満たす。さらには:
filter valid . expand . completions = filter valid . expand . prune . prune . ... . prune . completions
とprune
を任意の回数入れても等価。prune
することで確定したマスが出てきた場合、もう一度prune
することでさらに絞り込みが可能になる場合も多い。
結果が変わらなくなるまでf
を適用し続ける、という関数manyを
many :: (Eq a) => (a -> a) -> a -> a many f x = if x == y then x else many y x where y = f x
と定義する。これを使えばsolve
の定義を:
solve = filter . valid . expand . many prune . choices
と表現でき、これは
solve = filter . valid . expand . choices
と等価であることを証明できた。(many
の等価性担保は本当に自明か?)
まだ最適化の余地は大きい。次回はその話。
Thinking Functionally with Haskell勉強メモ: 第5章1 数独ソルバー最適化以前
第5章は数独ソルバーの実装を通してHaskell/関数型プログラムの書き方(の一つ)を紹介するというもの。
まずは問題をできるだけ宣言的に解くコードを記述して、その後最適化していく。
この記事は第一段階の非常に非効率な宣言的コードの説明まで。
データ型の定義
まずは問題の入力と出力を表すデータ構造を型として定義する:
data Matrix a = [Row a] data Row a = [a] type Grid = Matrix Digit type Digit = Char digits :: [Digit] digits = ['1' .. '9'] blank :: Digit -> Bool blank = (=='0')
Matrix
型は[[a]]
のalias。一枚の数独問題(回答前・後を問わず)はGrid
型で表現する。Grid
はMatrix Digit
のaliasであり、Digit
はChar
のalias。記入されていないマスは0
で表される。
solve
関数
solve :: Grid -> [Grid] solve = filter valid . completions valid :: Grid -> Bool completions :: Grid -> [Grid]
数独を解く関数solve
の定義。
問題をGrid
として受け取り、正解回答となるGrid
すべてのリストを返す。completions
とvalid
という二つの関数を組み合わせて作る。
まずはcompletions
。問題Grid
を受け取り、、空のマスに1から9までを当てはめていくことで問題を(間違っていてもいいので)埋めきったすべての(非常に多くの)パターンのGrid
のリストを返す。
valid
は埋めきったGrid
が数独のルールに従っているかを判定する。valid
でフィルタすることで、すべてのパターンから正解例のみを取り出す。
completions
定義は:
completions = expand . choices
構成要素である二つの関数の型は:
choices :: Grid -> Matrix [Digits] expand :: Matrix [Digits] -> [Grid]
あるいはこちらのほうがわかりやすいかもしれない:
choices :: Matrix Digits -> Matrix [Digits] expand :: Matrix [Digits] -> [Matrix Digits]
choices
はDigits
のMatrix
である問題文から、確定しているマスは要素数1、空のマスは要素数9(1..9)のDigits
のリストのMatrix
に変換する。
expand
はその「各マスが取り得るDigit
値を列挙したリストのMatrix
」を引数にとり、「各マスの取り得る値の直積集合である『Digit
のMatrix
のリスト』」を返す。
choices
choices = map (map choice) choice d | blank d = digits | otherwise = [d]
個別のマスに対して、'0'以外ならその値のみ入っているリスト、'0'なら['1'..'9']を返すchoice
関数を適用する。
例えば引数が
[['1', '2', '3', '4', '5', '6', '7', '0', '0'], ..., ......]
だった場合、以下を返す:
[[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'], ['1', '2', '3', '4', '5', '6', '7', '8', '9'], ['1', '2', '3', '4', '5', '6', '7', '8', '9']], ..., ......]
expand
expand
を定義するためにはまずcp
関数を定義する。cp
とは直積集合Cartesian Productの略。定義は:
cp :: [[a]] -> [[a]] cp [] = [[]] cp (xs:xss) = [x:ys | x <- xs, ys <- yss] where yss = cp xss
例えばcp
の引数が
[['1'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'], ['1', '2', '3', '4', '5', '6', '7', '8', '9'], ['1', '2', '3', '4', '5', '6', '7', '8', '9']]
だった場合、以下を返す:
[['1', '2', '3', '4', '5', '6', '7', '1', '1'], ['1', '2', '3', '4', '5', '6', '7', '1', '2'], ['1', '2', '3', '4', '5', '6', '7', '1', '3'], ['1', '2', '3', '4', '5', '6', '7', '1', '4'], ... ['1', '2', '3', '4', '5', '6', '7', '9', '8'], ['1', '2', '3', '4', '5', '6', '7', '9', '9'], ]
map cp
で一つ一つの行を「その行が取り得るすべての「9つのDigitのリスト」のリスト」に変換する。
それをさらにcp
することで、各行が取り得るすべての値の直積に変換する。それがマスを埋めたGridのパターンすべてを網羅したリストだ。
なのでexpand
の定義は:
expand = cp . map cp
なのでcompletions
を展開していくと:
completions = expand . choices = cp . map cp . choices = cp . map cp . map (map choice)
となる。
valid
数独のルールに従ってvalid
の定義は以下の通りになる:
valid g = all nodups (rows g) && all nodups (cols g) && all nodups (boxs g)
nodups
はno duplicatesの略。各行(rows)、各列(cols)、各3x3の区切り(boxs)にダブった数字がないことが正解の条件である。
all
all
関数はリストのすべてがpを満たしているかチェックする:
all :: (a -> Bool) -> [a] -> Bool all p = and . map p
nodups
あるリストに同じ要素が二回出てくるかチェックする関数。
nodups :: (Eq a) => [a] -> Bool nodups [] = True nodups (x:xs) = all (/=x) xs && nodups xs
一見O(n2)だがn=9なので気にしない。
rows
rows
はあるGrid
の行のリストを返す関数。
本には型はrows :: Matrix a -> Matrix a
とあったが、個人的には意味的にrows :: Matrix a -> [[a]]
ではないか?と思っている。
たまたまMatrix a
は[Row a]
なのでrow
の定義は:
row = id
cols
cols
はGrid
の列のリストを返す関数。
これと次のboxs
はどう考えてもMatrix a -> Matrix a
というよりMatrix a -> [[a]]
じゃないか?
cols = transpose transpose :: [[a]] -> [[a]] transpose [xs] = [ [x] | x <- xs] transpose (xs:xss) = zipWith (:) xs (transpose xss)
transpose
は私が定義した。本の中ではtranspose
関数はなく、直接cols
をこの定義にしていた。ただ、cols
という名前で書くのは(とくに次のboxs
でも使う手前)不適当な気がしたのでいったんtranspose
という名前で定義してcols
をaliasとした。
しかしこのtranspose
の定義、一番最後のリストを一要素ずつのリストのリストにする、というのを再帰の終了条件にして、そのリストにzipWithで前のリストの要素を付け加えていく。理屈はわかるのだが自前で考えろと言われて書ける気が全くしない・・・
boxs
boxs
は3マスx3マスの各区切りの中の数リストが9つ入ったリストを返す。
boxs :: Matrix a -> [[a]] boxs = map ungroup . ungroup . map transpose . group . map group group :: [a] -> [[a]] group [] = [] group xs = take 3 xs : group drop 3 xs ungroup :: [[a]] -> [a] ungroup = concat
リストを「3つの要素ごとのリスト」のリストに変換する関数がgroup
、それを元のリストに戻すのがungroup
(実際の処理はただのconcat
)。
挙動を理解するのがけっこうややこしい関数の組み合わせなので、4x4のMatrix
を2x2の区切り内の要素のリストに変換する様を書いてみる。
まず入力:
[[a1, a2, a3, a4], [b1, b2, b3, b4], [c1, c2, c3, c4], [d1, d2, d3, d4]]
map group
適用:
[[[a1, a2], [a3, a4]], [[b1, b2], [b3, b4]], [[c1, c2], [c3, c4]], [[d1, d2], [d3, d4]]]
中の4つの要素のリストが、2つの要素のリスト二つのリストに変換されている。
次はgroup
適用:
[[[[a1, a2], [a3, a4]], [[b1, b2], [b3, b4]]], [[[c1, c2], [c3, c4]], [[d1, d2], [d3, d4]]]]
4x2x2要素のリストのリストのリストだったのが、2x2x2x2のリストx4になっている。
map transpose
:
[[[[a1, a2], [b1, b2]], [[a3, a4], [b3, b4]]], [[[c1, c2], [d1, d2]], [[c3, c4], [d3, d4]]]]
[a3, a4]
と[b1, b2]
、[c3, c4]
と[d1, d2]
の位置が入れ替わった。
ungroup
適用:
[[[a1, a2], [b1, b2]], [[a3, a4], [b3, b4]], [[c1, c2], [d1, d2]], [[c3, c4], [d3, d4]]]
2x2x2x2から4x2x2に。
map ungroup
適用:
[[a1, a2, b1, b2], [a3, a4, b3, b4], [c1, c2, d1, d2], [c3, c4, d3, d4]]
4x4のリストのリストに戻っているが、最初のリストはa1, a2, b1, b2
と左上にあった数字、次のリストは右上、次は左下、最後は右下、と2x2に分割した各区切りの数字のリスト4つになっていることがわかる。
これを3x3の区切りで9x9のGrid
に適用したのが上記のboxs
。
結論
rows
もcols
もboxs
もすべてのリストがvalid
ならそのGrid
は数独のルール上正しく記入されていることになる。なのでcompletions
の戻り値をvalid
でフィルタすれば、正しい回答のリストが得られる。
が、completions
が返すリストは遅延評価とはいえ981-k (kは数字が知られているマスの数)もの結果を返し、それを一つ一つフィルタしていかなければいけない。現実的に不可能である。
なので次回はこの非常に効率の悪いコードに対しどのような等価な変換を加えていくことで最適化ができるか、というポイントを探っていく。
Thinking Functionally with Haskell勉強メモ: 第4章問題3
Exercise J
以下のものの意味を考え、間違っているものを見つけよ:
map f . take n = take n . map f
リストの先頭からからn個の要素を取ってfを適用するのと、リストのすべての要素にfを適用してできた新しいリストの先頭からn個の要素を取るのは等しい。正しい。
map f . reverse = reverse . map f
リストを逆順にしてすべての要素にfを適用するのと、リストのすべての要素にfを適用してから逆順にするのは等しい。正しい。
map f . sort = sort . map f
リストをソートしてからすべての要素にfを適用するのと、リストの要素すべてにfを適用してからソートするのは等しい。間違い。a < b
だからf a < f b
とは限らないため。
map f . filter p = map fist . filter snd . map (fork (f, p)) fork :: (a -> b, a -> c) -> a -> (b, c) fork (f, g) x = (f x, g x)
fork
は二つの関数のtuple
とひとつのナニカ(これって何て呼ぶべきなんだろう?オブジェクト?要素?)を引数にとり、そのナニカに二つの関数適用した結果をtuple
として返す。
あるリストを関数pでフィルタして、残った要素すべてにfを適用するのは、そのリストの各要素にfが適用された結果とpが適用された結果のtuple
のリストをtuple
の第二要素でフィルタして第一要素だけ取り出すのと等しい。正しい。
filter (p . g) = map (invertg) . filter p . map g invertg . g = id
あるリストを、各要素にgとpをその順に適用した結果でフィルタするのは、そのリストの全要素にgを適用しpでフィルタしてからgの逆関数を残った要素すべてに適用するのと等しい。正しい。
reverse . concat = concat . reverse . map reverse
あるリストを統合してから逆順にするのは、そのリストの要素の中身を逆順にしてからリスト自体を逆順にして統合するのに等しい。正しい。
filter p . concat = concat . map (filter p)
リストを統合してからpでフィルタするのは、リストの中のリスト一つ一つをpでフィルタしてからすべて統合するのに等しい。正しい。
Exercise K
zip
とcross
が以下のように定義されているとする:
unzip = fork (map fst, map snd) cross (f, g) = fork (f . fst, g . snd)
unzip
とcross
の型は?:
unzip :: [(a, b)] -> ([a], [b]) cross :: (a->b, c->d) -> (a, c) -> (b, d)
unzip
、cross
、fork
などに関して以下の等式が成り立つ:
cross (f, g) . fork (h, k) = fork (f . h, g . k) fork (f, g) . h = fork (f . h, g . h) fst . cross (f, g) = f . fst snd . cross (f, g) = g . snd
上記の等式とfunctor law of mapsを使って
cross (map f, map g) . unzip = unzip . map (cross (f, g))
が成り立つことを証明せよ:
cross (map f, map g) . unzip = {unzipの定義} cross (map f, map g) . fork (map fst, map snd) = {等式1:cross().fork()=fork()} fork (map f . map fst, map g . map snd) = {functor law of map} fork (map (f . fst), map (g . snd)) = {等式3:fst . cross(f, g) = f . fst} fork (map (fst . cross (f, g)), map (g . snd)) = {等式4:snd . cross(f, g) = g . snd} fork (map (fst . cross (f, g)), map (snd . cross (f, g))) = {functor law of map} fork (map fst . map (cross (f, g)), map snd . map (cross (f, g))) = {等式2:fork (f, g) . h = fork (f . h, g . h)} fork (map fst, map snd) . map (cross (f, g)) = {unzipの定義} unzip . map (cross (f, g))
Exercise L
cross (f, g) . cross (h, k) = cross (f . h, g . k)
を証明せよ:
cross (f, g) . cross (h, k) = {crossの定義} cross (f, g) . fork (h . fst, k . snd) = {等式1:cross() . fork() = fork()} fork (f . h . fst, g . k . snd) = {crossの定義} cross (f . h, g . k)
cross (id, id) = id
の理由は?:
cross (id, id) = fork (id . fst, id . snd) = fork (fst, snd)=id
最後の一歩はforkの定義から自明な気がするが証明できていない・・・
Functor
が一つの関数をとって要素に適用するfmap
が定義されているデータ構造の型クラスだとしたら、Bifunctor
は二つの関数をとって要素に適用するbimap
が定義されている型クラスである。cross
はbimap
に非常に近いが、bimap
の型は:
bimap :: (a -> c) -> (b -> d) -> bif a b -> bif c d
である。(bif
はBifunctorインスタンスの型)
cross
をbimap
を使って定義するとcross (x, y) = bimap x y
になる。
data Either a b = Left a | Right b
という型をBifunctorのインスタンスにしたい場合の定義は:
instance Bifunctor Either where bimap (f, g) Left a = f a bimap (f, g) Right b = g b
Effective C++勉強メモ: Item 23 Non-Member Non-Friend関数をNamespaceで管理しよう
前回の項目でカプセル化の重要性について書いてあったが、そのカプセル化を最大に実現するためには、内部実装に触れることのできる面積を最小化する必要がある。
つまり、メンバ関数、フレンド関数といった「クラス内部のデータメンバに直接アクセスできる関数」をなるべく減らすのがポイントになる。クラスに最小限のAPIを定義し、そのAPIを使ったNon-Member Non-Friend関数を使ってオブジェクトとやりとりする。
そのような関数を(たとえばJavaからきたプログラマは)別のユーティリティクラスのメンバ関数にすることも多いが、C++で自然な書き方はnamespaceを使って関連性のあるクラスと関数をまとめるやり方である。
例としては:
namespace WebBrowserStuff { class WebBrowser { ... }; void clearBrowser(WebBrowser& wb); } WebBrowserStuff::WebBrowser wb; WebBrowserStuff::clearBrowser(wb);
と、本に書いてあることをまとめたが、個人的には少し微妙な気がする。APIに対する期待としてclearBrowser
はWebBrowser
オブジェクトのclear
メンバ関数であってほしい。
- 直接内部データにアクセスしなくても実装できるならそのようにするべき(実装の問題)
- そのオブジェクトのAPIはそのオブジェクトが保持するべき(プログラムの意味の問題)
というコンセプトがぶつかっている。この齟齬は、クラスという言語機構がデータアクセスと外部APIをかなり粒度の大きいレベルでしか扱っていないのが原因だろうか?
ここにすごく違和感を感じるのは、私がPythonという「データアクセスを制限する」側面を完全に放棄している言語に慣れ親しんでいるからかもしれない。
Thinking Functionally with Haskell勉強メモ: 第4章問題2
Exercise F
data List a = Nil | Snoc (List a) a
というリストの別定義があったとする。
head
とlast
関数を定義せよ:
last :: List a -> a last (Snoc xs x) = x head :: List a -> a head (Snoc Nil x) = x head (Snoc xs x) = head xs
toList
とfromList
を実装せよ。例えばfromList
は
[1, 2, 3] == fromList (Snoc (Snoc (Snoc Nil 1) 2) 3)
となる:
toList :: [a] -> List a toList = toReverseList . reverse where toReverseList [] = Nil toReverseList (x:xs) = Snoc (toReverseList xs) x fromList :: List a -> [a] fromList = reverse . fromReverseList where fromReverseList Nil = [] fromReverseList (Snoc xl x) = x : fromReverseList xl
Exercise G
length :: [a] -> Int length [] = 0 length (x:xs) = 1 + length xs
というリストの長さを計算する関数のメモリ量は
length [1, 2, 3] = 1 + (1 + (1 + 0))
のようにO(n)である。それは定義が
length :: [a] -> Int length xs = loop (0, xs) where loop (n, []) = n loop (n, (x:xs)) = loop (n+1, xs)
のようにかわっても同じ。何故かというと遅延評価で
length [1, 2, 3] = loop ((((0 + 1) + 1) + 1), [])
となるまで計算されないので、加算処理がO(n)でメモリに積みあがっていく。先行評価ならloop
を使う方法だとO(1)になる。
Exercise H
take
とdrop
の再帰的な定義:
take :: Int -> [a] -> [a] take _ [] = [] take 0 _ = [] take n (x:xs) = x : take (n-1) xs drop :: Int -> [a] -> [a] drop _ [] = [] drop 0 xs = xs drop n (x:xs) = drop (n-1) xs
この定義だとtake undefined []
は[]
だがtake 0 undefined
はundefined
になる。なぜならtake _ []
とマッチするか確認するためにtake 0 undefined
の二番目の引数を評価しないといけないからだ。
そう考えると、take _ [] = []
とtake 0 _ = []
の順番を入れ替えることによってtake undefined []
とtake 0 undefined
のどちらかを[]
にすることはできるが、かならずどちらかはundefined
になる。
splitAt :: Int -> [a] -> ([a], [a])
の定義:
splitAt n xs = move n ([] xs) where move _ xst@(ys, []) = xst move 0 xst = xst move n (ys, (z:zs)) = move (n-1) (ys++[z], zs)
ただys++[z]
って遅そう・・・
Exercise I
map (f .g) xs = map f (map g xs)
という等式に関して:
- 任意のリストxs (finite, partial, infinite)、任意の(型のあった)関数fとgにおいて成り立つ。なので
map (f . g) = map f . map g
と表すこともできる。 map
と.
の定義から証明されなければいけない- 右辺から左辺へと変換するのは最適化
- 遅延評価で
map
がすべての要素に一気に適用されないとはいえ、二つの遅延リストが存在するより一つだけのほうが効率がいい
といった点が挙げられる。
ClojureとQuilでClifford Attractor
昔のProcessing Advent Calendarをいろいろ漁っていたらこんな記事があった:
上記の記事の冒頭の絵は
で表されるアトラクターである。
以下の参考サイトにはもっと例が載っている。
まさにClojureのiterate
関数の使いどころという趣があるので、Quilで書いて並べてみた:
コードを簡単に説明する。
a, b, c, d
のパラメータを受け取り、新たに無名関数を返す高階関数clifford
。無名関数lはアトラクターの計算式を表している:
(defn clifford [[a b c d]] (fn [[x y]] [(+ (-> y (* a) q/sin) (-> x (* a) q/cos (* c))) (+ (-> x (* b) q/sin) (-> y (* b) q/cos (* d)))]))
その高階関数に9つの違うパラメータを渡して、9つの違うアトラクター関数を作成する:
(def clifs (vec (map clifford [[-1.4 1.6 1.0 0.7] [1.6 -0.6 -1.2 1.6] [1.7 1.7 0.6 1.2] [1.5 -1.8 1.6 0.9] [-1.7 1.3 -0.1 -1.2] [-1.7 1.8 -1.9 -0.4] [-1.8 -2.0 -0.5 -0.9] [-1.4 -1.6 -1.0 -0.7] [1.8 -1.5 0.4 0.9]])))
各フレームでのアップデートでは、最後に描画した点の座標をアトラクター関数に渡し、その結果をまた同じアトラクター関数に渡し、と200回繰り返して出来た200個の座標をvector
として保持する、というのを各アトラクターごとに(つまり9回)行う。
update-state
で9つの関数と9つのvector
をupdate-series
にmap
し、update-series
内でアトラクターごとに新しい座標200点を算出する:
(defn update-series [f vs] (->> vs last (iterate f) (take 200) vec)) (defn update-state [state] (vec (map update-series clifs state)))
ここでiterate
を使っている。(iterate f x)
は(f x) (f (f x)) (f (f (f x))) ...
の無限リストなのでClifford Attractorを表現するには完璧。
あとは9つの座標リストを少しずつずらして描画するだけ:
(defn draw-state [state] (doseq [[i vs] (map-indexed vector state)] (q/with-translation [(-> i (mod 3) (* 200) (+ 150)) (-> i (/ 3) int (* 200) (+ 150))] (doseq [[x y] vs] (q/point (* 40 x) (* 40 y))))))
けっこう数字をいじっても面白いパターンが現れやすいので楽しい。
Effective C++勉強メモ: Item 22 データメンバは黙って`private`
クラス内のデータはpublic
でもprotected
でもなくprivate
にしよう。という話。
ポイントは三つ
構文の統一
オブジェクトのAPIはすべて関数(か演算子)になり、a.something
だったかa.something()
だったか悩む必要がなくなる。
データアクセスレベルのコントロール
getter/setter関数を実装するかどうかで
- read-write
- read-only
- write-only
- hidden
の四つのアクセスレベルを簡単にコントロールできる。
カプセル化
データメンバに直接アクセスされる場合、内部での実装変更はAPI変更になり、その影響の範囲は限定できない。これはprotected
でも同じで、このクラスを継承するすべてのクラスを影響し得る以上、原則的には変更の影響範囲を限定することができない。
逆に外部、継承クラスに提供するものが関数のみであれば、getterだったものが実は別のデータをもとに計算するようになった、などの実装変更をAPI変更なしで行える。
データは黙ってprivate
。