本章的主要内容来自Paul Chiusano的“ Scala中的功能编程”。变形,高级别类型,模式匹配和其他内容。
关于高级别函数的概念,诸如Corrihua,纯函数,非Pure函数,副作用等函数,我在上一篇文章中介绍了它,并且在这里不会详细介绍。
功能编程(FP”)是基于纯函数构建的,纯函数是副作用。结果,我们至少获得了最重要的好处:该程序的操作环境不会影响计算过程,因此它不会影响计算结果。最简单的纯函数示例是数学的(+)函数。代码中出现的任何公式都可以简单地使用其结果。
上面的示例描述了纯函数的参考透明度特征:纯函数的所有调用都可以替换为其计算结果。此限制使衍生程序的值更简单和自然。我们称其为替代模型:计算结果就像简单地求解科学问题的数量,并不断更改等效替代的参数。解释的例子:
第一个是Java和Scala中的字符串字符串。它是不可分割的数据结构。所有更改只会产生新的结果而不会改变自身:
显然,无论您在哪里打电话,此结果始终保持不变。因此,我们称其为透明的表达方式。在任何呼叫代码中,我们都可以使用其结果来替换它。
第二个反面示例是方法。此方法将自身更改,这意味着每次调用该方法时,其先前的状态都会被破坏:
显然,此功能破坏了参考透明度的原理。在呼叫之前,状态的原始状态已更改。尽管它们描述了相同的表达,它们的结果不同,并且副作用使程序行为的推理更加困难。
相反,代数模型更容易推理,因为我们不必担心执行函数之前和之后的环境变化。
FP的另一个优点是使程序更模块化。A系统由多个模块组成。这些模块应独立理解并支持它。在FP的世界中,系统的功能仅取决于模块本身的功能和序列,并且与任何其他因素无关。例如,我们可以使用表达任何复杂表达的“加法,减法和乘法”的任何组合。相似,函数可以在函数之间组合,并最终形成强大的计算系统。
作者强烈建议您首先阅读本文:有趣的Scala语言 - 递归地思考。
大多数抽象逻辑可以简单地在递归思维中表达。进行快速排序为例,您只需要在一个小段落中描述它即可汇总:在序列中选择一个基准点,然后在之前和之后进行两个数组:正面的所有元素都小于这个基准点,其次是所有基准的基准,这些基准比该基准都要大。点元素。为了使列表整体列表,在两个子序列中反复进行递归的过程一直持续到基准标准直到基准测试在特定时间选择基准点之前和之后不再需要的序列。
得益于Scala提供的模式匹配,仅需要一行代码:
如果使用或精心解释了程序的每个步骤,则功能编程的递归只需要清除两件事:该做什么以及何时何时进行边界条件)。感兴趣的学生也可以转到上述文章阅读“变化的变化”,并认为更复杂的问题是通过递归巧妙地解决的。
拥抱递归似乎是一切。但是这里有一个新问题在这里无法忽略:每次调用功能时,都需要创建一个新的堆栈框架。对于深层递归电话,这有堆叠空间的风险。在回答此问题时,作者应介绍相关的呼叫和递归概念:
假设有两个函数,如果该函数最终返回正确的呼叫,而没有任何其他值或计算操作,则据说被调用。
结束时无法调用以下情况,因为调用呼叫后,一个步骤分配操作:
以下情况无法称为尾声,因为呼叫后需要以下 - up + 1计算:
如果您通过调用重新出版,则称为递归函数。以下功能代表一个步骤乘法计算:
此函数要么返回值的递归值,要么仅返回一个新调用。以下功能不能称为尾部递归:
这是编写顺序乘法功能而不是尾巴的另一种方法,因为它必须参与另一个计算。
如果进行了解释,则当解释器创建一个新的堆栈帧时,可以弹出原始的pop -up,因为没有后面的计算。跑步。这很容易推断。由于这种“侧面的边缘”机制,整个递归过程实际上只占据了一个堆栈框架。
但是,如果未执行尾声,则需要在返回值之后进行后续计算,然后执行完成。因此,当将程序引入堆栈中时,它不能从堆栈框架中弹出在此“无法输入”的情况下,解释器需要创建和递归堆栈框架。在这种情况下,存在风险。
CSDN:尾部递归和尺寸|CSDN:功能编程尾递归和尾部校正
对于递归函数,Scala编译器通常试图将其转换为等效的迭代循环,但不会通知您成功尝试。Scala提供了有关该功能的注释:它用于明确检查该函数是否是递归功能如果不是,它将直接提示汇编错误。
对于使用Intellij Idea的程序员,IDE使用符号来标记递归(或尾声)功能,而不递归的功能则使用该符号。
功能数据结构只能由纯函数操作,并且仅返回一个新值而不更改原始状态。因此,在FP范式中运行的数据结构自然是不变的。在本章中,我们以直观的方式模仿了Scala的列表,以直观的方式,一个 - 道路链接列表。
在Scala提供的本机列表中,非空节点是(我们跟进以分析熟悉的操作员是什么,以及为什么命名SO),而节点节点是。但是,为了“避免怀疑”,作者具有不同的命名。一个链接列表可以包含两种类型:非空节点和“空节点”的类型。
此外,与IT合作的一种类型的参数已构建-in,因此以这种方式继承的“链接类型底部类型”可以在任何类型的链接列表中使用,以充当“空节点点”角色。足以指示“空”的节点,因此我们还将其设置为一个对象。
在上一个链接列表的定义中,我们经常为每个节点设置一个“以前的节点”或“后一个节点”。第一个元素。
在上一个认知中,链接的列表是“一个接一个的戒指”,在这里变成了“一层和一层”。对于所有链接的列表操作,您可以拆卸其余链接的操作和递归操作列表。此定义方法与后续类型的转换器完全匹配。
下面给出的方法都很容易匹配。我们从“第三人称”的角度操作链接列表,因此所有后续功能(或“方法”)都定义为伴随对象。
为了轻松通过黄瓜的形式创建链接列表并使用,我们还需要在伴奏对象中定义一种方法。根据链接列表的递归定义,其逻辑如下:
每当从中提取参数时,它都会构造一个带有其第一个元素的子链接列表,然后递归加载剩余参数,直到将所有参数链接到此链接列表中,并且使用了链的末尾。递归功能,但我们并不急于优化它。
尝试创建和打印此自定义链接列表以观察其结构。在它们中,这意味着一个内部的元素并将其作为多个参数传递:
可以通过打印结果观察到此自定义链接列表是一个嵌套结构。对于至少一个元素链接列表,将打印屏幕,对于空的链接列表,根据定义,屏幕只会打印一个。
当数据不变时,如何实现数据删除和增加的功能?它可以通过与上述模式匹配的代码块找到,如果您想在现有链接列表的前面添加元素,则只需要返回一个。由于链接列表是不可变的,因此我们不需要重复原始的链接列表来避免修改和污染,而是直接重复使用。这种方法称为数据共享。
同样,如果要返回的尾部部分,我们只需要使用该模式与链接列表的第一部分以外的所有元素匹配,并且原始链接列表不会受到任何影响。我们说功能数据结构耐用并且可以合理,这意味着由于随后的操作,现有的引用不会改变。
除了添加和删除之类的基本操作外,我们还将尝试逐步实现Scala本机列表中提供的各种转换器功能。
让我们首先设置一种链接列表,然后考虑如何在递归中实现元素积累和积累操作,最后仅返回一种类型的结果。
回顾作者刚才提到的:“对于所有链接列表操作,您可以分为其余链接列表的一对操作和递归操作。”实际上,我们只需要考虑两件事:第一个是正确的操作,然后递归就足够了;另一件事是当递归条件处于临界条件下时返回值。
显然,这两个代码在结构上具有很高的一致性。唯一的区别是在达到临界条件时的返回值,以及在递归过程中中间结果的累积方法(作者使用标记的位置)。
现在,尝试将其概括为更通用的右折叠方法:将累积的操作抽象成符合该类型的函数。假定我们的最后一个累积结果是类型。在每个递归过程中,所有元素都积累到右侧。
在这里,我们还进行了咖喱操作的两个步骤。目的是,当我们单独传递时,Scala编译器可以自动得出实际类型。例如:
该程序将使用0作为初始值,链接列表中的元素将被收集到右侧。显然,这个结果应该是6。
但是,这仍然并不令人满意,因为当前的递归不是递归。原因是当递归调用时“保存场景”,直到下一个递归返回结果,它将无法继续计算,这导致了一系列“等待链”:
它的效率不是很高,因为除操作外,每一层递归,然后“投掷”下一个递归调用,它无能为力。直到递归开始到边界开始,该程序开始通过一步步。
然后,在递归过程中,您可以直接处理元素正确的法规并将其用作计算结果,然后将其作为参数传递给下一个递归调用。在这种方式上,每层递归不再需要等待对于计算结果,并构建了递归函数。也许可以理解:我们已经将操作过程从“连续到扩散到扩展”转换为“向后计算边缘”的过程。
现在,当正确的折叠达到递归条件时,累积结果将直接返回。可以提升将非尾部 - 端付费转换为尾部的rec Cost-的例程,例如实现反向链接列表元素的方法,以及基于此尾巴的递归功能的方法。
该函数的想法是:“首先加载的元素将被排名在后面”(想象一下不断插值的过程)。此外,加上非尾递归可以优化到“ shell函数”中提供初始值并拨打另一个递归表。
但是,对于Scala,我们还可以使用隐式参数来简化此代码。这实际上是提供第一个调用函数时提供初始值的步骤,因此不再需要“ Shell函数”的外部层。
隐式参数的本质在于“隐藏”:对于代码的呼叫者,隐式参数意味着他不需要主动提供初始值(最好让他感知到这一点),并在某些情况下使用它非常合适。例如,伴随对象的链接列表中定义的一种方法。作为代码的用户,他自然地认为,此方法仅需要一个按长度计算的链接列表,而不是提供额外的列表”累积链列表长度的初始价值“他自己。
此外,这需要对Scala的隐式价值机制有足够的了解,以避免隐藏变量被上下文的隐含价值覆盖以获得预期结果。以下是一个例子:
如果调用函数,最终结果将为。从结构性的角度来看,可以认为该函数是函数的闭合。如果分配了函数的隐藏变量,则将隐式表示“所有类型该字段范围中的隐藏值应为“。上下文环境。
有一种类型的嵌套列表来设计一个函数以将其“弄平”到一个类型中。从现有函数的角度来看,直接实现此函数有点困难。我们可以执行执行步骤:
这三种方法的逻辑是渐进式的关系:可以在低级别方法的帮助下实现高级方法。
实施该方法有两种方法。首先是重复一个原始链接列表,并在末尾添加新元素(因为原始链接列表无法修改);链接列表为“倒置”,头部插入了新元素,然后插入链接列表即将到来。这个想法非常清楚,但是运营效率很低。作者在这里选择了前者:
对于这两个链接列表,一个直观的想法是:只要不断追逐到连续追逐的第一个元素的尾巴(实际上,新的链接列表已返回,而不是更改本身),并且效果方法可以最终实现。这是正确的。
至于该方法,可以想象它是一个专门的右(左)叠加过程:以两种或两种形式的形式处理多个内部形式,最终实现了“减少维度”的目的。
这涉及Scala语法现象。为了正确得出预期类型的类型,以便为编译器。在这里,我们指的是一个实例的示例。否则,编译器将相信它应该是一种类型并指示“错误”函数类型。
实际上,链接列表的所有操作都可以视为左折/右折叠的演变版本。BELOW基于折叠操作或其他现有的尾随功能和方法。
在这一点上,我们实施的所有功能都已经拥有以下分层关系:
要求:给定长序列和短序列,它们属于相同类型,需要确定短序列是否是长序列的子序列。
同样,我们首先考虑类型的情况,然后考虑如何给出广义版本。您可以将其想象为“对齐基准”的过程:首先对齐长序列的一端,然后继续进行尝试将长序列和短序列的其余部分对齐。如果它失败,然后“滑动”到后续序列,以继续找到长序列和短序列的部分,然后再递归。
如果长序列的剩余长度已经比短序列短,我们可以断言短序列不得是长序列的子序列。其他关键点是,在滑动过程中不得更改短序列process.let使用动画来表达此逻辑:检查它是否是后续的。
使用代码来描述此逻辑:
完成子序列匹配后,我们将促进类型的一般情况。显然,这里的要求是比较类型,否则我们将无法准确描述“相同,相同”的概念。
因此,这是使用上下文定义方法的适配器:
Scala的“启蒙”是允许使用符号组合用作标识符来增强代码的可读性,因此用户有时会认为您已经设计了一组新的DSL。表示,不是空列表是一个符号。当我们使用构造函数构造不可抗拒的列表时,实际上是。
在这里,我们需要介绍Scala提供的另一个语法特征。对于样本类别,如果它只有两个参数,例如:
然后,当图案匹配时,可以将其视为中型装饰操作员:
Scala命名为“良好意愿”的非空的列表是表达列表的复杂匹配,以便以简洁的方式表达列表。请注意,以下四个匹配句子,在下面表达相同的含义。显然,第一线的可读性是最强的。
另一个特殊的机制是,Scala访问了将结肠终止的所有操作符号作为合适的操作员,这意味着此符号将绑定到正确的操作元素。例如,应根据常规的Scala表达式理解表达式应该是。但是没有办法进行不合理的元素类型。实际情况是:作为正确的操作员,该公式的含义是。
此外,在非正式的匹配场合,它仍然可以以类似的方式构造。这是因为提供了同名方法:
不难理解为什么使用符号列表时,编译器要求右侧必须是类型,因为该方法的关键字始终指向对象。
这次,作者给出了Scala本机列表的另一个模仿版本,其中包括一个简化的版本,仅包含方法和依赖方法。在同一时间,为了避免在逆变器点出现的co-变换类型,作者使用上部边界将其替换为上世界(请参阅作者面前介绍的缩放章节)。
现在,我们还可以构建一个具有符号的非空列表:
或使用符号表格以匹配形式表达匹配逻辑: