私がコメントで述べたように、再帰はHaskellのループプリミティブです。しかし、Haskellは再帰を直接使用するのではなく、よりユーザーフレンドリな抽象化を構築するための多くの力を私たちに提供します。 @Lazersmokeが述べたように、Comonadはコレクションの他の値(値の隣人など)に基づいてコレクションの個々の値を更新するときのよい抽象です。
Comonadクラスの例はウェブ上にはたくさんありますが、悲しいかなかMonadによって覆われています。だから、ここでも私のスコアに少しでもしようとしています。
これは長いポストになるので、私は結果から始めましょう。これはGHCiからのものです:
λ display example
[[A,A,A],[B,B,A],[A,A,B]]
λ display (transition example)
[[B,B,A],[A,B,B],[B,B,A]]
[OK]を今すぐビジネスに着きましょう。最初の数行政の事:
module Main where
import Control.Comonad -- from the comonad package
私は慎重に各部分を説明しようとするつもりだけど、大きな画像が明らかになる前に、それはしばらく時間がかかることがあります。まず、ジッパーと呼ばれる興味深いデータ構造を作成し、Functor
のインスタンスを実装します。
data U x = U [x] x [x] deriving Functor
instance Functor U where
fmap f (U as x bs) = U (fmap f as) (f x) (fmap f bs)
このデータ構造はあまり特別ではないようです。それは我々がそれを冷やすようにU
を使用する方法です。 Haskellは怠惰なので、U
コンストラクタで無限リストを使うことができます。たとえば、i1 = U [-1,-2..] 0 [1,2..]
はすべての整数を表します。しかし、それだけではありません。別の情報があります。中心点は0です。すべての整数をi2' = U [0,-1..] 1 [2,3..]
とすることもできます。これらの値はほとんど同じです。彼らはちょうど1つだけシフトされます。実際には、一方を他方に変換する関数を作成することができます。
rightU (U a b (c:cs)) = U (b:a) c cs
leftU (U (a:as) b c) = U as a (b:c)
あなたが見ることができるように、私たちはただの要素を再配置することにより、左または右にスライドU
をすることができます。 U
のインスタンスをShow
にして、rightU
とleftU
が機能することを確認します。私たちは明らかに無限のリストを印刷することはできないので、それぞれの面から3つの要素を取ります。
instance Show x => Show (U x) where
show (U as x bs) = (show . reverse . take 3) as ++ (show x) ++ (show . take 3) bs
λ i1
[-3,-2,-1]0[1,2,3]
λ leftU i2
[-3,-2,-1]0[1,2,3]
λ i2
[-2,-1,0]1[2,3,4]
λ rightU i1
[-2,-1,0]1[2,3,4]
究極の目標を確認しましょう。私たちは、すべての隣人に基づいて各値を更新できるデータ構造を作りたいと思っています。私たちのU
データ構造でそれを行う方法を見てみましょう。各数値をその近傍の和で置き換えたいとします。
sumOfNeighbors :: U Int -> Int
sumOfNeighbors (U (a:_) _ (b:_)) = a + b
そして、ちょうどそれが動作することを確認する:まずは、U
の現在位置の近傍を計算する関数を書いてみましょう
λ sumOfNeighbors i1
0
λ sumOfNeighbors i2
2
は残念ながら、これは私たちだけに単一の結果を提供します。この機能をあらゆる可能な位置に適用したいと考えています。よくU
にはFunctor
のインスタンスがあるので、fmap
の関数を使用できます。私たちの関数がInt -> Int
のような型を持っていればそれはうまくいくでしょうが、それは実際にはU Int -> Int
です。しかし、もしU Int
をU (U Int)
にすることができたら?その後、fmap sumOfNeighbors
は、私たちが望むものを正確に行うでしょう!
いくつかの初歩レベルのデータ構造を用意してください。私たちは、U Int
を取ると、次のようになりますU (U Int)
を作成しようとしている:
-- not real Haskell. just for illustration
U [leftU u, (leftU . leftU) u, (leftU . leftU . leftU) u..] u [rightU u, (rightU . rightU) u, (rightU . rightU . rightU) u..]
この新しいU (U a)
のこのセンターは、元U a
です。左にスライドすると元のU a
が左にスライドし、同様に右にスライドします。言い換えれば、新しいU (U a)
はここで、我々はそれを行う方法です元U a
のすべての左と右のスライドが含まれています
duplicate :: U a -> U (U a)
duplicate u = U lefts u rights
where lefts = tail $ iterate leftU u
rights = tail $ iterate rightU u
私たちは、私たちが望む機能の書き込みにduplicate
を使用することができます。
extend :: (U a -> b) -> U a -> U b
extend f = fmap f . duplicate
をそれを試してみましょう。
λ extend sumOfNeighbors i1
[-6,-4,-2]0[2,4,6]
このように見えます。これらの関数名、duplicate
とextend
は、少なくとも私が選んだものではありませんでした。これらの関数はComonad
型クラスの一部です。 U
のデータ型用に実装しています。
class Functor w => Comonad w where
extract :: w a -> a
duplicate :: w a -> w (w a)
extend :: (w a -> b) -> w a -> w b
唯一欠けているU
ための簡単ですextract
です:
extract (U _ x _) = x
それは、このクラスがまだあるか便利おそらく明白ではありません。 2次元の場合をどのように扱うかを見てみましょう。私たちはジッパーのジッパーで2次元を行うことができます。すなわち、U (U a)
は左右に動かすと内側のジッパーが移動し、上下に動くと外側のジッパーが移動します。
newtype V a = V { getV :: U (U a) }
instance Functor V where
fmap f = V . (fmap . fmap) f . getV
-- shift the 'outer' zipper
up :: V a -> V a
up = V . leftU . getV
down :: V a -> V a
down = V . rightU . getV
-- shift the 'inner' zippers
left :: V a -> V a
left = V . fmap leftU .getV
right :: V a -> V a
right = V . fmap rightU . getV
はここComonadがV
のために次のようになります。
instance Comonad V where
extract = extract . extract . getV
duplicate = fmap V . V . dup . dup . getV
where dup u = U (lefts u) r (right u)
lefts u = tail $ iterate (fmap leftU) u
rights u = tail $ iterate (fmap rightU) u
extract
機能は非常に簡単です。それはちょうど現在の値をつかむためにジッパーの2つの層を掘るだけです。一方、duplicate
は、ある種のモンスターです。新しいタイプV
を無視すると、タイプはduplicate :: U (U a) -> U (U (U (U a)))
になります。 dup
ヘルパー機能の目的は、U
レイヤーを追加することです。それは2回呼び出されます。次にV
にそれを書き込んでV (U (U a))
にします。次にfmap V
はU (U a)
の結果をV (V a)
にするために内部をラップします。
ところで、extend
がどこにあるのか疑問に思っているのであれば、実際には書き込む必要はありません。上記の定義はデフォルトです。
これはたくさんの作業でしたが、今すぐ元の問題に取り組むことができます。これをチェックしてください。私はあなたのA
とB
値を含むデータ構造を作るつもり、とも私たちは気にしない値、C
だ:
data Token = A | B | C deriving (Eq,Show)
そして、ここでは簡単にV
を構築し、表示を行うためにいくつかのものです。
example = toV C ((A,A,A)
,(B,B,A)
,(A,A,B))
とルールがによって実装されています:
:最後に、我々は
extend
を使用して、すべての値に
rule
を適用することができます
-- move into each neighboring position and get the value in that position
neighbors :: V a -> [a]
neighbors v = fmap (extract . ($ v)) positions
where positions = [ up . left
, up
, up . right
, left
, right
, down . left
, down
, down . right ]
numberOfBs :: V Token -> Int
numberOfBs = length . filter (==B) . neighbors
rule :: V Token -> Token
rule v = case extract v of
C -> C -- C's remain C's forever
_ -> if numberOfBs v >= 2 then B else A
ここ
-- a list of U's containing nothing but x's
filled x = repeat $ U (repeat x) x (repeat x)
type Triple a = (a,a,a)
-- create a U with the middle values a, b, and c, and all the other values the defaulted to d
toU :: a -> Triple a -> U a
toU d (a,b,c) = U (a : repeat d) b (c : repeat d)
-- create a V centered on the 9 given values and default all other values to d
toV :: a -> Triple (Triple a) -> V a
toV d (as, bs, cs) = V (U x y z)
where x = (toU d as) : filled d
y = toU d bs
z = (toU d cs) : filled d
display :: Show a => V a -> [[a]]
display v = fmap g [ [up . left, up, up . right]
, [left, id, right]
, [down . left, down , down . right] ]
where g = fmap (extract . ($ v))
は一例は次のようになります。
transition = extend rule
λ display (transition example)
[[B,B,A],[A,B,B],[B,B,A]]
このルールはちょっと退屈です。すべてがすぐにBになる。
λ take 10 $ fmap display (iterate transition example)
[[[A,A,A],[B,B,A],[A,A,B]],[[B,B,A],[A,B,B],[B,B,A]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,B,B],[B,B,B],[B,B,B]]]
異なるルールを作成するのは簡単です。
rule2 :: V Token -> Token
rule2 v = case extract v of
C -> C
A -> if numberOfBs v >= 2 then B else A
B -> if numberOfBs v >= 4 then A else B
λ take 10 $ fmap display (iterate (extend rule2) example)
[[[A,A,A],[B,B,A],[A,A,B]],[[B,B,A],[B,B,B],[B,B,B]],[[B,A,B],[A,A,A],[B,A,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,A,B],[A,A,A],[B,A,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,A,B],[A,A,A],[B,A,B]],[[B,B,B],[B,B,B],[B,B,B]],[[B,A,B],[A,A,A],[B,A,B]],[[B,B,B],[B,B,B],[B,B,B]]]
クール、右?私が言いたい最後のひとつ。エッジを処理する特別なケースは記述していないことに気がつきましたか?データ構造は無限であるので、値域C
で気にしない範囲のものを埋めて、それを無視して隣人を考える。
単純な答えは、再帰を使用することです。私はこの質問をより良くするために2つのことをお勧めします:1)問題をよりよく定義する(例えば隣人とは何ですか?)2)何かを試してみてください。 – user2297560
あなたは "ワンステップ" 「f」と入力します。何らかの条件に達するまで、 'f 'がこの関数を何度も何度も*使うことができると考えてください。 – Euge
これはComonadにとってまさに完璧な使用例です。初心者にはわかりませんが、これを実装する最も洗練された方法は、[Comonad](https://hackage.haskell.org/package/comonad-5/docs/Control-Comonad.html#v:-61 (各 "セル"とその隣接セルを含むデータ型と共に)インスタンスを生成する。初心者に優しいソリューションは、隣接するセルに基づいて個々のセルを更新するリストの各レイヤー上のマップを使用します。 – Lazersmoke