谈谈函数式编程

来源: OSCHINA
编辑:
2016-09-24 00:00:00

函数式编程 ( Functional Programming ) 是一种以函数为基础的编程方式和代码组织方式,能够带来更好的代码调试及项目维护的优势。本篇主要结合笔者在实际项目开发中的一些应用,简要谈谈函数式编程。

函数

在函数式编程中,任何代码可以都是函数,且要求具有返回值,如下示例

// 非函数式
var title = "Functional Programming";
var saying = "This is not";
console.log(saying + title); // => This is not Functional Programming
// 函数式
var say = title => "This is " + title; 
var text = say("Functional Programming"); // => This is Functional Programming

纯函数

纯函数在这里指函数内外间是“无”关联的。主要有下面两点

  • 没有副作用(side effect)
    不会涉及到外部变量的使用或修改

  • 引用透明
    函数内只会依赖传入参数,在任何时候对函数输入相同的参数时,总能输出相同的结果

// 非纯函数(函数内依赖函数外的变量值)
var title = "Functional Programming";
var say = ()=> "This is not" + title;   // <= 依赖了全局变量 title
// 纯函数
var say = (title)=>"This is " + title; // <= 依赖了以参数 title 传入
say("Functional Programming");

不可变数据(immutable)

这里主要是指变量值的不可变。当需要基于原变量值改变时,可通过产生新的变量来确保原变量的不变性,如下

// 可变数据
var arr = ["Functional", "Programming"];
arr[0] = "Other"; // <= 修改了arr[0]的值
console.log(arr)  // => ["Other", "Programming"] // 变量arr值已经被修改
// 不可变数据
var arr = ["Functional", "Programming"];// 得到新的变量,不修改了原来的值
var newArr = arr.map(item => {    
    if(item === "Functional"){       
         return "Other"; 
    } else {        
        return item;
    }
})
console.log(arr);  // => ["Functional", "Programming"] 变量arr值不变
console.log(newArr); // => ["Other", "Programming"]  产生新的变量newArr

之所以使用这种不变值,除了更好的函数式编程外,还能够维持线程安全可靠,落地在业务中,实际上也能让代码更加清晰。
设想,如果你定义了一个变量A,A在其他地方被其他人修改了,这样是不方便定位A的当前值的。关于定义多个变量引发的内存等问题,可以通过重用结构或部分引用的方式来减轻,可参考 immutable.js

使用 map, reduce 等数据处理函数

强大的 JavaScript 有着越来越多的高能处理数据函数,其中包含了 map、 reduce、 filter 等。

map 能够对原数组中的值进行逐个处理并产生新的数组,一个简单例子

// map
var data = [1, 2, 3];
var squares = data.map( (item, index, array) =>  item * item );
console.log(squares); // => [1, 4, 9]
console.log(data);// =>  [1, 2, 3] data 还是那个 data

reduce 能够对原数组中的各个值进行结合处理,来产生新的值,如下面例子中,previous 代表上一个值,current 代表当前值,reduce 函数可以传入第二个参数作为 previous 初始值,不传时则 previous 初始值为数组中第一个值。

// reduce
var sum = [1, 2, 3].reduce( (previous, current, index, array) => previous + current );
console.log(sum); // => 6

函数柯里化 Currying

柯里化 是将多参函数转换成一系列的单参函数。结合下面例子来说明下

// 一个多参函数
var add = (a, b) => a + b;
add(1, 2); // => 3

将上面的多参函数进行柯里化,如下

// 柯里化函数
var add = a => b => a + b;

上面柯里化后的函数调用方式也有所转变,第一次传入一个参数返回了一个函数,再传入参数则完成整体的调用,这也是利用的闭包的特性

var add1 = add(1);
add1(2);  // => 3

柯里化后的函数,也可以应用在生产 “ 函数 ” 上,如下示例

var say = title => type => title + " is " + type;

var sayFP = say("Functional Programming");
var sayOther = say("Other Programming");

sayFP("good"); // => Functional Programming is good
sayOther("good"); // => Other Programming is good

组合函数 compose

顾名思义,组合函数是将多个函数进行组合成一个函数。举个例子

var compose = (fn1, fn2) => (arg) => fn1(fn2(arg));
var a = arg => arg + 'a';
var b = arg => arg + 'b';
var c = compose(a, b); // 将a,b函数进行组合

c('c');  // => cba

上面示例中,当调用组合函数 c 时,传入的参数会经过 b 函数,接着将 b 函数的返回值作为 a 函数的参数值,从而输出最终结果。
组合函数 c 就像管道一样,将水流( 返回值 )流经各个函数中进行处理。

当想要组合很多函数成一条很长很长的“管道”时,那么显然上面的 compose 函数已经不够用了。下面看看 redux 是怎么做这个 compose 工具函数的。

// 源自: redux/src/compose.js
export default function compose(...funcs) {  
    if (funcs.length === 0) {
        return arg => arg
      } else {
        const last = funcs[funcs.length - 1]
        const rest = funcs.slice(0, -1)    
        return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
   }
}

代码很简洁,主要利用了递归方式和数组的 reduceRight 方法来处理,reduceRight 跟上边提到的 reduce 方法功能是一样的,不同的是 reduceRight 是从数组的末尾向前逐个处理。就这样,想拼多长的就多长。

以上,便是笔者在项目实践中应用较多的函数式编程内容,如有不妥,请斧正。

附: 一些可供学习函数式编程的内容

转自:joeyguo个人博客

展开阅读全文
点击加入讨论🔥(32) 发布并加入讨论🔥
本篇精彩评论
作为一个写过数值计算代码的处理实际工作的程序员,我出于好奇,跑去看了下 OCaml 的教程的第一章,然后看到只支持双精度浮点,直接震惊了:

float IEEE double-precision floating point, equivalent to C's double

啊?居然有人在推广这样的语言?是不是我英文太差,理解错啦?

C 语言是支持 float 单精度浮点的,但很长一段时间,都被认为不适合数值计算。因为老的 C 规范,为了不损失精度,所以规定,float + float 的时候,自动转型为 double + double,计算完毕后,再转回 float。

就这 2 个转换损失,好了,C 语言,你出局了,回家等通知吧。

所以后来新的 C 规范,是 float + float 是直接运算的,不转换。

可能写操作系统和编译器的人,是完全不需要单精度浮点。而计算机科学家,则在大脑里有无限精度的浮点。

至于并行程序,过程式编程需要程序员找可并行化的地方,FPL 一样必须依靠程序员来找可以 map / reduce 的位置。FPL 又不能自己决定什么地方应该自动并行化 / 矢量化,和 C++ 搭配 TBB,cuda 搭配 thrust 之类一样,需要程序员提示。

能有啥大差别。

FPL 唯一的优点在于,按照它的惯用法写出来的程序,能比较容易地并行化。FPL 强制要求把大部分根本不需要并行化的代码,做成并行化友好的形式,代价是对广大不具备数学式抽象能力的普通程序员来说,FPL 程序不直观,很不好写。

这其实就是典型的传说中的 “过早优化”。

过程式编程,要达到类似效果并不难,程序员写并行程序的时候,稍微自我约束一下就行了。如果象 gcc 那样,提供 attribute__ ((pure)) 来辅助一下就更好。有经验的程序员,练习一下,一般都能做到。

还做不到的,估计他也用不好 FPL。
2017-04-04 22:16
1
举报
32 评论
52 收藏
分享
返回顶部
顶部