递归如何满足基本情况Haskell

时间:2019-03-21 14:23:39

标签: list haskell recursion

我试图理解这段代码,该代码返回传递给它的[a]的所有可能组合:

-- Infinite list of all combinations for a given value domain
allCombinations :: [a] -> [[a]]
allCombinations []     = [[]]
allCombinations values = [] : concatMap (\w -> map (:w) values)
                                        (allCombinations values)

我在这里尝试了此示例输入:

ghci> take 7 (allCombinations [True,False])
[[],[True],[False],[True,True],[False,True],[True,False],[False,False]]

在这里,我似乎无法理解,递归最终将如何停止并返回[ [ ] ],因为allCombinations函数当然没有任何指针在列表中移动,在每次调用中,并在遇到基本情况[ ]时返回[ [ ] ]。据我说,它将无限调用allCombinations函数,并且永远不会停止。还是我想念什么?

另一方面,在通过递归调用完成后返回所有计算之后,take仅返回7中的前final list个元素。那么实际上递归如何满足这里的基本情况?

第二,这里concatMap的目的是什么,在这里我们还可以在这里使用Map函数只是将函数应用于列表,而在内部函数中我们可以排列列表? concatMap在这里实际上是做什么的。根据定义,它concatMap告诉我们它首先映射了函数,然后将列表连接到了我所看到的已经在函数内部进行的列表中?

任何宝贵的意见将不胜感激?

3 个答案:

答案 0 :(得分:8)

简短的回答:它将从不满足基本情况。

但是,不需要。通常,最基本的情况是停止递归,但是在这里您要返回一个无限列表,因此无需停止它。

另一方面,如果您尝试使用allCombination []的多个元素,则此功能会中断-请查看@robin的答案以更好地理解原因。这是您在此处看到基本情况的唯一原因。

主要功能的工作方式是从一个空列表开始,然后在参数列表中的每个元素的开头追加。 (:w)进行递归操作。但是,仅此lambda会返回无限嵌套的列表。即:[],[[True],[False]],[[[True,True],[True,False]等。Concatmap在每个步骤中都删除外部列表,并且由于递归调用,因此最后只返回一个列表列表。这可能是一个复杂的概念,因此请寻找使用concatMap的其他示例,并尝试了解它们的工作原理以及仅凭map的原因是不够的。

这显然仅由于Haskell懒惰的评估而起作用。同样,您知道在foldr中需要传递基本情况,但是当您的函数只接受无限列表时,您可以将undefined作为基本情况来更清楚地说明:不应使用有限列表。例如,可以使用foldr f undefined代替foldr f []

答案 1 :(得分:4)

@Lorenzo已经解释了关键点-递归实际上永远不会结束,因此这会生成一个无限列表,由于Haskell的惰性,您仍然可以从中获取任意数量的元素。但是,我认为提供更多有关此特定功能及其工作方式的详细信息将有所帮助。

首先,定义开头的[] :告诉您,第一个元素总是 []。当然,这是从values的元素中创建0元素列表的唯一方法。列表的其余部分为concatMap (\w -> map (:w) values) (allCombinations values)

concatMap f就像您观察到的组成concat . (map f)一样:它将给定函数应用于列表的每个元素,并将结果连接在一起。在此,函数(\w -> map (:w) values获得一个列表,并生成列表,该列表是通过将values的每个元素添加到该列表之前而给出的。例如,如果values == [1,2],则:

(\w -> map (:w) values) [1,2] == [[1,1,2], [2,1,2]]

如果我们map在列表列表中起作用,例如

[[], [1], [2]]

然后我们得到(仍然将values设为[1,2]):

[[[1], [2]], [[1,1], [2,1]], [[1,2], [2,2]]] 

这当然是列表列表的列表-但是concat的{​​{1}}部分将我们的工作弄平了,使最外层变平,并得到如下列表列表:< / p>

concatMap

我希望您可能已经注意到的一件事是,我开始使用的列表的列表不是任意的。 [[1], [2], [1,1], [2,1], [1,2], [2,2]] 是起始列表[[], [1], [2]]中大小为0或1的所有组合的列表。实际上,这是[1,2]的前三个元素。

回想一下,我们在查看定义时所知道的“肯定”是该列表的第一个元素为allCombinations [1,2]。其余列表为[]。下一步是将其递归部分扩展为concatMap (\w -> map (:w) [1,2]) (allCombinations [1,2])。外层[] : concatMap (\w -> map (:w) [1,2]) (allCombinations [1,2]) 然后可以看到列表所映射的列表的头是concatMap-生成一个列表,列表以[]开头,并继续将[1], [2]然后是1附加到另一个元素-无论它们是什么。但是我们已经看到,接下来的两个元素实际上是2[1]。我们结束了

[2]

({allCombinations [1,2] == [] : [1] : [2] : concatMap (\w -> map (:w) values) [1,2] (tail (allCombinations [1,2])) 在评估过程中没有严格要求,而是通过模式匹配来完成的-我试图通过单词解释,而不是通过等式的显式叠加)。

看着我们知道尾巴是tail。关键是,在流程的每个阶段,我们都可以肯定知道列表的前几个元素是什么-它们恰好都是从[1] : [2] : concatMap ...取值的所有0元素列表,然后是所有具有这些值的1元素列表,然后是所有2元素列表,依此类推。一旦开始,该过程就必须继续,因为传递给values的函数可确保我们只获取从获取到目前为止生成的每个列表并将concatMap的每个元素附加到他们的前面。

如果您对此仍然感到困惑,请查找如何在Haskell中计算斐波那契数。获取所有斐波纳契数的无限列表的经典方法是:

values

fib = 1 : 1 : zipWith (+) fib (tail fib) 示例相比,这更容易理解,但实际上是基于同一件事-纯粹根据自身定义列表,但是使用惰性求值来逐步生成与您一样多的列表根据一个简单的规则。

答案 2 :(得分:4)

这不是 基本情况,而是特殊情况,这不是递归,而是 corecursion * 永不停止

也许下面的重新制定会更容易遵循:

allCombs :: [t] -> [[t]]
--        [1,2] -> [[]] ++ [1:[],2:[]] ++ [1:[1],2:[1],1:[2],2:[2]] ++ ...
allCombs vals = concat . iterate (cons vals) $ [[]]
    where
    cons :: [t] -> [[t]] -> [[t]]
    cons vals combs = concat [ [v : comb | v    <- vals]
                               |           comb <- combs ]

-- iterate   :: (a     -> a    ) -> a     -> [a]
-- cons vals ::  [[t]] -> [[t]]
-- iterate (cons vals)           :: [[t]] -> [[[t]]]
-- concat    ::                              [[ a ]] -> [ a ]
-- concat . iterate (cons vals)                      :: [[t]]

看起来不同,做同样的事情。 * concat是相同的concat,您只需要稍微倾斜一下头就可以看到它。

这也显示了为什么这里需要concat。每个step = cons vals都会产生一批新的组合,每个step应用程序的长度都增加1,而concat将它们全部粘合到一个结果列表中。

每个批次的长度是前一个批次的长度乘以n,其中nvals的长度。这也表明需要对vals == []情况进行特殊处理,即n == 0情况:0*x == 0,因此每个新批次的长度为0,因此尝试获取一个结果带来的更多价值永远不会产生结果,即进入无限循环。据说,该函数在此时变成非生产

顺便说一句,cons

几乎相同
                   == concat [ [v : comb | comb <- combs]
                               |           v    <- vals  ]
                   == liftA2 (:) vals combs

liftA2 :: Applicative f => (a -> b -> r) -> f a -> f b -> f r

因此,如果每个步骤结果的内部顺序对您来说都不重要(但请在帖子底部看到一个重要警告),则可以将其编码为

allCombsA :: [t] -> [[t]]
--         [1,2] -> [[]] ++ [1:[],2:[]] ++ [1:[1],1:[2],2:[1],2:[2]] ++ ...
allCombsA   []   =  [[]]
allCombsA  vals  =  concat . iterate (liftA2 (:) vals) $ [[]]

* 实际上,这是指它的一些修改版本,

allCombsRes vals = res
             where res = [] : concatMap (\w -> map (: w) vals)
                              res
-- or:
allCombsRes vals = fix $ ([] :) . concatMap (\w -> map (: w) vals)
--  where
--  fix g = x where x = g x     -- in Data.Function

或使用伪代码:

 Produce a sequence of values `res` by
      FIRST producing `[]`, AND THEN
      from each produced value `w` in `res`, 
          produce a batch of new values `[v : w | v <- vals]`
          and splice them into the output sequence
               (by using  `concat`)

因此res列表是从其起始点[]开始以核心方式递归生成的,它基于先前的元素(如{{1基于}}的版本,或此处的一对一版本,将通过后向指针输入的结果输入到先前产生的结果中(俗话说,将其输出作为其输入-当然有点欺骗性,因为我们以比生产速度慢的速度走,否则过程将不再是生产性的,如上所述。

但是。有时通过递归调用产生输入,在运行时创建一系列函数,每个函数将其输出沿链向上传递到其调用者,这可能是有利的。尽管如此,数据流还是向上的,与常规递归不同的是,规则递归首先向下向基本情况发展。

刚才提到的优点与内存保留有关。 corecursive iterate好像在它本身正在生成的序列中保留了一个反向指针,因此该序列不能被即时收集。

但是您的原始版本在运行时由隐式创建的流生产者链意味着,它们中的每个 都可以被即时收集为垃圾allCombsRes,每个下游元素,因此整个过程仅相当于n = length vals嵌套循环,每个循环都具有 O(1)空间状态,以生成 i < / i>序列的第i个元素。

这比corecursive / value-recursive k = ceiling $ logBase n i O(n)内存需求要好得多,后者实际上将向后指针保留在的输出中allCombsRes 位置。实际上,对数空间需求最有可能被视为或多或少的 O(1)空间需求。

此优势仅与版本中的生成顺序相同,即与i/n一样,而不是cons vals,而必须回到其输入序列liftA2 (:) vals的开头(因此必须保留combs中每个新的v),因此我们可以肯定地说,问题中的表述非常巧妙。

如果我们要进行无点重新配方-有时可以照亮无点 -是

vals

因此,在使用allCombsY values = _Y $ ([] :) . concatMap (\w -> map (: w) values) where _Y g = g (_Y g) -- no-sharing fixpoint combinator 的公式中更容易理解代码,然后我们仅将fix与语义等效的fix进行切换,以提高效率,得到( )的原始代码。

以上有关空间需求行为are easily tested的声明。我还没有这样做。

另请参阅: