F#中的递归发生了什么?

时间:2018-02-25 01:28:18

标签: recursion f#

我找到了一个f#递归函数的基本示例,它接受一个列表并返回一个只有偶数整数的列表。我在很大程度上理解这一点,但我对此感到困惑。

let numbers = [1..4]
let rec even ls =
   match ls with
   | [] -> []  
   |head :: tail when head % 2 = 0 -> head :: even tail
   |_::tail -> even tail 

匹配头部的线条让我感到困惑。这就是我读它的方式。当头部均匀时追加尾部,然后再次呼叫even tail。因为我们一对一地追加尾巴,不会因为一次又一次地添加头而陷入困境? 此外,最后一行_::tail我假设意味着什么都不做,再次递归",但是 我在f#中查找了运算符_,它说它是一个通配符模式。这实际上意味着如果我的前两场比赛没有覆盖,那么这样做吗?

希望有人能澄清! f#看起来很有趣,但是从强制性的背景来看,它很难理解。

3 个答案:

答案 0 :(得分:5)

这是模式匹配表达式。读取每一行的方法是:箭头->左侧是如何检查数据的说明,箭头右侧是检查结果的说明。

将此应用于您的案例,请按照评论(为了清晰起见,我已插入一些换行符):

match ls with    // Look at `ls`

| [] ->        // when `ls` is an empty list,
    []         // return an empty list

| head :: tail            // when `ls` is a `head` attached to a `tail`, 
    when head % 2 = 0 ->  // AND `head` is an even number,
    head :: even tail     // call `even tail`, attach `head` to it, and return that result

| _::tail ->     // when `ls` is "something" attached to a `tail`,
    even tail    // call `even tail` and return that result

请注意,最后一种情况适用于第二种情况适用的所有情况。这种歧义通过案例的顺序得到解决:程序将依次根据每个案例尝试匹配(“查看”,“检查”)数据,并将执行匹配的第一个案例的代码。

您似乎缺少的另一个微妙点:不变性。该列表是不可变的。您无法“更新”(“更改”,“更改”)列表。如果你有一个列表,它将始终保持这种状态,编译器保证它。相反,数据通过程序演变的方式是基于旧数据创建新数据。特别是,如果您说a :: b,则不会修改列表b,而是创建一个新列表,其中a为首,b为尾。< / p>

这些是F#中非常基本的概念,而你错过它们这一事实告诉我你刚刚开始研究这种语言(也许还有一般的函数式编程)。如果这是真的,我建议先阅读一些介绍性材料,以熟悉基本概念。我最喜欢的是fsharpforfunandprofit.com,我不能推荐它。

答案 1 :(得分:4)

  

head为偶数时,将tail附加到head,然后再次致电even tail。因为我们将head附加到tail,所以不会只是在一次又一次地添加头部的循环中被抓住了吗?

关闭但不完全。它 head添加到递归even tail返回的列表中。每次递归时,此模式匹配表达式中的tail值都会少一个项目。

  

此外,我假设的最后一行_::tail意味着“什么都不做,再次递归”,但我在F#中查找了运算符_,它说它是一个通配符模式。这实际上意味着如果我的前两场比赛没有覆盖,那么这样做吗?

_可以是通配符模式,但在此模式匹配示例中,它只是表示我们在解构列表时不关心 head 值;我们只关心tail。此子句(因为它位于测试均匀性的子句之后)会导致非偶数 head 值被丢弃。

答案 2 :(得分:1)

您正在处理不可变数据类型(不可变列表)。这意味着列表不会更改,每个操作都会创建并返回一个新列表。

模式匹配是一种将数据解构为多个部分的方法。举个例子。如果你有列表[1;2;3;4]并且你写了。

let list = [1;2;3;4]
let (head :: tail) = list

然后head是值1tail列表为[2;3;4]list仍为[1;2;3;4]

让我们一步一步地完成您的示例。

调用

even [1;2;3;4]。然后有三种模式匹配的情况。第一个检查[1;2;3;4]是否为空列表。它并非如此,它会检查下一个。 head :: tail将列表提取为两个部分,如上所述。因此head1tail代表[2;3;4],但when head % 2 = 0添加了条件。它会检查head(当前1)是否可以分为两个。它不是,所以它转到最后的模式匹配。

最后一个是_::tail。首先,它与head::tail完全相同。它提取第一个值1并将其存储在变量_中。使用_作为变量名的原因是为了澄清从未使用过的第一个名称。您也可以将_更改为head,代码的工作方式相同。

最后一次模式匹配匹配,现在您1存储了_[2;3;4]存储了tail。你要做的只是致电even tail。或者在这种情况下,您调用even [2;3;4]并返回此函数调用的结果作为结果。

调用even [2;3;4]时,它会执行相同的操作。

检查它是否为空列表。不是。然后,它将2head中的第一个值[3;4]提取到tail。它检查何时的状况。这一次是真的。现在它执行head :: even tail

或者,如果我们替换值2 :: even [3;4]

::是列表的串联,但在我们可以连接列表之前,首先函数调用even [3;4]需要返回一个列表。所以even [3;4]被调用。

然后再次检查。

  1. [3; 4]是一个空列表。不。
  2. head3是否可以分割2. Nope。
  3. 3提取到_,将[4]提取到tail,然后致电even [4]
  4. even [4]然后做同样的事情:

    1. [4]是否为空列表。不。
    2. 4分配给head的值是偶数吗?是的。所以调用了4 :: even []
    3. even []然后做同样的事情:

      1. []是否为空列表。是的。所以返回空列表。 []
      2. 然后它倒退了。

        -> means "returns"
        
        even []         -> []
        4 :: even []    -> [4]
        even [3;4]      -> [4]
        2 :: even [3;4] -> [2;4]
        even [2;3;4]    -> [2;4]
        even [1;2;3;4]  -> [2;4]