固定点:Haskell的懒惰,递归和固定点

2021-06-13 04:29:44

HASKELL为AH HA提供充足的机会!片刻,在其中弄清楚一些功能或功能工作如何解锁一定的新思考如何编写程序。 Ah-HA瞬间的一个很好的例子来自您可以首先开始了解固定点,为什么您可能想要使用它们,以及他们在Haskell工作的究竟是如何工作的。在此帖子中,您将通过Haskell的固定点函数工作,沿途建立几个示例。在帖子的最后,您将在更深入地了解递归以及哈尔克尔的懒惰评估如何改变您对写作计划的方式变化。

如果您已经有了Haskell的一些经验,您可能希望跳过第一个部分并直接跳跃参加修复

递归函数是一个函数,其归还自身。有不同的方式可以完成递归,并且在整个文章中,我们将研究其中的几个。我们将首先定义几个新的术语来帮助我们区分递归的一些特定方面,这将重要的是我们探索的固定点:手动递归,自动递归和递归绑定。在整个文章的整个部分中,我们将花一些时间与这些类型的递归,构建一些示例,并在更好地了解他们如何让我们如何考虑递归的一般性质以及它与固定的方式进行思考要点。

我们将通过思考手动递归来开始调查递归。当您首次学习Haskell时,手动递归可能是您在审视单词递归时所考虑的事情。我们称之为手动递归,因为它发生在程序员时,直接将递归调用回到您当前正在编写的功能。

让我们看一下以直接递归风格编写的递归的经典典范。我们首先撰写阶乘函数。如果您不熟悉因素,则它是一个函数,当给出0时,返回1.当给出大于0时,它会给您提供从1到n乘以所有数字的结果。例如:

为了让自己在下一步中更容易,让我们考虑从我们的输入号码计算的因子函数,而不是向它计数,所以我们可以说:

我们需要做的第一件事就是考虑我们如何采取我们的因子函数并将其分解为越来越小的碎片,具有与我们的整体功能相同的形状。每当我们编写递归函数时,它就有助于通过查看我们如何在越来越小的东西来重新解决问题的过程中。

看看我们如何将因素分成较小的部分的一种方法是注意: 因子5 = 5 * 4 * 3 * 2 * 1因子4 = 4 * 3 * 2 * 1 因子5 = 5 *(4 * 3 * 2 * 1)= 5 *(因子4) 因子4 = 4 *(3 * 2 * 1)因子3 =(2 * 2 * 1)因子4 = 3 *(因子3) 从这些示例开始,您可以开始看到我们正在编写的递归函数的形状,以及我们在每个步骤中解决的子问题如何变小。 在我们编写递归函数之前,我们想要考虑的下一件事就是我们应该停止的时候。 这被称为基本情况。 对于我们的阶乘函数,基本情况是我们可以计算阶乘计算的最小数字,这是0个是由阶乘的定义给我们的。 阶乘::整数 - > 整数因子n = 0 - > 1 n - > n *(因子(n - 1))

与手动递归不同,我们可以通过查找函数调用的代码中的位置来看,在函数调用本身的位置,使用自动递归的功能间接地使用自动管理递归的函数。

您可能熟悉的自动递归的一个示例是折叠函数:foldl和foldr。这两个函数和其他这样的函数允许您处理可以自然地递归地迁移的数据,同时只有必须实现代码来处理当前元素和所需跨电话的任何状态。

我们可以使用像foldr这样的函数来写一个因子,通过让它为我们进行递归来写:

阶乘::整数 - >整数因子n =让handlestep currentnum currentfact = currentnum * constultep 1 [1 ..n]

即使您之前使用过Foldr,也会有所帮助,因为我们构成了自己的问题来构建自己的版本,因此我们可以通过这些递归函数的方式思考。

折扣::(a - > b) - > B - > [a] - > B折叠F累积= [] - > Accum(下一个:休息) - > F next(Foldr F Accum Recr)

查看此功能,您可以看到功能的形状如何,具有案例语句,具有空列表的基本情况以及每次在列表中向前移动一个元素的递归调用。我们的实施方式更通用,可以肯定 - 我们已经更换了因素0是1的知识,其中一般陈述是我们在基本情况下折叠的价值是提供的初始累加器值,现在而不是直接在我们的递归调用中执行乘法,我们将详细信息放在传递中的函数,但如果您眯起了一下,您可以看到两个功能真实的函数。

使用像折叠一样处理数据的形状并处理我们的递归的函数具有许多好处。首先,它删除了一些不必要的代码重复。遍历数据结构并在所有元素上做某事在功能编程中非常常见,并且如果我们每次将其从头从头开始,我们将需要我们更长的时间来编写程序,并且有更多的错误有关错误。其次,它通过让我们的函数“业务逻辑”来使我们的代码有点可读。在大多数情况下,我们的数据表示为列表,二叉树等的事实是偶然的。通过将逻辑分离出来从逻辑处理各个元素以进行遍历数据结构,我们将驻留的代码的相关位居中。最后,也许最重要的是,像折叠一样的功能为我们提供了谈论我们程序结构的共同语言。对于一段时间的人来说,有一段时间的人来说,有些东西“只是折叠的一些数据”可以传达关于如何实现程序的一般想法的大量信息,而无需在太多无线时间内陷入困境细节。

我们将在第一个部分中查看的最终类型的递归不是那么多种特定的技术来递归,因为它是Haskell的一个特征,允许您更轻松地使用手动递归:递归留言。

Haskell的递归允许绑定意味着您可以在函数中使用ex表达式的递归。这是一个简单的例子,继续我们的阶乘示例,这是计算机的函数,即代表的双因子,也就是说输入数字的阶乘:

- 注意:此功能会非常快速增长。 - Dudefactorial 5是199位数字 - DoubleFactorial 8是168187位数字DoubleFactoral :: Integer - >整数duceacherial n =让因子a =案例a为0 - > 1 a - > a *因子(a - 1)在因子(因子n)中

FIX函数,在数据中定义为基本的功能,我们在Haskell中逼近递归的另一种方法。让我们开始看看修复的文档:

修复FIS函数F的最小固定点F,即最小定义的X,使得f x = x。

λ留下Fac n =如果n< = 1,则在fac 5 120中的其他n * fac(n - 1)

这使用了Haskell的允许介绍递归绑定的事实。我们可以使用“修复”来重写此定义,

λ修复(\ rec n - >如果n< = 1那么另外1个n * rec(n - 1))5 123

我们介绍了一个虚拟参数rec;而不是制作递归调用。在修复范围内使用时,此参数然后是指修复的参数,因此重新引入递归。 FIX ::(A - > a) - >一种

每当我们想要了解Haskell中的新事物时,首先是一个好的第一直觉就是首先通过查看这些类型,因为这告诉我们一定关于职能可以的东西,通常更重要的是。

FIX ::(a - > a) - > a告诉我们它会采取函数,并返回一个值。为了讨论,让我们给出我们传递的功能来修复名称g。所以,g :: a - > A和修复G :: a。

乍一看,这可能看起来并不困难。修复只需用一个值调用g来获得一个值返回它可以返回的值。我们可以想象任意数量的类似功能,这些功能将为某些特定类型,int:

同样,我们可以考虑一下我们可以通过的G的任何候选人,并恢复良好的结果:

不幸的是,这依赖于ApplyZero可以选择一些数字来传递的事实。它可以这样做,因为我们知道它正在使用INT值,所以我们可以选择一个int值来传入它。修复没有东西如此简单 - 因为a可能是任何没有值,我们可以选择进入g以恢复值。

如果我们尝试传递一些函数,我们可以看到这个播放,如(+1)进入修复:它永远不会让我们退回一个值,因为它不能。你可以在ghci中自己尝试。当您满意不会恢复值时,可以键入Control + C以取消当前功能。

修复的诀窍是,有时,它可以回馈结果。当您退回的最终值时,它可以做到这一点不依赖于任何特定的输入值。例如,如果我们使用const函数,它将忽略传递到它的任何参数并刚返回值,那么我们可以从修复中获取结果:

