???????? 在本文的第一部分中,我没有从学术角度,也没有从营销角度,而是以一种搬砖码农能看懂的方式,解释了什么是函数式编程语言。更重要的是,我希望我对副作用的定义,能帮助搬砖码农更轻松地在应用程序失控前找到它们。 现在,让我们来看看现实世界的函数式编程语言吧... 1编程领域盘点 有了快速定位副作用的技能之后,我们可以查看一个指定函数并指出它的隐藏复杂体。有了函数式编程的定义之后,我们可以盘点一下编程领域,在每个方向上提出一些新观点... 函数式编程不是... 它不是map和reduce 即使你知道函数式语言中都有这些函数,但它们并不是决定语言是函数式的关键。只不过每次在你试图从一系列处理中分离复杂性时,它们都意外地出现罢了。 它不是λ函数 同样,你可能知道每一个FP(函数式编程)语言都推崇“函数是一等公民”。但它只是你创建一门避免副作用语言的自然产物。它是一个助推器,但不是根本原因。 它不是类型 静态类型检测是一个非常有用的工具,但它不是FP的必备条件。Lisp是最早的函数式编程语言,也是最早的动态语言。 不过静态类型非常有用。Haskell在对抗副作用时非常优雅地使用了类型系统。但它们不是构成或破坏函数式语言的要素。 一直说,大声说,函数式编程就是讨论副作用。 (当然,也讨论副原因。) 2这对于语言来说意味着什么? JavaScript不是函数式编程语言 函数式语言会帮你消除副作用,不能消除时会控制副作用。JavaScript并不符合这条准则。实际上,很容易发现JavaScript许多推荐使用副作用的地方。 最明显的就是this。这个隐含输入存在每一个函数里。特别不可思议的是this的含义改变的是那么任性。即使是JavaScript专家也会在定位this当前所指对象时遇到困难。从函数式的角度看,this的神出鬼没应该算设计上的一个败笔。 但是你可以使用JavaScript的函数式编程库(例如,Immutable.js),它可以轻松将编程变成函数式风格,而不会改变语言的本质。 (顺便说一句,如果你喜欢这个JavaScript领域大受欢迎的函数式库,那想象一下你会多喜欢一门完全支持函数式风格的语言。) Java不是函数式编程语言 Java绝不是函数式编程语言。Java1.8版本加入的那些λ函数也不会改变这个事实。Java是完全站在函数式编程的对立面。它的核心设计原则表明,代码应该以一系列副作用,也就是依赖同时又会改变对象本地状态的方法来实现。 事实上,Java对函数式编程是不友好的。如果你写了一段无副作用的Java代码,这段代码不读取也不改变任何本地对象的状态,那你就是一个糟糕的Java程序员。因为Java代码不应该那么写啊。无副作用的代码将会处处都是static关键字,其他人看了之后会皱起眉头然后把你赶出去。 我不是说Java错了。(呃,好吧,我就是这个意思。)但关键是它对副作用的太对与FP完全不同。Java认为局部副作用是优质代码的基石;而FP认为它们是魔鬼。 你可以从略微不同的角度看待这个事情。你可以把Java和FP看做解决副作用问题的两个不同答案。两种模式都认为副作用是个问题,只是解决方法不同。面向对象的解决方案是,“将它们包含在被称之为‘对象’的范围内”,而FP的解决方案是“消灭他们”。然而,在实践中Java并不仅仅试图封装副作用;还利用它们。如果你没有使用有状态的对象制造副作用,那你就是一个糟糕的Java程序员。经常写static的人实际上会被鱿鱼。 Scala身负重任 其实,Scala是一个非常有挑战性的命题。如果它的目标是统一OO和FP这个两个世界,那么从副作用来看,我们认为它试图弥合“强制性副作用”与“禁止副作用”之间的鸿沟(对,此处我说的OO就是指Java,在Scala这个上下文中,我认为把他们划上等号没有什么不妥。)。这么多相对的观点,我不能确定它们都能被化解。仅通过让对象支持map函数是绝对不能统一这两者的。你需要更深入和协调关于副作用的这两个对立立场之间的冲突。 Scala是否成功协调留给你自己判断。但如果我负责推广Scala,我会把它的卖点定位为从Java的副作用向纯粹FP的一个平滑过渡。与其统一他们,不如让它成为一条桥梁。事实上,许多人在实践中都是这么认为的。 Clojure Clojure对副作用的态度很有意思。它的开发者,RichHickey,说过Clojure“80%是函数式的”。我想我能解释为什么只有80%。从一开始,Clojure就被设计为解决一个特定类型的副作用:时间。 举例说明,这里有个Java笑话: 5加2等于多少? 7。 答对了。5加3等于多少? 8。 错。等于10,因为上一步的5变成7了,还记得吗? 好吧,这不是很好笑。但关键是,在Java中,值不会一成不变。我们可以把5赋给某个值,然后调用函数,你会发现那个值不再是5了。数学告诉我们5永远不可能改变——我们可以调用函数返回一个新值,但是我们不可能影响数字5的本质。而Java却说值一直在发生着变化,除非把它们放在某个对象里。 这个整数的例子可能看不出来什么,但遇到更大的值时影响也会被放大。还记得第一部分中InboxQueue的例子吗?InboxQueue的状态是一个随着时间变化的值。我们可以认为时间就是InboxQueue的一个副原因。 Clojure一直恶狠狠地盯着时间的副原因。RichHickey的观点(所有观点之一!)是,时间的隐含影响意味着我们无法依赖不变的输入值;如果我们不能依赖这个值,那我们也就不能依赖函数输入,结果就是我们无法依赖任何可预见的或重复的行为。如果连输入值都有副作用,那一切都会有副作用。如果输入值不是纯净的,那程序里的任何东西都不会纯净。 所以Clojure拿时间开刀了。它所有的值默认都是不可变的(随时间不可变)。如果你需要一个可变值,Clojure提供对不可变值的包装器,这些包装器的使用都有着严格的限制: 必须使用包装器来改变值。 你不能不知不觉地创建一个可变值。你必须使用guards显式地标明潜在的副作用。 你也不会不知不觉地销毁一个可变值。你必须使用guards显式地告知副作用的风险。 当你打开一个可变值的包装器,你得到的依然是不可变值。这样就可以轻松摆脱依赖时间的环境,进入一个纯净的环境。 从时间的角度看,Clojure是函数式编程语言一个极佳的例子。这个语言强烈敌视时间副作用。默认情况下它会消除副作用,但当你必须使用副作用时,它帮你牢牢的控制它,防止副作用污染其他的程序。 Haskell 如果说Clojure只是敌视时间,那Haskell就是明显的敌意了。但Haskell真的也很讨厌副作用,且花费了大量努力来控制它们。 Haskell与副作用斗争过程中的一个方式就是使用类型。它将所有的副作用都推进类型系统。例如,假设你有一个getPerson的函数。在Haskell中它看起来像这样: getPerson::UUID-DatabasePerson 你可以把它解读成,“在Database的上下文中,传入一个UUID,返回一个Person”。这很有趣——你只需看一下Haskell函数的类型签名就可以确定包含了哪些副作用,没包含哪些副作用。你也可以做如下担保,“该函数不会访问文件系统,因为它没有申明那种类型的副作用。”严格控制(PureScript进一步采取了这个思想,这也是值得研究)。 同样的,你可以看看如下函数: formatName::Person-String ...这个函数就是传入一个Person,返回一个String。没有别的了,因为如果存在副作用,你会看到它们被锁进了类型签名中。 但也许最有趣的是,下面这个例子: formatName::Person-DatabaseString 这个签名告诉我们,这个版本的formatName函数包含数据库相关的副作用。什么鬼?为什么formatName函数需要数据库?你的意思是我将需要初始化和模拟出一个数据库就为了测试一个名字格式器?这真的很奇怪。 仅仅通过函数签名,我就知道这个函数设计的有问题。我不需要查看代码,仅从概览就知道这个函数不对劲。多么神奇! 简短地与Java的签名对比一下: publicStringformatName(Personperson){..} 这与哪个Haskell版本等价?如果不看函数体,你没有办法知道。它也许是个纯净版,也有可能会访问数据库,还有可能会删除文件系统然后返回,=“去你的老板!”=。Java的类型签名里关于函数的功能信息和表面信息都很有限。 相反,Haskell的类型签名则告诉了你很多关于函数设计的东西。又因为它们是由编译器检查,那它们告诉你的东西你能确定都是真的。这就意味着它们是个非常好的架构工具。它们能非常高效地暴露设计缺陷,它们也能暴露代码模式。我会在本文之外也会继续使用“functor”和“monad”,但我依然认为高水准的软件设计模式首先得有高水准的需求分析,但当你有一个高水准的符号系统,那高水准的需求分析也会更容易(我有一些伟大的关于Clojure的设计讨论,在这些讨论中我们使用Haskell签名来解释我们自己,验证设计的一致性,并总结我们的结论。是的,你没听错,就是Clojure的讨论。Haskell的符号系统具有远超语言本身的价值。)。 Perl Perl在任何关于副作用的讨论中都值得被提及。因为它有个神奇的参数,$_,该参数是“上一个函数调用的返回值(查看manperlvar获取准确定义)。”它被许多核心函数库隐式地使用和/或改变。据我所知,这让Perl成为了唯一的将全局副作用当做核心特性的语言。 Python 我们来快速浏览一下Java中一个基本的副作用模式: publicStringgetName(){ returnthis.name; } 我们如何使这个调用纯净化?因为,this是一个隐含输入,所以我们要做的就是将它作为一个参数输入: publicStringgetName(Personthis){ returnthis.name; } 现在getName是一个纯函数。值得一提的是Python默认选择了第二种方式。在Python中,所有对象方法使用this作为第一个参数,按照惯例一般写成self: defgetName(self): self.name 显式确实优于隐式。 数据模拟 数据模拟框架一般来说会做两件事情。 第一件事就是帮你初始化输入对象。语言初始化复杂值时越困难,数据模拟越有用。但这不重要。 第二件事才是本次讨论的重点——通过测试给函数初始化正确的副原因,测试之后查看正确的副作用是否发生。 通过副作用的视角,数据模拟是代码不纯的一个标志,在函数式程序员的眼里,数据模拟就是证明某些事情是错误的。我们应该消除副作用,而不是去下载一些库来检查它的正确性。 一个铁杆TDD/Java兄弟曾经问我,在Clojure中如何模拟数据。答案是,我们不模拟。如果需要模拟数据,说明我们需要重构代码了。 3设计缺陷 如果我是小间谍(I-Spy)出一本关于副作用的书,那最容易找到的目标就是无参数输入的函数,与无返回值的函数。 没有参数意味着副原因 无论何时你看到一个没有参数的函数,以下两个说法定有个对的:要么函数是总返回相同的值,要么函数的输入来自其他地方(函数有副原因)。 例如,下面这个函数必须始终返回一个相同的整数值(否则就有副原因): publicIntfoo(){} 没有返回值意味着副作用 无论何时你看到一个没有返回值的函数,那么这个函数要么有副作用,要么调用它就毫无意义: publicvoidfoo(...){...} 根据函数签名,绝对没有理由去调用这个函数。因为它不会给你任何东西。唯一调用它的理由就是,它会悄悄地制造一些神奇的副作用。 4概要/总结 对副作用真正的,直观的认识,会改变你对写码的看法。会改变一切你对单独的函数,甚至整个系统架构的看法。会改变你评判编程语言,工具和编程技巧的方式。它会改变一切。从今开始消灭副作用... ???????? 文:FloydSmith 原文:用什么治疗白癜风最好北京治疗白癜风好处有哪些
|