拥抱函数式编程 I - 基本概念

函数编程与命令性编程

为支持使用纯函数方法解决问题,特此创建了函数编程范例。 函数编程是一种声明性编程形式。相比之下,大多数主流语言,包括面向对象的编程 (OOP) 语言(如 C#、Visual Basic、C++ 和 Java–)主要都是为支持命令性(过程性)编程而设计的。

使用命令性方法时,开发人员编写的代码应严格细致地说明计算机为完成目标而必须执行的步骤。 这有时称为算法编程。相比之下,函数方法涉及将问题编成一组要执行的函数。 您要仔细地定义每个函数的输入值和每个函数的返回值。 下表说明了这两种方法之间的一些总体差别。

特征 命令性方法 函数方法
程序员关注点 如何执行任务(算法)和如何跟踪状态更改。 需要哪些信息和需要进行什么转换。
状态更改 重要。不存在。
执行顺序 重要。 不太重要。
主要流控制 循环、条件和函数(方法)调用。 函数调用,包括递归。
主要操作单元 结构或类的实例。 作为第一类对象和数据集合的函。

虽然多数语言旨在支持特定编程范例,但许多通用语言具有很高的灵活性,能够支持多个范例。 例如,包含函数指针的多数语言都可用于可靠地支持函数编程。 而且,在 C# 3.0 和 Visual Basic 9.0 中,已添加了显式语言扩展以支持函数编程,包括 lambda 表达式和类型推理。 LINQ 技术是一种声明性函数编程形式。

函数式编程与面向对象

在不少的论坛或社区我们不难听见这样的一些声音:函数式编程必将取代面向对象, 面向对象已死。这令到我不得不想起在《代码大全》中看到的这样一番话:

如果百分之一百地相信一种方法论,那么你就只能用一种观点看世界了。

方法学不存在对立,只有互补。一种方法学的发展必然建立于另一种方法学的基础力求补充其不足而得发展并被人认同。

为什么要采用函数式编程

使用函数式编程的最大理由是,使用相同的语言如果以FP方式开发可以:

  • 大幅度减少代码量 - 函数式比指令式代码需求更少
  • 提幅度提升代码性能 - 函数式是并行运行的
  • 提幅度提高稳定性 - 因为状态不变性导致代码并“无副作用”

如果用传统的观点阅读函数式代码会觉得“可读性”极差,“难以理解” 甚至有国外的专家曾引用 Martin fowler 的名句抨击函数式编程:

连傻子都能写出让机器机能运行的代码,但优秀的代码却是能让人理解的。

而所谓的“难以理解”我认为是因为更多的可能性是用过去的习惯的思路去看待函数式编程,那当然“难以理解”,存在即合理如果函数式编程真的不具有生命力,那么各种的开发语言的新版本中就不会不断地为支持函数式编程而引入相关的特性,函数式编程已不是专属于OCaml, Groovy, Scala, Erlang, ML, F#和LISP 这些因函数编程而生的语言了,C#, java , python, ruby 甚至 javascript同样可以使用函数式编程,因为这是一种通用的方法论,一种能改善传统OOP设计和开发中的短板的方法论,因此我们需要它,即使不是马上使用也不应该拒绝了解。

融合而非对立

在我的项目开发中OOP会占据极重要的位置,OOP的方法学极为之擅长于在软件的分析与设计领域,而函数式编程更则重于高效编码。这两种方法学之间本质上并没有互斥性,如同任何一位能驾驭OOP的的设计师能用任何一种非OOP的语言开发出OOP的程序一样,FP只是一种高效编码的思维,只是从由至顶向下的顺序式,有状态化的传统写法中转化为并行式,无状态式的短少精干的代码。他们本来都是干将莫邪,合而为一将无坚不催。

FP可以拟补OOP语言中代码量大,线程不确保安全性能低下等的问题,同时反过来在类中同样OOP也能拟补大量函数的组织与分类的问题。因此在面对函数式编程是在我们的认知中扩展新的方法学,而非更换方法学。

函数式编程的基本概念

以下是函数式编程中的一般性概念:

  • 第一类对象/一级对象 (First-class functions)
  • 纯函数 (Pure functions)
  • 递归 (Recursion)
  • 变量的不变性 (Immutable variables)
  • 非严格求值 (Nonstrict evaluation)
  • 语句 (Statements)

第一类对象

在函数式编程中函数是第一类对象、或称第一类值即所谓的 First-Class functions(正如类是面向对象编程中的第一类对象一样)且要满足下列特征:

  • 可以被存放在变量和数据结构中
  • 可作为参数传递给子函数
  • 可作为子函数的返回值
  • 可在运行期间被创建

第一类函数(First-Class functions)是函数式编程的最基本的风格要素,是一个高度抽象的概念,而高阶函数(Higher-order functions)则是一级函数的最佳实践。

我们经常也会把函数对象传递给我们自己定义的函数,不过一般情况下这些自定义的函数就是前文提及的内建函数的某种形式的组合。通过组合使用这三种函数式编程内建的函数, 能够实现范围惊人的“执行流程”操作(全都不用语句,仅仅使用表达式实现)。

纯函数 - Pure Functions

“纯”表示这些函数是“可组合的”,“无副作用的” (No Sides effects)。这要求这些函数具有以下特点:

  • 独立的,这样函数就可以自由排序和重新排列,而不会与程序的其他部分相互牵连和依赖。
  • 无状态的,因而对相同输入执行相同的函数或特定的一组函数将始终产生相同的输出。

可以简单地理解为:纯函数不会引用或更改函数体作用域以外的任何变量或对象,甚至输入参数,它只负责处理输入->求值->返回输出值。

递归 - Recursion

函数式编程中是采用递归函数或高阶函数取代传统的forfor each语句,避免枚举而不得不违反DRY原则。这里并不是指所有的循环都会采用递归进行处理,因为递归会直接影响程序的运行速度,直正应用中更多会采用“尾递归”对传统递归进行优化。

不变性变量 - Immutable Variables

这是一个挺难理解概念,不变的变量不就是常量吗?其实不然,常量与变量在内存中的存在形式并不一样,常量在程序销毁前是不会释放的,而变量则是跟随作用域释放的。具有不变性的变量就是只一但赋值就不会再修改变量的值,在java或是c#中可以用 final 关键字定义,在swift则使用 let 而不是 var。 这看上去可能是一件不可能的事,如果变量不可被修改,那么变量不就完全失去意义了吗?回顾上文,函数式编程写出的代码面向的是多线程的运行环境,如果变量状态不确定,那么就不能确保代码的线程安全,而出现求值错误、死锁甚至崩溃。然而,先不要以指命式编程的思想去考虑这个如何实现,在后面的章节我会用代码实例说明具有不变性变量的具体实现,现在只需要知道这是FP中的一个重要概念。

非严格求值 - Nonstrict Evaluation

非严格求值又称为惰性求值(Lazy Evaluation),就是指变量的取值不是像指令式编程的方式定义时就赋予初始值,而是在引用时才计算其具体的值,反言之则是如果变量从未被引用则永远不会具有值。

语句 - Statments

函数式编程是通过一小段代码对变量求值或执行单一操作,所以并不需要需要 iffor 一类的流控制语句,取而代之的可以是匿名方法或是ladbma表达式。这也是为什么函数式编程的代码量会比指命式编程的代码少很多的原因之一。因为流程控制语句对FP而然就是对DRY的违反,每一种语句都可以通过函数予以取缔。

结语

由于函数式编程中的概念很多,本文只是从抽像概念上进行了一些介绍。而更多具体的概念如:匿名函数、柯里化、闭包等的具体内容将会在接续的章节中配合原源代码示例一一具体说明 ,下一章将会详细地讲述“第一类函数”的实现,而为了说明得更容易并且能真实地将函数式编程应用到实战中,我会采用 javascript 和 coffeescript为作范例的基准语言 。

posted @ 2014-11-13 10:35  Ray Liang  阅读(829)  评论(0编辑  收藏  举报