忽略它可能有效的问题,有意义。函数的定义是它是一个值,当传递到函数时,会导致函数返回相同的值。这正是Const - 忽略其输入并返回一些值:

λ:t const const :: a - > B - >一个λ:t const" foo" const" foo" :: b - > [char]λf= const" foo" λf1" foo"

这意味着,无论我们传递到const的任何值都将是它返回的函数的固定点。

在数学定义之外的一个定点,如果我们在懒惰和可计算性方面考虑它,修复的行为也有意义。我们已经注意到,因为修复是多态,因此固定本身不能得到一个值来传递到它试图找到固定点的功能。在严格评估的语言中,这将是一个问题,但由于Haskell的懒惰,“我们无法实际计算的价值”仍然是我们可以使用的东西。

在修复的情况下,它传入其函数的参数可能是我们实际上无法计算的值,但它转变为实际完全完全没问题,只要我们永远不会尝试计算它。换句话说,如果我们通过的函数在其参数中懒惰,那么我们永远不会尝试运行创建值的不可能的计算,因此一切都效果。

既然你了解解决方法如何利用懒惰的工作,还有另一个方面来解决,可能会通过文档读取阅读。回想一下,修复的类型是FIX ::(A - > a) - >一个,但文档在阶乘函数中传递了两个参数的过程:

λ修复(\ Rec n - >如果n< = 1那么1 ele n * rec(n - 1))5 120

我们可以从这个例子中提供函数并给它一个名字,并确认它实际上是两个参数:rec,一个类型(p - > p)的函数,n,p类型的值。

λfacial= \ rec n - >如果是n< = 1那么1 ever n * rem(n - 1)λ:t因子势:(ord p,num p)=> (p - > p) - > p - > P.

因此,我们谈论它有点易于,让我们选择一些特定的类型,因为我们正在考虑这个。因为没有特别原因,让我们使用int,所以我们可以让阶乘有类型:

我们需要记住有两件事能够汇总这项工作。首先,它有时对我们来说有时会对我们停止并记住Haskell函数是核发的,并通过我们的类型签名在我们看看时意味着什么。

我们可以自然地阅读类型(Int - > int) - > int - >作为一个函数,从int到int,返回int的函数。大多数时候我们可以通过这种方式阅读我们的功能类型时,我们可以得到很好的时间,但每次偶尔都会向我们抛出循环。

因子::(int - > int) - > int - > INT因子= \ rec n - >如果是n< = 1,那么另外1个n * rec(n - 1)

因子::(int - > int) - > int - > INT因子= \ rec-> \ n - >如果是n< = 1,那么另外1个n * rec(n - 1)

当我们将其重写时,我们可以自然地希望描述函数:一种函数,它从int到int函数和int返回int到int的函数。我们可以重写我们的类型签名,以反映我们的功能的此重置,以便它读取:

因子::(int - > int) - > (int - > int)因子= \ rec-> \ n - >如果是n< = 1,那么另外1个n * rec(n - 1)

现在正在查看这种重写类型的签名,我们可以开始看到我们需要牢记的第二重要。当我们处理采取A的多态函数时,A可以是任何东西,包括函数。如果我们用(int - > int)替换类型参数,那么修复程序的类型将成为:

修复@(int - > int)::((int - > int) - > int - > int) - > int - > ㈡

在下一部分中,我们将看看如何实际实现修复。一旦你有机会看到实施,我们就会回到两种类型的修复类型以及它如何与懒惰一起使用,并将所有知识共同融入更具凝聚力的理解,以更具凝聚力的理解。

对于关于解决方法的所有讨论,其实现非常短。每当我们发现自己在Haskell完全未知的东西时,我们都可以通过查看类型来开始,并且再次读取下一步是读取源代码。 [修复的源代码](https://hackage.haskell.org/package/base-4.15.0.0/docs/src/data -function.html#fix可在黑客上找到,它非常短:

FIX ::(A - > a) - > FIX F =设定x x = f x

让我们走过这里发生的事情,看看我们是否可以掌握它。我们从一个参数f开始,f,这是我们想要找到固定点的任何功能。

接下来,我们创建一个递归,让我们将X定义为x,以将f到x应用于x。此递归Let绑定是固定点计算工作原理背后的魔力。

当我们第一次调用修复并创建Let绑定我们定义X时,我们知道它必须具有A类型的A,并且在需要时,将由表达式f x计算。

同样,该计算中的X也不是值。这是一个thek,如果被评估,将通过调用f x来计算。换句话说,我们从:

如果尊重此函数的人决定他们需要x的值,那么他们会得到:

如果F是常量返回值的函数,而不会看出其输入值,则x将设置为该值,并且可以在没有任何问题的情况下进行评估。

另一方面,如果f确实需要评估x,就像我们尝试过(+1)时,我们将结束一个永远无法完成的计算,因为每次我们尝试查看x会回到另一层一些未评估的。在表面上,这似乎有点有限。毕竟,如果我们需要通过始终返回值的函数并从不查看其输入,我们只能限于Const的排列,而不是其他方式,除非我们可以从其他地方获得一些数据......

FIX功能不需要一个永不评估其参数的函数,以便最终向我们提供值。相反,我们需要给它一个最终不评估其参数的函数。这里的单词差异从不且最终是终止的计算与定义良好的差异,并且一个未定义的计算。这是将两个参数的函数转换为修复的地方。当我们有一个函数(int - > int)时,除了我们给出何时终止的输入值之外,没有选项,所以我们总是必须评估它。另一方面,具有类型(int - > int)的功能 - > (int - > int)具有更大的灵活性。要了解如何,让我们回到我们对因子的定义:

因子::(int - > int) - > (int - > int)因子rec = \ n - >如果是n< = 1,那么另外1个n * rec(n - 1)

在这个因素函数中,我们正在参加参数,Rec :: Int - > int,但我们只会评估它如果n大于1.由于n次数随着每个步骤减少,我们都知道它最终将达到1(假设我们开始使用正数),因此我们知道REC最终不会是评估,我们可以恢复良好的价值。

当我们深入看待这一点,我们可以看到这实际上是一个非常有趣的方法 - 我们正在利用懒惰,以便我们可以返回一个函数,只会在输入到返回的函数时要评估其关闭中的值足够高。这几乎就像我们通过时间向后传递信息,但实际上我们只是利用了懒惰评估的行为和呼叫堆栈来传播信息,最终解析一些已经耐心等待的Thunks允许计算它们。

作为最后的练习,让我们一步一步地走过一个例子,让我们更好地了解我们使用修复时发生的事情。

因子' ::(int - > int) - > (int - > int)因子' Rec = \ n - >如果n< = 1那么其他n * reb(n - 1)阶乘:: int - > INT因子=修复因子'

修复(\ rec n - >如果n< = 1那么另外1个n * rec(n - 1))5

让x =(\ rec n - >如果n< = 1那么在x 5中的其他1个rec(n - 1))x

如果我们将此函数应用于5,并用5替换n,我们最终结束:

让x =(\ rec 4 - >如果5< = 1那么1在x 5中的其他rec(5 - 1))x

让x =(\ rec 5 - >如果5< = 1那么另外1个5 * rec(5 - 1))$(\ rec' 4 - >如果4< = 1那么有1个* Rec'(4 - 1)))$(\ Rec'' 3 - >如果3< = 1那么1其他3 * Rec''(3 - 1) ))$(\ rec''' 2 - >如果2< = 1那么2 * rec'''(2 - 1)) )$(\ _REC 1 - >如果1< = 1那么1 e { - - 从不评估# - - })x $ 5

一旦我们终于达到了n == 1的情况,我们停止评估rec,我们可以开始以相反的顺序解决的呼叫堆栈,所以'' 变成1,我们得到: 让x =(\ rec 5 - >如果5< = 1那么另外1个5 * rec(5 - 1))$(\ rec' 4 - >如果4< = 1那么有1个* Rec'(4 - 1)))$(\ Rec'' 3 - >如果3< = 1那么1其他3 * Rec''(3 - 1) ))$(\ rec''' 2 - >如果2& ......