此功能中的惯用Haskell终止递归方式

时间:2016-02-26 17:29:54

标签: haskell recursion functional-programming

停止此函数递归的更多haskellish方法是什么?目前我使用嵌套的if / else,如果下一个组合"溢出"则返回一个空列表。

nextcomb [] []     = []
nextcomb lst maxes | length lst == length maxes = 
let lastel = last lst
in if    lastel < last maxes
   then (init lst) ++ [lastel+1]
   else let higherbit = (nextcomb (init lst) (init maxes)) 
        in if   higherbit == [] 
           then []
           else higherbit ++ [1]
nextcomb lst maxes | otherwise = []

为了澄清,它的作用是需要一个像[1,1,1,1]这样的数字列表并将它增加如下:

[1,1,1,1] - &gt; [1,1,1,2]

...

[1,1,1,9] - &gt; [1,1,2,1]

...

[1,1,9,9] - &gt; [1,2,1,1]

但是,第二个参数是一个列表,指示每列的maxmum值。因此,如果最大值为[2,3],并且初始列表为[1,1],那么进展将是:

[1,1] - &gt; [1,2]

[1,2] - &gt; [1,3]

[1,3] - &gt; [2,1]

[2,1] - &gt; [2,2]

[2,2] - &gt; [2,3]

[2,3] - &gt; []

编辑:&#34; Little Endian&#34; chepner推荐的版本

