Haskell递归基础

时间:2018-11-27 02:11:38

标签: haskell recursion

我不确定我是否应该在这个平台上问更一般的,非特定的问题,但是我对编写Haskell和一般的代码是陌生的,并且深入的解释将不胜感激。我已经非常习惯于使用其他语言中的循环系统的典型方法,但是由于Haskell的变量是不可变的,因此我发现递归的确很难解决。 Haskell Wikibook中的一些示例包括:

length xs = go 0 xs
    where
    go acc []     = acc
    go acc (_:xs) = go (acc + 1) xs

zip []     _      = []
zip _      []     = []
zip (x:xs) (y:ys) = (x,y) : zip xs ys

[]     !! _ = error "Index too large" -- An empty list has no elements.
(x:_)  !! 0 = x
(x:xs) !! n = xs !! (n-1)

第一个是不言自明的,只是从头开始为字符串编写一个长度函数。第二个类似于索引搜索,该索引在指定点返回一个char,第三个我想是将列表转置在一起的方法。 尽管多少知道这些代码是做什么的,但我仍然难以理解它们的功能。我们将非常感谢您对这些事情的实际处理过程进行任何一步一步的分析。

编辑:谢谢大家的回答!我还没有彻底研究所有这些信息,但是在阅读了一些内容之后,这正是我所需要的信息。我现在没有太多时间来练习,即将进入决赛,但在休息期间,我决定在递归上再做一次尝试:

ood x 
    |rem x 2 == 1 = ood (x-1)
    |x <= 0 = _
    |otherwise = ood (x-2)

我想尝试制作一个小的函数,该函数打印从x到1的所有奇数。它只打印1。我相信它确实击中了每个奇数,只是没有间歇地显示答案。如果你们中的任何人可以尝试编写代码并向我展示如何创建成功的递归函数,那将对我有很大帮助!

6 个答案:

答案 0 :(得分:4)

让我们看看如何构造其中的两个。

zip

我们将从zip开始。 zip的目的是将两个列表“压缩”到一个列表中。该名称来自将拉链的两个侧面拉在一起的类比。这是它如何工作的示例:

zip [1,2,3] ["a", "b", "c"]
  = [(1,"a"), (2,"b"), (3,"c")]

zip类型签名(通常是您要写的第一件事)是

zip :: [a] -> [b] -> [(a, b)]

也就是说,它需要一个a类型的元素列表和一个b类型的元素列表,并生成一个具有每种类型的一个分量的成对列表。

要构建此功能,让我们进行标准的Haskell模式匹配。我们有四种情况:

  1. 第一个列表为[],第二个列表为[]
  2. 第一个列表为[],第二个列表为弊(使用:构造)。
  3. 第一个列表是一个缺点,第二个列表是[]
  4. 第一个列表是一个缺点,第二个列表也是一个缺点。

让我们一起解决这些问题。

zip [] [] = ?

如果将两个空列表压缩在一起,则没有可使用的元素,因此可以肯定地得到了空列表。

zip [] [] = []

在下一种情况下,我们有

zip [] (y : ys) = ?

我们有一个y类型的元素b,但是没有与之配对的a类型的元素。因此,我们只能构建空列表。

zip [] (y : ys) = []

在其他不对称情况下也会发生同样的情况:

zip (x : xs) [] = []

现在我们来看两个有趣的有趣情况:

zip (x : xs) (y : ys) = ?

我们有正确类型的元素,因此我们可以成对(x, y),类型为(a, b)的一对。这就是结果的重点。结果的尾巴是什么?好吧,这是将两条尾巴拉在一起的结果。

zip (x : xs) (y : ys) = (x, y) : zip xs ys

将所有这些放在一起,我们得到

zip [] [] = []
zip [] (y : ys) = []
zip (x : xs) [] = []
zip (x : xs) (y : ys) = (x, y) : zip xs ys

但是您给出的实现只有三种情况!怎么样?看一下前两种情况的共同点:第一个列表为空。您可以看到何时第一个列表为空,结果为空。因此,您可以结合以下几种情况:

