我需要在Haskell中实现嵌套列表操作。
f :: [[String]] -> [[String]]
我的输入是一个二维数组
[ [ ”A” , ”A” , ”A” ]
, [ ”B” , ”B” , ”A” ]
, [ ”A” , ”A” , ”B” ] ]
我随意生成了这个列表。
A A A
B B A
A A B
因此,在我的实现中,我需要执行以下操作。
因此,在第一步之后,我的表格将如下所示。
B B A
A B B
B B A
如果我打算使用C或C ++,我的算法会这样:
复制我的输入。
遍历2for循环中的两个列表,检查是否有语句对位置进行更改,每当我要进行更改时,我将更改第二个列表而不是第一个列表,以便遍历第一个列表不会影响其他“A”和“B”。
返回第二个清单。
问题是,在Haskell中,我无法使用迭代。我该如何解决这个问题?
答案 0 :(得分:3)
正如我在评论中所说,递归是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]]
好的,现在让我们开始做生意。首先是一些管理事项:
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..]
。这些值几乎相同;他们只是换了一个。事实上,我们可以创建将一个转换为另一个的函数。
rightU (U a b (c:cs)) = U (b:a) c cs
leftU (U (a:as) b c) = U as a (b:c)
正如您所看到的,我们只需重新排列元素即可滑动一个U
左侧或右侧。让我们为Show
制作U
个实例,然后验证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
数据结构来做到这一点。假设我们想用其邻居的总和替换每个数字。首先,让我们编写一个函数来计算U
的当前位置的邻居:
sumOfNeighbors :: U Int -> Int
sumOfNeighbors (U (a:_) _ (b:_)) = a + b
只是为了验证它是否有效:
λ 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
唯一缺少的是extract
,这对于U
来说是微不足道的:
extract (U _ x _) = x
可能并不清楚这门课程有多么有用。让我们继续看看如何处理二维案例。我们可以用拉链拉链做二维。也就是说,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
功能相当简单;它只需挖掘两层拉链即可获取当前值。另一方面,duplicate
有点像怪物。忽略新类型V
,其类型为duplicate :: U (U a) -> U (U (U (U a)))
。 dup
辅助函数的目的是添加U
图层。它被调用两次。然后我们将其包含在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
。
-- 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))
以下是示例:
example = toV C ((A,A,A)
,(B,B,A)
,(A,A,B))
规则由以下方式实施:
-- 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
最后,我们可以使用rule
将<{1}}应用于每个值:
extend
这条规则有点无聊。一切都很快成为B&#39。
transition = extend rule
λ display (transition example)
[[B,B,A],[A,B,B],[B,B,A]]
创建不同的规则很容易。
λ 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]]]
来填充我们不关心的范围,并在考虑邻居时忽略它。