功能风格–第1部分

一个介绍。

如今,函数式编程是一个非常热门的话题。 人们对功能语言和混合功能语言(例如Haskell,Scala,F#和Clojure)越来越感兴趣,而功能编程是在会议和编程社区中最受欢迎的讨论主题之一。 既然您正在阅读本文,也许您也有兴趣进一步了解它。 如果是这样,则本系列文章适合您。 我之所以写它们是因为我意识到需要更多的文献来解释如何以函数式编程。 我想用大量的代码示例来说明它,并强调当您尝试这样做时应该看到的好处。

函数式编程经常以学术和数学术语进行讨论,但是我不想去那里。 那不是我自己学到的。 我不是计算机科学专业的毕业生,所以从未正式教过它。 我学习了如何在家编程,就像许多90年代的青少年一样,在20多年的专业经验中,我的学习一直在继续。 而且,我从来没有觉得自己知道我需要知道的一切。 我一直很关注领域的最新发展,并对它的历史非常感兴趣。 本系列文章面向的是类似的人群:务实的程序员,他们热爱自己的领域,并且通过编写代码来学习得最好,他们谦虚地意识到总是有更多的东西要学习,而又足够实际的人可以从中受益。

因此,在这个由九部分组成的系列文章中,我希望涵盖我认为在函数式编程中很重要的主题。 我将尝试解释一等和高阶函数,映射和归约,curry,函数组成和monad,惰性求值和持久数据结构。 所有可能的地方都将通过代码示例进行说明。 大多数(但不是全部)代码将使用Java,Groovy和Clojure,因为这是我最了解的。

在本系列中不会找到的主题包括monoid,函子,应用程序和类别理论。 如果您想进一步了解这些东西,那么我建议您阅读Bartosz Milewski的ProgrammingCafé作为起点。 他从基本原理讲得很好。 如果您已经对这些主题有很好的理解,那么您可能不太喜欢我的解释。 我只能请您原谅:本系列不适用于您。 我包括少量的代数,因为我认为在计算机程序员的读者群中对数学有所了解是合理的。 另外,我不会羞于使用计算机编程领域的标准术语。 这些文章面向的是不熟悉函数式编程的人员,而不是针对一般性的编程人员。 我很想知道新手是否发现以函数式而不是命令式的语言学习编程会更困难,但这不是本系列的目的。

我应该学习一种功能语言吗?

不一定,确实,我在本系列文章中的主要目的是演示功能样式 ,展示如何在某种程度上在多种语言中采用它,以及如果这样做会给您带来什么好处。 我将使用Java,并在较小程度上使用C#来说明本系列中的许多要点。 尽管如此,并非为函数式编程而设计的语言(从现在起,我将其称为命令式语言)在表达函数式代码方面都存在某些缺陷。 每种语言都有用于函数式编程的自己的“亮点”,您会通过经验发现它在您选择的语言中所处的位置。

很明显,可以使用专门为函数式编程创建的语言来最自然地表达函数式代码。 Haskell通常被认为是最纯粹的功能语言。 它诞生于学术界,其社区喜欢以非常数学的方式思考和讨论编程。 另一种流行的功能语言是Clojure。 这是在Java虚拟机上运行的Lisp的方言。 并非所有的Lisps都是正确的功能语言,但是Clojure是专门为功能编程而设计的,并且对并发性也有出色的支持。

Haskell和Clojure在某种程度上存在于FP频谱的相对两端:Clojure是动态键入的,而Haskell是静态键入的,并且非常强。 您可能已经知道,在静态和动态类型之间的面向对象的编程世界中,一场宗教战争已经持续了数十年之久。 FP也正在与之抗争。 我不会再提及它,因为那不是我想要的本系列内容。

通常被认为是功能性的两种语言Scala和F#实际上是混合语言:它们同时支持功能性和命令式编程风格。 选择权留给程序员。 F#是.NET家族的一部分,可以编译为在公共语言运行时上运行。 像Clojure一样,Scala在JVM上运行。

那么什么是函数编程?

我记得自己第一次听说函数式编程时,会以类似于Wikipedia的这句话来解释它:

在函数代码中,函数的输出值仅取决于传递给该函数的参数,因此,对参数x两次调用具有相同值的函数f每次都会产生相同的结果f(x)。

具有此字符的功能被称为“纯”。 当函数不是纯函数时,原因是它们依赖于可能在函数外部更改的状态。 此外部状态可能采用全局变量或对象的形式,也可能处于文件或数据库中,或其他任何形式。 功能程序员将这种状态变化称为副作用:

消除副作用,即不依赖于功能输入的状态变化,可以使理解和预测程序行为变得更加容易,这是开发功能程序设计的主要动机之一。

因此,功能编程是为了尽可能避免这些副作用的编程。 但是,以这种方式从智力上理解它不足以让我看到函数式编程的真正优势。 最后是学习Clojure,这也教我如何充分利用Java流API,这向我展示了函数式编程的好处。 我发现我现在对编程问题的看法有所不同,并且功能风格使我可以更直接地在代码中表达自己的意图。

副作用和命令式语言。

我希望本系列文章不那么专业,但是我已经提到了两个术语: 副作用命令式语言 。 命令式语言是指所有未明确设计用于函数式编程的语言,因此包括所有过程式和面向对象的语言,例如Fortran,Algol,C,Smalltalk,C ++,Java,C#等。命令式意味着发出命令: 执行此操作执行那 。 这些命令的目的是引起副作用。 副作用意味着某处的某些状态已更改。

大多数命令式编程语言中的元素可以分为三类:

  • 控制结构:if-then-else,循环等
  • 语句:分配变量,重新分配变量,调用过程等。
  • 表达式:产生值的代码。

在这三个语句中,语句是命令式编程的必要部分。 陈述会引起副作用。 现在,几乎每个人都同意全局变量是一件坏事。 为什么? 它们很糟糕,因为全局变量可以随时通过代码中任何位置的语句进行更改,这使代码非常难以理解和调试。 这就是为什么我们希望保持全局可访问数据不变。 函数式编程使这一思想更进一步,并断言最好不要修改局部私有变量。

因此,函数式编程或多或少是没有语句的编程。 通常,仅使用控制结构和表达式,然后甚至控制结构实际上也是表达式。 也许您想知道我的意思。 您几乎可以肯定已经知道一个示例,并且可能会定期使用它。 考虑以下(愚蠢的)代码:

if (myVar == "foo")
    return "myVar was foo";
else
    return "myVar was something else";

使用三元运算符可以更简洁地表达这一点:

return (myVar == "foo")
        ? "myVar was foo"
        : "myVar was something else";

if语句已转换为表达式,而含义却保持完全相同。 它不仅可以说更干净,而且现在只有一个return语句。 这给出了函数式编程的味道。

避免改变状态意味着迭代也需要区别对待。 实际上,在函数式编程中,迭代趋于完全采用不同的形式。 但这将会向前发展; 这一切将在稍后阐明。

实用的定义。

因此,一个好的开始是确定函数式编程的实用定义。 就是说:(1)使编程风格具有功能性的是什么(与命令相反);(2)使编程语言具有功能性的是什么。 上面我给出了纯功能和副作用的定义,但是这并没有真正帮助我理解它。 我发现此定义更有用:

函数式编程对变异状态施加约束。

就像我说的那样,我不希望这个系列过多地使用行话,但是我将允许通过“突变状态”,因为这是FP中的基本术语。 改变状态意味着在已经分配某物后更改其值。 例如,考虑以下代码片段:

int x = 0;
x = x + 1;

最初,符号x已与值0关联。 这是作业。 然后,它与一个新值关联,该值恰好是旧值加一个。 这是重新分配,并且重新分配行为已更改状态:

x为零, 现在为1

对于那些沉迷于命令式编程的人来说,变异状态是如此普遍,以至于我们往往不去考虑。 但是,让我们退后一步。 也许这并不像我们想象的那么自然。 我对计算机编程的介绍来自阅读BBC micro随附的BBC BASIC编程语言手册,当时男孩大概十二岁或十三岁。 我记得在其中看到以下形式的声明:

LET X = X + 1

这让我感到困惑。 我已经对方程式很熟悉,在中学时就已经将其引入代数,但是这种说法没有任何意义。 X如何等于X加1? 该手册显然是为具有数学知识但没有计算经验的读者编写的,它承认这很奇怪。 它解释说,它根本不是一个方程式,它是一个命令式陈述,需要两个连续步骤:

  1. 计算等号X + 1右侧的表达式。
  2. 将结果分配给左侧X上的符号。

怪异来自这样一个事实,即同一符号X可以包含在语句的两个步骤中。 如果是这样写的,那就不会奇怪了:

LET Y = X + 1

这与一阶多项式相同,并且是完全明确的。 原则上,没有理由不能编写避免在任何赋值语句的两侧使用相同符号的程序。 在实践中,对于任何相当复杂的程序,您最终可能会遇到很多符号,可能是无数个数字。 但是您还将拥有一个不会改变任何状态的程序。

为什么我们不这样编程呢?

这两种不同的编程方法(不管是否改变状态)都可以追溯到现代计算的曙光。 1936年,阿隆佐·丘奇(Alonzo Church)发布了数学逻辑形式系统来表达计算,他称其为Lambda微积分。 大约在同一时间,艾伦·图灵(Alan Turing)独立地为称为图灵机的设备创建了一个理论模型,该模型可以通过操纵磁带上的符号来进行计算。 这两个想法随后被整合到一个正式的计算理论中,即所谓的Church-Turing论文,为现代计算奠定了基础。

Turing机器是有状态的:它在机器中拥有一个可以更改的符号,并且还可以在磁带上写入和覆盖符号。 相比之下,lambda演算是一种纯数学方法,没有状态概念。 Lambda演算对最早的编程语言之一Lisp产生了影响,但总体而言,以FORTRAN为例的命令式有状态编程风格已占主导地位。 原因很容易辨别:主要是处理速度和内存。 早期的计算机几乎没有。 同样,直到今天为止,所有计算机的基本编程指令都是必不可少的: 将这两个数字相加在此处存储结果比较这两个数字设置状态标志 ,等等。最早的程序是使用此指令集编写的因为缺乏实用性,第一种高级语言在实际的设计中仍然受到运行机器的强烈影响。 直到后来,功能编程风格才开始流行。

是什么使语言起作用?

如果我们同意函数式编程是按照重新分配原则进行编程,那么这也为我们提供了函数式语言的有用定义。 这是我的定义,您可以选择接受还是不接受:

功能语言默认情况下使所有数据不变。

在功能性语言中,如果注定要重新分配符号,则必须以某种方式声明该符号,并且该语言会强制执行某种形式的仪式。 如果一种语言具有一流的功能或支持lambda表达式,那么我不认为这些东西本身就使其成为一种功能性语言。 这些确实是所有功能语言的功能,但它们也已添加到现代命令式编程语言中。 这些功能确实可以以功能样式进行编程。 这是一件好事,这是我的系列文章的主要主题,但它本身并没有使它们成为功能性语言。

命令式语言。

为了举例说明,C绝对不是功能语言。 在C语言中,默认情况下所有内容都是可变的,除非您明确将其设为不可变的:

int mutable = 0;
const int immutable = 1;

Java和C#也是如此。 通过这种方法,Ruby的功能甚至更少:在Ruby中,“常量”由其名称以大写字母开头,但是您仍然可以更改其值。 这样做只会产生警告。

混合功能语言。

Scala,Kotlin和F#都坐在栅栏上:这些语言使您可以选择是否应将符号设置为变量,或者符号的值永远不会改变。 在Scala中:

val immutable : Integer = 0; 
var mutable : Integer = 1;

而在Kotlin中,它具有更类似于Java的语法:

val immutable = 0;
var mutable = 1;

F#更倾向于功能性方面,因为除非您另外声明,否则所有“变量”都是不可变的:

let x = 0
let mutable y = 1;

在所有三种语言中,集合类型(列表,地图,集合等)是不可变的,并且可根据要求提供可变的版本。 因此,可以将这些语言视为混合功能。 它们使程序员决定要编写功能性代码还是命令性代码。

功能语言。

在Clojure中,功能方面的偏见要牢固得多。 当然,如果需要,可以将其放入代码中:

(def foo "foo")
(def foo "bar")

但是,如果您在程序中的任何地方使用符号foo ,则该值将始终为“ bar”,而不是“ foo”。 第二个定义取代第一个定义,您的代码分析工具可能会将第一个定义标记为未使用。 Clojure与变量赋值最接近的是let

(let [foo "foo"]
  (do
    (println (str "originally: " foo))
    (let [foo "bar"]
      (println (str "inside: " foo)))
    (println (str "afterwards: " foo))))

当您评估此表单时,它将打印:

originally: foo
inside: bar
afterwards: foo

因此我们可以看到let实际上并没有修改foo的值; 它创建了一个新的作用域,其中将符号foo绑定到值“ bar”,但是在该作用域之外,它仍然绑定到原始值“ foo”。

同样,Clojure的集合类型列表,集合,向量和地图都是不可变的:一旦创建,就无法更改。 如果我有清单:

(def my-list (list 1 2 3))

然后(cons 0 my-list)将产生一个新列表(0 1 2 3)但是my-list仍然只包含(1 2 3) 。 映射,集合和向量的行为都相似。 如果您确实想更改Clojure中某物的值,则必须将其定义为atom

(def foo (atom "foo"))

然后使用reset! 改变它的价值。 在Clojure中,用!指示改变状态的函数。 字符:

(do
  (println (str "was: " @foo))
  (reset! foo "bar")
  (println (str "is now: " @foo)))

打印:

was: foo
is now: bar

如果您想更改某件物品的价值,Clojure提出了更高的穿越障碍。 当然,这不是一个无法克服的障碍,但是它使程序员非常确定,是的,我想更改此东西的值,现在我要更改它的值。 即使您必须引用值的方式也不同。 顾名思义,atom保证了您更改原子性。

在Haskell中,从数学意义上讲,变量只是“变量”,因为x是方程式中定义直线y = mx + c的变量。 您可以改变x来计算y的相应值,但这只是描述不可变的东西的一种表示法:直线。 您不能在纯粹的Haskell中进行状态变异,当您需要进行状态转换时,您必须使用monads(我将在稍后解释)。 如果您在此博客文章上比较Ruby和Haskell中的冒泡排序实现,以获取有关如何改变状态的示例,我想您会认为它在命令式语言中看起来更简单。 我怀疑Haskell程序员会在Haskell中编写命令式程序。 但这说:

首先,Haskell是一种功能语言。 尽管如此,我认为它也是世界上最美丽的命令性语言。 ( 西蒙·佩顿·琼斯

如果有人会知道Haskell,那就是他。

因此,按照我的定义,Haskell和Clojure是功能语言。 它们迫使程序员弄清楚可变状态在哪里,并且对如何完成变异施加了约束。 而且,这两种语言的设计都使程序员远离命令式声明中的编程。 我可能在上面的Clojure代码段中使用过do ,但是我认为这是我唯一一次使用它。 在本系列文章中,我将大量使用Clojure,但不再使用do。 我认为这是没有必要的,我们也不会因为它的缺失而松懈。

下次:

到目前为止,我们已经讨论了很多有关什么是函数式编程的知识,但这仅引发了一个广泛的问题。 您如何编程? 你为什么要 在我们真正回答第二个问题之前,我们需要开始回答第一个问题。 在下一篇文章中,我们将在函数式编程中迈出第一步。 特别是,我们将解决以下问题:如何在不改变任何状态的情况下实现循环,以及功能语言如何在避免明显效率低下的情况下做到这一点。

翻译自: https://www.javacodegeeks.com/2018/08/functional-style-part-1.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值