zip [] _ = []
zip (x : xs) [] = []
zip (x : xs) (y : ys) = (x, y) : zip xs ys

现在看看第二种情况。我们已经知道第一个列表是一个弊端(因为否则我们将采用第一种情况),并且我们不需要进一步了解其组成,因此可以将其替换为通配符:

zip [] _ = []
zip _ [] = []
zip (x : xs) (y : ys) = (x, y) : zip xs ys

这将产生您复制的zip实现。现在事实证明,有一种不同方式可以组合我认为更清楚地说明自己的模式。重新排列四个模式,如下所示:

zip (x : xs) (y : ys) = (x, y) : zip xs ys
zip [] [] = []
zip [] (y : ys) = []
zip (x : xs) [] = []

现在您可以看到第一个模式产生了一个弊端,而所有其余的都产生了空列表。因此,您可以折叠其余所有三个零件,从而产生紧凑的

zip (x : xs) (y : ys) = (x, y) : zip xs ys
zip _ _ = []

这解释了当两个列表都相同时会发生什么,而当情况并非如此时会发生什么。

length

实施length的幼稚方式非常直接:

length :: [a] -> Int
length [] = 0
length (_ : xs) = 1 + length xs

这将为您提供正确的答案,但是效率低下。在评估递归调用时,实现需要跟踪以下事实:完成后,需要在结果中加1。实际上,它可能将1+推送到某种堆栈上,进行递归调用,弹出堆栈,然后执行加法操作。如果列表的长度为n,则堆栈的大小将为n。这对效率不是很好。您复制的代码有些模糊的解决方案是改写一个更通用的函数。

-- | A number plus the length of a list
--
-- > lengthPlus n xs = n + length xs
lengthPlus :: Int -> [a] -> Int
-- n plus the length of an empty list
-- is n.
lengthPlus n [] = n
lengthPlus n (_ : xs) = ?

好吧

 lengthPlus n (x : xs)
 = -- the defining property of `lengthPlus`
 n + length (x : xs)
 = -- the naive definition of length
 n + (1 + length xs)
 = -- the associative law of addition
 (n + 1) + length xs
 = -- the defining property of lengthPlus, applied recursively
 lengthPlus (n + 1) xs

所以我们得到

lengthPlus n [] = n
lengthPlus n (_ : xs) = lengthPlus (n + 1) xs

现在,该实现可以在每个递归调用上增加counter参数,而不是将其延迟到之后。好吧...差不多。

由于Haskell的按需调用语义,因此不能保证此方法可以在恒定内存中运行。假设我们打电话

lengthPlus 0 ["a","b"]

这可以减少到第二种情况:

lengthPlus (0 + 1) ["b"]

但是我们实际上并没有要求总和的值。因此,实现可能会延迟该添加工作,从而创建一系列延迟,这与之前看到的堆栈一样糟糕!实际上,编译器足够聪明,可以在启用优化后找出正确的方法。但是,如果您不想依靠它,可以给它一个提示:

lengthPlus n [] = n
lengthPlus n (_ : xs) = n `seq` lengthPlus (n + 1) xs

这告诉编译器实际上必须对整数参数求值。只要编译器不是故意变钝的,它将确保首先对其进行评估,以清除所有延迟添加的内容。

答案 1 :(得分:2)

我不确定您对哪一部分感到困惑。也许您只是在想这个?让我们慢慢走过zip

出于参数的考虑,假设我们要执行zip [1, 2, 3] ['A', 'B', 'C']。我们该怎么办?

  • 我们有zip [1, 2, 3] ['A', 'B', 'C']。现在怎么办?
  • zip定义的第一行(“等式”)说

    zip [] _ = []
    

我们的第一个参数是否为空列表?不,是[1, 2, 3]。好,所以跳过这个方程式。

  • zip的第二个等式

    zip _ [] = []
    

