我正在阅读本教程http://learnyouahaskell.com/a-fistful-of-monads并偶然发现了这个定义:
type KnightPos = (Int,Int)
moveKnight :: KnightPos -> [KnightPos]
moveKnight (c,r) = do
(c',r') <- [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1)
,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2)
]
guard (c' `elem` [1..8] && r' `elem` [1..8])
return (c',r')
in3 :: KnightPos -> [KnightPos]
in3 start = do
first <- moveKnight start
second <- moveKnight first
moveKnight second
我有一个关于函数in3
的问题(它在棋盘上获取一对坐标(Int,Int)并产生一个字段列表[(Int,Int)]可以从该字段到达3个移动一个骑士)。
是否有可能(如果是这样 - 如何做)将该功能重新制作为inNMoves :: (Num a) => KnightPos -> a -> [KnightPos]
以便它也可以作为参数的移动次数,而不是绑定到3次跳跃?
答案 0 :(得分:9)
由于本练习是关于List Monad ,所以尽量不要考虑你对列表的了解,但要限制自己的monad结构。那就是
move :: Monad m => Pos -> m Pos
也就是说,move
需要一个Pos
,并在某些monadic context Pos
中为您提供约m
项内容。 (在列表的情况下,“上下文”是“任意多重性+排序”。但是尽量不要考虑它。)
另外,我们不要在这里讨论do
,这只是使用(>>=)
的语法糖。出于本说明的目的,您需要知道如何使用(>>=)
转换为表达式。
(>>=)
有签名m a -> (a -> m b) -> m b
。我们需要的实例是m Pos -> (Pos -> m Pos) -> m Pos
。您看到我们已将a
和b
实例化为Pos
。您还可以在此处识别中间部分(Pos -> m Pos)
是move
的签名。因此,使用(>>=)
并将move
作为第二个参数,我们可以创建一个m Pos -> m Pos
类型的函数。
moveM :: Monad m => m Pos -> m Pos
moveM mp = mp >>= move
monad内同胚的组成
很明显,m Pos -> m Pos
可以按照您的意愿顺序执行,因为它是从类型到自身的函数(我认为可以称为 monad endomorphism ,因为type是monad)。
让我们写一个做两个动作的函数。
move2M :: Monad m => m Pos -> m Pos
move2M mp = moveM (moveM (mp))
或者采用无点样式(仅考虑转换,而不是转换后的对象):
move2M :: Monad m => m Pos -> m Pos
move2M = moveM . moveM
对于一般情况(由整数n
参数化的移动数),我们只需要通过函数链运算符moveM
连接一些.
个。因此,如果n为3,我们需要moveM . moveM . moveM
。以下是以编程方式执行此操作的方法:
nmoveM :: Monad m => Int -> m Pos -> m Pos
nmoveM n = foldr1 (.) (replicate n moveM) -- n "moveM"s connected by (.)
这里出现一个问题:移动0次的结果是什么?对于foldr1
&lt; = 0的值,n
未定义。将nmoveM 0
定义为“无所事事”非常有意义。换句话说,身份函数id
。
nmoveM :: Monad m => Int -> m Pos -> m Pos
nmoveM n = foldr (.) id (replicate n moveM)
在这里,我们使用foldr
而不是foldr1
,这需要额外的“基本情况”,id
。
现在我们基本上拥有了我们想要的东西,但是类型签名不适合100%:我们有m Pos -> m Pos
,但我们需要Pos -> m Pos
。这意味着,给定Pos
,我们首先必须将其嵌入上下文m
,然后执行nMoveM
。这个嵌入运算符(我认为它可以称为初始代数)是return
(类型为Monad m => a -> m a
)
nmoves :: Monad m => Int -> Pos -> m Pos
nmoves n = nmoveM n . return
让我们马上写下所有这些,这样你就可以充分欣赏它的光辉。
nmoves :: Monad m => Int -> Pos -> m Pos
nmoves n = foldr (.) id (replicate n move) . return
箭头的组成
使用(>>=)
实际上通常有点不干净,因为它非常不对称:需要m a
和a -> m b
。换句话说,它对转换的对象有点过于关注,而只关注我们案例的转换。这使得组合转换变得不必要地困难。这就是我们必须应对. return
的原因:这是从Pos
到m Pos
的初始转换,因此我们可以自由组合任意数量的m Pos -> m Pos
。
使用(>>=)
会产生以下模式:
ma >>= f_1 >>= f_2 >>= ... >>= f_n
其中ma
是monad,而f_i
是a -> m b
类型的“箭头”(通常a = b)。
有一个更好的变体(>=>)
,它在序列中组合了两个a -> m b
类型箭头,并返回另一个箭头。
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
在这里,我们并没有不必要地关注转换的对象,而只关注转换及其组合。
现在让我们同意move
实际上就是这样的箭头(Pos -> m Pos
)。所以
move >=> move >=> move >=> move >=> move
是仍为Pos -> m Pos
类型的有效表达式。使用(>=>)
时,monad的可组合性质变得更加明显。
我们可以使用nmoves
重写(>=>)
,如下所示:
nmoves :: Monad m => Int -> Pos -> m Pos
nmoves n = foldr1 (>=>) (replicate n move) -- n "move"s connected by >=>
同样,我们使用了foldr1
,我们问“连续0次移动”是什么意思?它必须属于同一类型Pos -> m Pos
,答案为return
。
nmoves :: Monad m => Int -> Pos -> m Pos
nmoves n = foldr (>=>) return (replicate n move)
将此与我们之前在monad内同态世界中nmoves
的定义进行比较:我们现在将箭头与{{1}组合,而不是与(.)
和基本案例id
结合的函数。 }和基本案例(>=>)
。好处是我们不必将给定的return
注入Pos
。
更有意义的取决于您的情况,但m Pos
通常比(>=>)
更清晰。
答案 1 :(得分:4)
使用直接递归:
inNMoves :: KnightPos -> Int -> [KnightPos]
inNMoves start 0 = return start
inNMoves start n = do
first <- moveKnight start
inNMoves first (n - 1)
但正如评论中所述:您可以使用内置函数。例如:
inNMoves start n = (foldl (>>=) . return) start (replicate n moveKnight)
甚至完全无点:
inNMoves = (. flip replicate moveKnight) . foldl (>>=) . return
答案 2 :(得分:3)
请注意,concatMap moveKnight
的类型为[Knight] -> [Knight]
,并会返回从输入位置可到达的位置。
知道了,你可以使用:
iterate (concatMap moveKnight)
生成无限列表的位置集,其中通过使骑士从前一组中的位置移动来获得下一组位置。
例如:
iterate (concatMap moveKnight) [(1,2)]
= [ [(1,2)], -- the initial list
[(3,1),(3,3),(2,4)], -- after one iteration
[(5,2),(1,2),(4,3),(2,3), ... -- after two iterations
...
]
现在in3
可以写成
in3 xs = moves !! 3
where moves = iterate (concatMap moveKnight) xs