Haskell:如何简化或消除liftM2?

时间:2010-10-21 15:51:19

标签: list haskell monads

考虑我写的以下代码:

import Control.Monad

increasing :: Integer -> [Integer]
increasing n
    | n == 1    = [1..9]
    | otherwise = do let ps = increasing (n - 1)
                     let last = liftM2 mod ps [10]
                     let next = liftM2 (*) ps [10]
                     alternateEndings next last
    where alternateEndings xs ys = concat $ zipWith alts xs ys
          alts x y = liftM2 (+) [x] [y..9]

其中'increasing n'应返回一个n位数字列表,其数字从左到右增加(或保持不变)。

有没有办法简化这个?在任何地方使用“let”和“liftM2”对我来说都很难看。我想我错过了关于列表monad的重要内容,但我似乎无法摆脱它们。

5 个答案:

答案 0 :(得分:8)

好吧,就liftM函数而言,我首选的方法是使用Control.Applicative中定义的组合器。使用这些,你就可以写last = mod <$> ps <*> [10]。来自ap的{​​{1}}函数执行相同的操作,但我更喜欢中缀版本。

Control.Monad(<$>)是这样的:(<*>)将函数liftM2转换为函数a -> b -> c。普通m a -> m b -> m c仅为liftM,与(a -> b) -> (m a -> m b)fmap相同。

如果您对多参数函数执行此操作会发生什么?它会将(<$>)变成a -> b -> c -> d。这是m a -> m (b -> c -> d)ap进入的地方:他们所做的就是将(<*>)变为m (a -> b)。所以你可以按照你想要的方式为它添加字符串。


那就是说,Travis Brown是正确的,在这种情况下,你似乎并不需要上述任何一种。实际上,您可以大大简化您的功能:例如,m a -> m blast都可以写为映射在同一列表上的单参数函数next和{{ 1}}与pszipWith相同。所有这些映射都可以组合并下推到zip函数中。这使得map成为单参数函数,同时也消除了alts。最后,alts可以与zip合并为concat,如果愿意,也可以与map合并。以下是最终结果:

concatMap

请注意,我从你那里获得的所有重构都是纯粹的语法,只应用对函数结果没有影响的转换。均衡推理和参考透明度很好!

答案 1 :(得分:5)

我认为你要做的是:

increasing :: Integer -> [Integer]
increasing 1 = [1..9]
increasing n = do p <- increasing (n - 1)
                  let last = p `mod` 10
                      next = p * 10
                  alt <- [last .. 9]
                  return $ next + alt

或者,使用“list comprehension”,这只是列表的特殊monad语法:

increasing2 :: Integer -> [Integer]
increasing2 1 = [1..9]
increasing2 n = [next + alt | p <- increasing (n - 1),
                              let last = p `mod` 10
                                  next = p * 10,
                              alt <- [last .. 9]
                ]

列表monad中的想法是您使用“bind”(<-)迭代值列表,let根据您目前所拥有的值计算单个值当前的迭代。当您第二次使用bind时,迭代将从该点开始嵌套。

答案 2 :(得分:3)

当其中一个参数始终是单例列表时,使用liftM2(或<$><*>)看起来很不寻常。为什么不使用map?以下内容与您的代码完全相同:

increasing :: Integer -> [Integer]
increasing n
  | n == 1    = [1..9]
  | otherwise = do let ps = increasing (n - 1)
                   let last = map (flip mod 10) ps
                   let next = map (10 *) ps
                   alternateEndings next last
  where alternateEndings xs ys = concat $ zipWith alts xs ys
        alts x y = map (x +) [y..9]

答案 3 :(得分:3)

以下是我编写代码的方法:

increasing :: Integer -> [Integer]
increasing 1 = [1..9]
increasing n = let allEndings x = map (10*x +) [x `mod` 10 .. 9]
               in concatMap allEndings $ increasing (n - 1)

我按照以下方式获得了此代码。我做的第一件事是使用模式匹配而不是守卫,因为它在这里更清晰。我接下来要做的就是消除liftM2 s。它们在这里是不必要的,因为它们总是被称为一个大小一个列表;在这种情况下,它与调用map相同。因此liftM2 (*) ps [10]仅为map (* 10) ps,其他呼叫网站也是如此。不过,如果您想要liftM2的一般替换,可以使用Control.Applicative的{​​{1}}(仅<$>}和fmap替换<*> 1}}对于任何liftMnn变为liftMn f a b c ... z。它是否更好是品味的问题;我碰巧喜欢它。 1 但在这里,我们可以完全消除它。

我简化原始代码的下一个地方是f <$> a <*> b <*> c <*> ... <*> z。你从来没有真正利用你在do ... - 块中的事实,因此代码可以成为

do

从这里开始,到达我的代码主要涉及将所有let ps = increasing (n - 1) last = map (`mod` 10) ps next = map (* 10) ps in alternateEndings next last 融合在一起。其中一个不是map的剩余电话是map。但由于您实际拥有zipWith,因此您只能同时使用zipWith alts next last10*p,因此我们可以在同一个函数中计算它们。这导致

p `mod` 10

这基本上就是我的代码:let ps = increasing (n - 1) in concat $ map alts ps where alts p = map (10*p +) [y `mod` 10..9] 应始终变为concat $ map ...(顺便说一句,在列表monad中为concatMap),我们只使用=<<一次我们可以将其折叠,我更喜欢pslet


1:从技术上讲,这仅适用于where s,因此,如果您正在使用尚未成为monad的monad,Applicative为{{ 1}}和<$>`liftM`。但是,所有的monad都可以成为适用的仿函数,其中很多都是。

答案 4 :(得分:2)

我认为在单独的参数中传递最后一位数字并使用列表会更清晰。

f a 0 = [[]]
f a n = do x <- [a..9]
           k <- f x (n-1)
           return (x:k)

num = foldl (\x y -> 10*x + y) 0

increasing = map num . f 1