定义
函数式编程其实就是编写非故意副作用的程序。
课外知识——什么是函数
函数简单的说就是从A(定义域)到B(值域)的一个映射过程。当然具体的函数还有各种限制。
所以函数式编程也应该是一个从入参到返回值的黑盒子。
概述
并不是所有人在函数式编程的定义上达成了共识。一般来说,函数式编程是使用函数来编程的一种编程范式。但是这个定义并不能解释最重要的一点:函数式编程和其他编程范式的区别,以及究竟是什么让它(可能)成为编程的最佳方式。
函数式编程中没有赋值语句,因此变量一旦有了值就不会再改变。更通俗的讲,函数式编程没有副作用——除了结算结果,调用函数没有别的作用。这样消除了bug的一个主要来源,也使得执行顺序变得无关紧要。因为没有能够改变表达式值的副作用,可以在任何时候对它求值。由于能够在任何时候对表达式求职,所以可以用变量的值来自由替换表达式,反之亦然——即程序是“引用透明”的。
函数式编程是什么
理解事物是什么而不是什么往往都很重要。
函数式编程有时候被认为是一系列可以补充或替代的其他编程范式的技术,例如
函数是一等公民
匿名函数
闭包
柯里化
惰性求值
参数多态
代数数据类型
尽管大多数函数式编程语言使用了一些这样的技术,但是对于每一种技术你都可以找到函数式编程语言不支持的例子。所以让程序更加函数化的并非是编程语言,而是你写代码的方式。但是有些编程语言对函数更加友好一些。
与函数式编程语言对应的应该是命令式编程范式。在命令式编程风格里,程序由“做”事情的要素构成。“做”事情意外着一个初始状态、一个转换过程和一个终止状态——有时候称为状态改变。传统的命令式风格的程序通常是描述了一系列由条件判断区分的改变(更关注改变过程的细节)。例如a和b相加的程序,可以表示为
1. 如果b==0,返回a 2. 否则a自增,b自减 3. 用新的a和b重新计算
另一方面,函数式编程是由“是”什么的元素组成,而不是“做”什么。a和b的和并不是会“造”出一个结果。例如2与3的和,并不是会造出5,它就是5——每当你遇到2+3,就可以用5把它替换。
那在命令式编程里面可以这样替换吗?有时候可以,但是有时候不改变程序的结果就无法做到——即如果替换掉的表达式没有副作用,就可以替换。在命令式编程里面,明显a和b在求和的过程中自身发生了变化,这就是一种副作用,这种情况是无法替换的。
所有,命令式和函数编程的最大一个不同是,函数式编程没有副作用。这意味着:
没有变量改变
没有打印到控制动态或其他设备
没有写入文件、数据库、网络或其他什么
没有抛出异常
这里的“没有副作用”是指没有可观测到的副作用。函数式程序是由接受参数并返回值的函数复合而成的,仅此而已,你不关心函数内部发生了什么。但是在实际上,程序是为完全不函数式的计算机而编写的。所有的计算机都基于相同的命令范式,所以函数就是如下黑盒:
接受一个参数(一个单独的参数)
内部做一些神秘的事情,例如改变变量的值,还有许多命令式风格的东西,但是外界看来并没有什么作用
返回一个(单独的)值
这是一种理论上的,实际上函数不可能完全没有副作用。函数会在某个时刻返回一个值,而这个值可能是变化的,这就是一个副作用。也可能会造成内存耗尽的错误,或者堆栈溢出的错误,导致应用程序奔溃,正在某种意义上就是一个可观测到的副作用。
所以函数式编程其实就是编写非故意的副作用的程序——副作用是程序预期的一部分。非故意的副作用也应该越少越好。
编写没有副作用的程序
前面提到了函数式编程是编写没有副作用的程序,那么如何实现呢?函数式编程并非编写没有可观测结果的程序。是关于编写除了返回值以外没有可观测结果的程序。但是如果这个就是程序的全部,那用途不大。实际上函数编程需要可观测的作用,例如把结果显示在屏幕上。换句话说与外界的交互不会发生在计算过程中,而是发生在计算完成后——将会推迟副作用并单独应用。
public static int add(int a,int b){ while(b>0){ a++; b--; } return a; }
这段代码是完全函数式。完全没有其他可观测的作用。虽然可能结果不正确(在溢出的情况下),但是与没有副作用并不矛盾。即使返回错误结果也是函数式的。
public static int div(int a,int b){ return a/b; }
这个代码就不是函数式的,虽然没有改变任何变量,但是b=0时会抛出异常——这就是一个副作用。但是下面的是函数式的。
public static int div(int a,int b){ return (int)(a/(float)b); }
即使b=0,也不会抛出异常。
无论抛异常是否有意为之还是无意的,终归是一个副作用。尽管在命令式编程里面副作用一般也是我们想要的。最简单的形式如下:
public static void add(int a,int b){ while(b>0){ a++; b--; } System.out.println(a); }
这个程序并不返回值,但是把结果打印到了控制台上,这就是期望的副作用。但是下面的代码不是函数式的。因为返回了值又有意加上了副作用——编写除了返回值没有意外可观测结果的程序。
public static int add(int a,int b){ log(String.format("Adding %s and %s",a,b)); while(b>0){ a++; b--; } log(String.format("Returning %s",a)); return a; }
函数式编程就是编写非有意副作用的程序,如果需要副作用尽可能的延迟副作用发生的时机。如果既有返回值又有副作用,这种程序就不是函数式。
引用透明如何让程序更安全
没有副作用(并不会改变外界的什么)并不足以让程序编程函数式的,同样函数式编程也不能被外界所影响——函数式程序的输出只能取决于自己的参数,这就意味着函数式代码不能从控制台、文件等读取数据,既函数式代码是引用透明的(不被外界影响到的代码是引用透明的)。
引用透明代码的特点:
它是独立的。不依赖与任何外部设备,可以在任何上下文中使用它——只需要提供一个有效的参数。
它是确定的。在引用透明的代码中,不会有意外发生,结果可能是错误的,但是一个参数对应的结果肯定是相同的。
它绝对不会抛出任何的Exception。但是它可能会抛出错误(不是异常)——抛出error。例如OOME或者SOE,这些表示代码有bug,并不是作为程序员的你或是你api的用户应该处理的——当然修复这个bug肯定是你要做的。
任何时候它都不会导致其他代码意外失败。例如他不会改变参数或者外界数据,从而导致调用者发现自己的数据过期或者并发访问异常。
它不会由于外部设备(数据库、文件系统)不可用、太慢或坏掉而崩溃——但是你的程序会。
image.png
函数式编程的优势
函数式程序更加易于推断,因为他们有确定性。确定的输入会有确定的输出,在许多情况下可以证明程序是正确的,而不是在大量测试后仍然无法确定程序是否会在意外情况下出错。
函数式程序更加易于测试。没有副作用,所以不需要那些经常用于在测试里隔离程序及外界的mock。
函数式程序更加模块化,值关心输入和输出,不用去处理异常,处理副作用,不用关心上下文变化,并发等。
函数式编程让符合和重新符合更加简单。基础函数组成高级别函数,因为要用骨头吗,所以无须修改就可以为其他程序所重用——例如编写了一个求和函数,A程序可以用,B程序也可以用。
函数式程序总是线程安全的,因为防止了共享状态的变化。并不意外这所有数据都不需要改变,只有共享数据才需要。
共同学习,写下你的评论
评论加载中...
作者其他优质文章