nextcomb' [] [] = []
nextcomb' lst maxes | length lst /= length maxes = []
nextcomb' lst maxes =
    let firstel = head lst
    in if    firstel < head maxes
       then (firstel+1) : (tail lst)
       else let higherbit = (nextcomb' (tail lst) (tail maxes))
            in if   higherbit == []
               then []
               else 1 : higherbit

4 个答案:

答案 0 :(得分:3)

你应该使非法国家无法代表

因此,不使用两个列表,而是使用元组列表。例如,每个元组中的第一个值可以是最大值,第二个值是实际值。

这也极大地简化了逻辑,因为错误&#34; maxes太长&#34;并且&#34; maxes太短&#34;不可能发生。

答案 1 :(得分:2)

你的if表达式只是隐藏真实的基本情况,即如果 参数为空,则返回空列表。

nextcomb [] [] = []
nextcomb lst maxes | length lst != length maxes = []
nextcomb lst maxes = let lastel = last lst
                     in if    lastel < last maxes
                        then (init lst) ++ [lastel+1]
                        else let higherbit = (nextcomb (init lst) (init maxes)) 
                             in if   higherbit == [] 
                                then []
                                else higherbit ++ [1]

我可能会像这样重写逻辑。 (注意,我离Haskell专家很远,并倾向于回答这些问题作为我自己的练习:)

-- Reversing the arguments and the ultimate return value
-- lets you work with the head of each list, rather than the last
-- element
nextcomb lst maxes = reverse $ nextcomb' (reverse lst) (reverse maxes)
-- The real work. The base case is two empty lists
nextcomb' [] [] = []
-- If one list runs out before the other, it's an error. I think
-- it's faster to check if one argument is empty when the other is not
-- than to check the length of each at each level of recursion.
nextcomb' [] _ = error "maxes too long"
nextcomb' _ [] = error "maxes too short"
-- Otherwise, it's just a matter of handling the least-significant
-- bit correctly. Either abort, increment, or reset and recurse
nextcomb' (x:xs)  (m:ms) | x > m = error "digit too large"
                         | x < m = (x+1):xs  -- just increment
                         | otherwise = 0:(nextcomb' xs ms) -- reset and recurse

(实际上,请注意,如果您在最后一位数后没有递归,nextcomb' [] _将不会触发。您可能会认为过长maxes并不是什么大问题。我留下这个不固定,因为下一部分正确处理它。)

或者,您可以在初始调用中验证长度是否匹配;那么你可以假设它们会同时变空。

nextcomb lst maxes | length lst == length maxes = reverse $ nextcomb' (reverse lst) (reverse maxes)
                   | otherwise = error "length mixmatch"

nextcomb' [] [] = []
nextcomb' (x:xs)  (m:ms) | x > m = error "digit too large"
                         | x < m = (x+1):xs
                         | otherwise = 0:(nextcomb' xs ms)

以下是使用Either报告错误的示例。除了说它进行类型检查和运行之外,我不会保证设计。它与以前的代码没有什么不同;它只使用<$>提升reverse(0:)来处理Either String [a]类型的参数,而不是类型为[a]的参数。

import Control.Applicative
nextcombE lst maxes = reverse <$> nextcombE' (reverse lst) (reverse maxes)

nextcombE' [] [] = Right []
nextcombE' [] _ = Left "maxes too long"
nextcombE' _ [] = Left "maxes too short"
nextcombE' (x:xs) (m:ms) | x > m = Left "digit too large"
                         | x < m = Right ((x+1):xs)
                         | otherwise = (0:) <$> (nextcombE' xs ms)

答案 2 :(得分:2)

请检查下一个实现是否对您有用,因为更多的“haskellish”方式(至少对我而言)是使用内置的递归函数来实现相同的目标

nextcomb [] [] = []
nextcomb lst maxes
  | length lst /= length maxes = []
  | lst == maxes = []
  | otherwise = fst $ foldr f ([],True) $ zip lst maxes
  where
    f (l,m) (acc, mustGrow)
      | mustGrow && l < m  = (l + 1:acc, False)
      | mustGrow = (1:acc, True)
      | otherwise = (l:acc, False)

(编辑)如果需要捕获错误,那么可以试试这个:

nextcomb [] _ = Left "Initial is empty"
nextcomb _ [] = Left "Maximus size are empty"
nextcomb lst maxes
  | length lst /= length maxes = Left "List must be same length"
  | lst == maxes = Left "Initial already reach the limit given by Maximus"
  | otherwise = Right $ fst $ foldr f ([],True) $ zip lst maxes
  where
    f (l,m) (acc, mustGrow)
      | mustGrow && l < m  = (l + 1:acc, False)
      | mustGrow = (1:acc, True)
      | otherwise = (l:acc, False)

答案 3 :(得分:1)

让我们画一幅图!我将做出与初始问题略有不同的假设:

  1. 像chepner建议的小端表示;
  2. 而不是包容性的最大值,我将使用独有的基础来使事情更加类似于随身携带。
  3. 我将使用[0, base)范围内的数字。
  4. 这是图表:

     digits = [d0, d1, ..., dn]
     bases  = [b0, b1, ..., bn]
     --------------------------
     result = [r0, r1, ..., rn]
    

    现在我们可以问:对于结果的每个数字ri,它的值取决于什么?好吧,这些东西:

    1. di
    2. 的值
    3. bi
    4. 的值
    5. 之前的r是否导致了进位
    6. 所以我们可以把它写成一个函数:

      import Control.Monad.State  -- gonna use this a bit later
      
      type Base = Int
      type Digit = Int
      type Carry = Bool
      
      -- | Increment a single digit, given all the contextual information.
      singleDigit' :: Base -> Digit -> Carry -> (Digit, Carry)
      singleDigit' base digit carry = (digit', carry')
          where sum = digit + if carry then 1 else 0
                digit' = if sum < base then sum else sum - base
                carry' = base <= sum
      

      请注意,我注意确保singleDigit'函数的类型以Carry -> (Digit, Carry)结尾。这是因为它符合状态monad典型的state -> (result, state)模式:

      -- | Wrap the `singleDigit'` function into the state monad.
      singleDigit :: Base -> Digit -> State Carry Digit
      singleDigit base digit = state (singleDigit' base digit)
      

      现在我们可以编写以下函数:

      increment :: [Base] -> [Digit] -> [Digit]
      increment bases digits = evalState (sequence steps) True
          where steps :: [State Carry Digit]
                steps = zipWith singleDigit bases digits
      

      我们在这里做的是:

      1. 使用zipWith将基数和数字列表“拼接”在相应的元素上。此列表的元素对应于计算的单个steps
      2. 使用sequence :: [State Carry Digit] -> State Carry [Digit]将所有单个步骤链接到一个通过中间Carry状态的大步骤。
      3. True作为该大步骤的初始Carry输入(导致链增加)。
      4. 示例调用:

        >>> take 20 (iterate (increment [3,4,5,10]) [0,0,0,0])
        [[0,0,0,0],[1,0,0,0],[2,0,0,0]
        ,[0,1,0,0],[1,1,0,0],[2,1,0,0]
        ,[0,2,0,0],[1,2,0,0],[2,2,0,0]
        ,[0,3,0,0],[1,3,0,0],[2,3,0,0]
        ,[0,0,1,0],[1,0,1,0],[2,0,1,0]
        ,[0,1,1,0],[1,1,1,0],[2,1,1,0]
        ,[0,2,1,0],[1,2,1,0]
        ]
        

        我要强调的课程:

        1. 将问题分成小块。不要尝试在一个功能中解决太多问题!在这种情况下,诀窍是将单个数字的解决方案拆分为自己的功能。
        2. 在问题中仔细考虑数据流是非常值得的:问题的每一步都需要哪些信息?在这种情况下,该图有助于推断出导致singleDigit'函数。
        3. 函数式编程的一个重要思想是将计算的“形状”与其“内容”分开。在这种情况下,“内容”是singleDigit操作,计算的“形状” - 如何将各个步骤组合成一个大解决方案 - 由State monad和{{0}}提供。 {1}}操作。
        4. 我没有写一个递归函数;相反,我充分利用了sequencezipWithsequencetake等库函数。你要求一个更惯用的Haskell解决方案,这就是:复杂的递归函数定义不像使用封装常见递归模式的库函数那样惯用。
        5. 这有望鼓励你更多地研究monad。有很多很多问题,如果你用iterate之类的标准monad来表达它们,你可以重用像State这样的通用函数来很容易地解决它们。这是一个很高的学习曲线,但结果是值得的!