Talk is cheap, Show me the code !

[译] 函数式编程核心概念

lambda    functional-programming 

像我们这样的程序员,可能是伴随着面向对象编程和编程范式过来的。可能对Java或者C++一头雾水,或者很幸运在使用Ruby、Python或者C#等简洁语言作为入门语言–所以你应该比较习惯”类”、”对象”、”实例”、”静态方法”等概念。可能不太适应被称作”函数式编程”的奇怪范式背后的一些核心概念 – 它不仅和面向对象相当不一样,而且与过程式、基于原型等一系列常见范式风格迥异。

最近几年,函数式编程已然成为热门话题,但是这种编程范式可不是什么新概念。1990年诞生的Haskell可能是最具代表性的函数式编程语言,其他的还有Erlang、Scala、Clojure也属于函数式编程语言,他们都有其特定的拥趸。函数式编程一个优点是,能写出能正常运行的并发(concurrency)程序 – 也就是说像deadlock(死锁)、starvation(饥饿)、thread-safety(线程安全)等常见问题不再是问题。基于过程式的语言中,并发是一个灾难,因为状态能在任何给定时刻改变。对象具有状态,只要在词法作用域(或者动态作用域内,少量语言会使用)内,任何函数都可以操作修改任何变量 – 这是十分强大的,但是在状态切换上很糟糕。

函数式编程有很多优其它优点可以吹捧,但是真正使其从当今众多编程语言中脱颖而出的是,能很好利用CPU的全部核心进行并发计算。所以今天我们来谈谈这种编程范式的一些核心概念。

前言:所有这些概念都是语言无关的(事实上,许多函数式语言并不完全遵循这些),但是你一定要结合一门语言,Haskell是最合适不过的了(因为Haskell严格遵循函数式核心概念)。下面5个概念,是严格的理论驱动,能帮助定义最纯粹的函数式范式。

1.函数纯粹

这是函数式编程最重要的法则。遵循以下两个限制的函数,在一定意义上是纯粹的:

  • 相同参数的函数,被多次调用总是返回相同的值
  • 在函数执行的整个过程中,不会产生副作用

第一点相对容易理解点,比如们调用函数sum(2, 3),那么它应该总是返回相同的值。过程式编程中,当你依赖一些函数无法控制的状态时很容易出问题,例如全局变量或者任何随机行为。一旦产生random()函数调用,或者访问在函数中没有定义的变量 – 该函数将失去纯粹性,显然这在函数式编程中不会发生。

第二点无副作用,相对宽泛的性质。副作用基本是指除了正在执行的函数之外的状态变化。修改在函数外定义的变量,控制台打印,引发异常,从文件读取数据,这些都是使函数失去纯粹性的副作用。乍一看,这些似乎是函数式编程很大的约束。但仔细想想,如果你确定函数不会修改任何处于函数外的状态,那么你完全会相信能在任何场景下调用该函数。这为并发变成和多线程编程提供了很多便利。

2.函数是第一类,可以是高阶函数

这个概念不是函数式编程独有的(在javascript、PHP等语言中也使用相当多),但是这是函数式编程的必要条件。Wikipedia上有关于”第一类函数”的完整概念。函数要成为第一类公民(对象),只需要能将其作为变量,仅此而已。这将允许我们能像操作普通数据类型(例如integer和string)一样操作函数,并且能在运行时的一些地方执行函数。

高阶函数建立在”函数是一等公民”的概念之上,其定义为接受另一个函数作为参数或者返回一个函数。高阶函数常见的示例是map函数,该函数一般在list上迭代,基于传入的函数参数修改数据,返回一个新的list。还有filter函数,接受一个指定list中被选中元素的函数作为参数,返回一个选择后的新list。

3.变量不可变

这个很简单,在函数式编程中,不能在初始化后修改变量。你可以创建新的变量,但是不能修改已存在的变量,这将有助于在程序的整个运行时中保持状态不变。一旦创建一个变量并赋值,你可以完全相信变量的值将不会改变。

4.引用透明

引用透明是一个棘手的定义,你询问5个开发者,可能会得到5个不同的答案。我同意的最精准的”引用透明”定义是,在函数调用的任何地方,函数返回值都能代替函数调用的值,并且函数的状态保持不变(译者注:即函数的返回值,只依赖于输入值)。

我们在JAVA中声明一个3+5的函数:

public int addNumbers(){
  return 3 + 5;
}
 
addNumbers() // 8
8            // 8

很明显,我在任何地方调用addNumbers()函数,我都可以很容易用返回值8来代替这个函数调用,所以这个函数引用是透明的。下面这个例子是引用不透明的:

public void printText(){
  System.out.println("Hello World");
}
 
printText()   // Returns nothing, but prints "Hello World"

这是一个void类型的函数,它被调用时不返回任何返回值。因此,为了使函数引用透明,我们应该能使用空代替函数被调用的值 – 显然这样行不通。该函数通过输出改变了控制台的状态,所以它不是引用透明的。

这是一个很棘手的难题,一旦你遵循这个原则,将是一个非常强力的了解函数如何运行的方式。

5.基于λ演算

函数式编程根植于lambda演算的数学体系中。我不是一名数学家,也不装作是数学家,所以不会深入探讨该数学问题的本质。但是我们可以看下lambda演算(塑造函数式编程如何运行的结构)的两个核心概念:

  • 在lambda演算中,函数可以匿名。因为函数名仅仅是函数头的一部分,影响函数执行是的参数列表。由于lambda演算,我们在现代编程中称之为lambda函数或者匿名函数。

  • 当被调用时,所有函数都将经历一个称之为”柯里化”(currying)的过程。当多参数的函数被调用时,将执行一次函数,但只在参数列表中设置一个变量。最后新函数将返回至少一个参数(刚才应用的那个参数),然后新函数将立即被执行。递归这个过程,直到函数完全被应用,然后返回一个最终结果。由于函数在函数式中是纯粹的,柯里化可以顺利执行。否则,状态改变将是一个隐患,柯里化将会产生一个不安全的结果。

正如我前面所提到的,有除此之外更多的lambda演算,但是我们只关心函数式编程的核心概念的来源。

结语

函数式编程可能涉及和以往相当不同的思维模式,但它确实很强大。我个人认为这篇文章将会在CPU提供更多核心去处理进程的时候一次又一次地被提及,而不是使用其中一两个来增强性能。虽然我提到Haskell是更纯粹的函数式编程语言,但还有其他一些流行编程语言也被归类为函数式,例如Erlang、Clojure、Scala和Elixir等,我强烈建议你去尝试他们其中一个。


来源:

Core Functional Programming Concepts

如何读懂并写出装逼的函数式代码


Posted on By legolas

本站点legolasng.github.io的评论插件已经替换为Disqus,需要FQ才能使用。