我们的第二个参数是否为空列表?不,是['A', 'B', 'C']。所以也不要理会这个方程。

  • 最后一个等式

    zip (x:xs) (y:ys) = (x, y) : zip xs ys
    

我们的第一个参数是非空列表吗?是!是[1, 2, 3]。因此,第一个元素变为x,其余元素变为xsx = 1xs = [2, 3]

我们的第二个参数是非空列表吗?同样,是的:y = 'A'ys = ['B', 'C']

好的,我们现在该怎么办?好吧,右手边的尺码怎么说。如果我再加上一些括号,则右侧基本上会说

(x, y) : (zip xs ys)

因此,我们正在构建一个新列表,该列表以(x, y)(一个2元组)开头,并以zip xs ys开头。所以我们的输出是(1, 'A') : ???

???部分是什么?好吧,就像我们执行了zip [2, 3] ['B', 'C']。回到顶部,以与以前相同的方式再次浏览。您会发现它输出(2, 'B') : ???

现在,我们从(1, 'A') : ???开始。如果我们用刚得到的东西代替它,那么我们现在有了(1, 'A') : (2, 'B') : ???

更进一步,我们有了(1, 'A') : (2, 'B') : (3, 'C') : ??????部分现在是zip [] []。应该清楚的是,第一个方程表示这是[],所以我们的最终结果是

(1, 'A') : (2, 'B') : (3, 'C') : []

也可以写为

[(1, 'A'), (2, 'B'), (3, 'C')]

您可能已经知道答案就是最终的答案。我希望您现在能看到我们如何得到答案。

如果您了解zip这三个方程式在每个步骤中所做的工作,我们可以总结如下过程:

zip [1, 2, 3] ['A', 'B', 'C']
(1, 'A') : (zip [2, 3] ['B', 'C'])
(1, 'A') : (2, 'B') : (zip [3] ['C'])
(1, 'A') : (2, 'B') : (3, 'C') : (zip [] [])
(1, 'A') : (2, 'B') : (3, 'C') : []

如果您仍然感到困惑,请尝试将手指完全放在混淆您的地方。 (是的,说起来容易做起来难……)

答案 2 :(得分:2)

递归的关键是不再担心您的语言如何为递归提供支持。您真的只需要知道三件事,我将以zip为例来演示这些事情。

  1. 如何解决基本情况

    基本情况是当一个列表为空时压缩两个列表。在这种情况下,我们只返回一个空列表。

     zip _ [] = []
     zip [] _ = []
    
  2. 如何将一个问题分解为一个(或多个)简单问题。

    非空列表可以分为两部分,头和尾。头部是一个元素。尾部是(子)列表。要压缩两个列表,我们使用(,)将两个头“压缩”在一起,然后将两个尾部压缩在一起。由于尾部都是列表,所以我们已经有一种将它们压缩在一起的方法:使用zip

    (正如我的前任教授所说,“请相信您的递归”。)

    您可能会反对,因为我们尚未完成对它的定义,所以我们不能称其为zip。但是我们还没有称呼它;我们只是说在将来的某个时刻,当我们调用此函数时,名称zip将绑定到一个将两个列表压缩在一起的函数,因此我们将使用它。

       zip (x:xs) (y:ys) = let h = (x,y)
                               t = zip xs ys
                           in ...
    
  3. 如何将各个部分放回原处。

    zip需要返回一个列表,我们的新列表的头h和尾t。要将它们放在一起,只需使用(:)

        zip (x:xs) (y:ys) = let h = (x,y)
                                t = zip xs ys
                            in h : t
    

    或更简单地说,zip (x:xs) (y:ys) = (x,y) : zip xs ys


在解释递归时,通常最简单的方法是从基本案例开始。但是,如果您可以先编写递归案例,Haskell代码有时会更简单,因为它可以让我们简单地了解基本案例。

zip (x:xs) (y:ys) = (x,y) : zip xs ys
zip _ _ = []  -- If the first pattern match failed, at least one input is empty

答案 3 :(得分:1)

循环是函数调用,是循环。使用更新的循环参数重新输入循环主体与使用更新的函数参数在新的递归调用中重新输入函数主体相同。换句话说,函数调用是goto,函数名称是跳转到的标签:

    loop_label: 
         do stuff updating a, b, c,
         go loop_label

    loop a b c = 
        let a2 = {- .... a ... b ... c ... -}
            b2 = {- .... a ... b ... c ... -}
            c2 = {- .... a ... b ... c ... -}
        in
           loop a2 b2 c2

您确实说过对循环感到满意。


让我们用报告中定义的更原始的结构case来翻译示例函数:

length xs = go 0 xs
    where
    go a b = case (a , b) of
               ( acc , []       )   ->      acc
               ( acc , (_ : xs) )   ->  go (acc + 1) xs

所以它与以前的普通线性递归相同。

其他两个定义相同:

zip a b = case ( a  , b  ) of
            (  []   , _     )  ->  []
            (  _    , []    )  ->  []
            (x : xs , y : ys)  ->  (x,y) : zip xs ys

(最后一个练习)。

答案 4 :(得分:1)

再往前走,让我们介绍一下您唯一需要的递归函数:

fix :: (a -> a) -> a
fix f = f (fix f)

fix计算其自变量的不动点。 函数的不动点是应用该函数时取回不动点的值。例如,平方函数square x = x**2的不动点是1,因为square 1 == 1*1 == 1

fix看起来并不是很有用,因为它似乎陷入了无限循环:

fix f = f (fix f) = f (f (fix f)) = f (f (f (fix f))) = ...

但是,正如我们将看到的,懒惰使我们能够利用对f的无限调用。


好的,我们实际上如何利用fix?考虑以下zip的非递归版本:

zip' :: ([a] -> [b] -> [(a,b)]) -> [a] -> [b] -> [(a,b)]
zip' f (x:xs) (y:ys) = (x,y) : f xs ys
zip' _ _ _ = []

给出两个非空列表,zip'使用接收到的帮助功能f将它们的尾部压缩,将它们压缩在一起。如果任何一个输入列表为空,它将忽略f并返回一个空列表。基本上,我们把辛苦的工作留给了叫zip'的人。我们相信他们会提供适当的f

但是我们怎么称呼{em> zip'?我们可以通过什么论点?这就是fix的来源。再次查看zip'的类型,但这一次进行替换t ~ [a] -> [b] -> [(a,b)]

zip' :: ([a] -> [b] -> [(a,b)]) -> [a] -> [b] -> [(a,b)]
     ::           t             ->          t

嘿,这就是fix所期望的类型! fix zip'是什么类型?

> :t fix zip'
fix zip' :: [a] -> [b] -> [(a, b)]

符合预期。那么,如果我们通过zip'自己的固定点会怎样?我们应该得到……定点,即fix zip'zip' (fix zip')应该是相同的函数。我们仍然还真的不知道zip' 的固定点是什么,但只是踢一下,如果我们尝试调用它会发生什么?

> (fix zip') [1,2] ['a','b']
[(1,'a'),(2,'b')]

看起来我们刚刚找到了zip的定义!但是如何?让我们使用方程式推理找出刚刚发生的事情。

(fix zip') [1,2] ['a','b']
  == (zip' (fix zip')) [1,2] ['a','b']  -- def'n of fix
  == (1,'a') : (fix zip') [2] ['b']     -- def'n of zip'
  == (1,'a') : (zip' (fix zip')) [2] ['b'] -- def'n of fix, but in the other direction
  == (1,'a') : ((2,'b') : (fix zip') [] []) -- def'n of zip'
  == (1,'a') : ((2,'b') : zip' (fix zip') [] []) -- def'n of fix
  == (1,'a') : ((2,'b') : [])  -- def'n of zip'

因为Haskell很懒,所以对zip'的最后一次调用不需要求值fix zip',因为从不使用它的值。因此,fix f不需要 终止;它只需要按需提供另一个对f的呼叫。

最后,我们看到递归函数zip只是非递归函数zip'的固定点:

fix f = f (fix f)
zip' f (x:xs) (y:ys) = (x,y) : f xs ys
zip' _ _ _ = []
zip = fix zip'

让我们简要地使用fix来定义length(!!)

length xs = fix go' 0 xs
  where go' _ acc []     = acc
        go' f acc (_:xs) = f (acc + 1) xs

xs !! n = fix (!!!) xs n
  where (!!!) _ [] _ = error "Too big"
        (!!!) _ (x:_) 0 = x
        (!!!) f (x:xs) n = f xs (n-1)

通常,递归函数只是合适的非递归函数的固定点。请注意,尽管并非所有函数都有固定点。考虑

incr x = x + 1

如果您尝试调用其固定点,则会得到

(fix incr) 1 = (incr (fix incr)) 1
             = (incr (incr (fix incr))) 1
             = ...

由于incr 总是需要其第一个参数,因此计算其固定点的尝试总是发散。很明显incr没有定点,因为没有x的数字x == x + 1

答案 5 :(得分:1)

这是一个很好的技巧,可以显示如何将常规命令式循环转换为递归。步骤如下:

  1. 通过不更改对象(例如,不x.y = z,仅x = x { y = z })使数据不可变
  2. 通过将所有变量更改都移到控制流之前,使变量“几乎不变”。
  3. 更改为“ goto形式”
  4. 计算出一组变异变量
  5. 添加“变量更改”以突变每次goto不变的变量
  6. 用函数替换标签,并用函数(尾部)调用替换goto

这是第1步之后但其他任何步骤之前的简单示例(组成语法)

let sumOfList f list =
  total = 0
  done = False
  while (not done) {
    case list of
    [] -> done = True
    (x : xs) -> 
      list = xs
      total = total + (f x)
  }
  total

除了更改变量外,这实际上并没有做其他事情,但是我们可以对步骤2做一件事:

let sumOfList f list =
  total = 0
  done = False
  while (not done) {
    case list of
    [] -> done = True
    (x : xs) -> 
      let y = f x in
      list = xs
      total = total + y
  }
  total

第3步:

let sumOfList f list =
  total = 0
  done = False
  loop:
    if not done then goto body else goto finish
  body:
    case list of
    [] -> 
      done = True
      goto loop
    (x : xs) -> 
      let y = f x in
      list = xs
      total = total + y
      goto loop
  finish:
    total

步骤4:变异变量为donelisttotal

第5步:

let sumOfList f list =
  done = False
  list = list
  total = 0
  goto loop
  loop:
    if not done then 
      total = total
      done = done
      list = list
      goto body
    else
      total = total
      done = done
      list = list
      goto finish
  body:
    case list of
    [] -> 
      done = True
      total = total
      list = list
      goto loop
    (x : xs) -> 
      let y = f x in
      done = done 
      total = total + y
      list = xs
      goto loop
  finish:
    total

第6步:

let sumOfList f list = loop False list 0 where
  loop done list total =
    if not done
    then body done list total
    else finish done list total
 body done list total =
   case list of
   [] -> loop True list total
   (x : xs) -> let y = f x in loop done list (total + y)
 finish done list total = total

我们现在可以通过删除一些未使用的参数来清理问题:

let sumOfList f list = loop False list 0 where
  loop done list total =
    if not done
    then body done list total
    else finish total
 body done list total =
   case list of
   [] -> loop True list total
   (x : xs) -> let y = f x in loop done list (total + y)
 finish total = total

意识到body中的完成总是False并内联loopfinish

let sumOfList f list = body list 0 where
 body list total =
   case list of
   [] -> total
   (x : xs) -> let y = f x in body list (total + y)

现在我们可以将case拉入多个函数定义:

let sumOfList f list = body list 0 where
 body [] total = total
 body (x : xs) total =
   let y = f x in body list (total + y)

现在内联y的定义,并给body一个更好的名称:

let sumOfList f list = go list 0 where
 go [] total = total
 go (x : xs) total = go list (total + f y)