资讯专栏INFORMATION COLUMN

JavaScript 函数式编程(一)

hoohack / 704人阅读

摘要:函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。函数组合面向对象通常被比喻为名词,而函数式编程是动词。尾递归优化函数式编程语言中因为不可变数据结构的原因,没办法实现循环。

零、前言

说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚。

所以本文希望针对工程师,从应用(而非学术)的角度将函数式编程相关思想和实践(以 JavaScript 为例)分享给大家。

文章内容其实主要来自于在下阅读各类参考文献后的再整理,所以有什么错误也希望大家帮忙斧正~

slide 地址

一、什么是函数式编程?
Functional programming is a programming paradigm

1.treats computation as the evaluation of mathematical functions

2.avoids changing-state and mutable data

by wikipedia

从以上维基百科的定义来看有三个要点

Programming Paradigm:编程范式

Mathematical Functions:数学函数

Changing-state And Mutable Data:改变状态和可变数据

下面分别解析一下以上要点。

1.1.什么是编程范式?

from Programming paradigms

编程范式从概念上来讲指的是编程的基本风格和典范模式。

换句话说其实就是程序员对于如何使用编程来解决问题的世界观和方法论

如果把一门编程语言比作兵器,它的语法、工具和技巧等是招法,那么它采用的编程范式也就是是内功心法。

一种范式可以在不同的语言中实现,一种语言也可以同时支持多种范式。例如 JavaScript 就是一种多范式的语言。

1.2.什么是数学函数?
一般的,在一个变化过程中,假设有两个变量 x、y,如果对于任意一个 x 都有唯一确定的一个y和它对应,那么就称 x 是自变量,y 是 x 的函数。x 的取值范围叫做这个函数的定义域,相应 y 的取值范围叫做函数的值域。

以上定义,在初中数学咱们都应该学过...

换句话说,函数只是两种数值之间的关系:输入和输出。

尽管每个输入都只会有一个输出,但不同的输入却可以有相同的输出。下图展示了一个合法的从 x 到 y 的函数关系;

与之相反,下面这张图表展示的就不是一种函数关系,因为输入值 5 指向了多个输出:

1.2.1.什么是纯函数(Pure Functions)?
纯函数是这样一种函数,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

根据定义可以看出纯函数其实就是数学函数,即表示从输入的参数到输出结果的映射。

而没有副作用的纯函数显然都是引用透明的。

引用透明性(Referential Transparency)指的是,如果一段代码在不改变整个程序行为的前提下,可以替换成它的执行结果。
const double = x => x * 2
const addFive = x => x + 5
const num = double(addFive(10))

num === double(10 + 5)
    === double(15)
    === 15 * 2
    === 30

不过说了半天,副作用又是啥...?

1.2.2.什么是副作用(Side Effects)?
副作用是在计算的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用可能包含,但不限于一下行为:

更改文件系统

往数据库中插入记录

发送一个 http 请求

改变数据

打印 log

获取用户输入

DOM 查询

访问系统状态

...

只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。

函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

当然这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。

在后面讲到函子(functor)和单子(monad)的时候我们会学习如何控制它们。

1.2.3.纯函数的好处都有啥(谁说对了就给他)
面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

by Erlang 作者:Joe Armstrong

所以使用纯函数将会有以下好处:

可缓存性(Cacheable)

可移植性/自文档化(Portable / Self-Documenting)

可测试性(Testable)

合理性(Reasonable)

并行代码(Parallel Code)

1.3.为什么要避免改变状态和可变数据?
Shared mutable state is the root of all evil

共享可变状态是万恶之源

by Pete Hunt

const obj = { val: 1 }
someFn(obj)
console.log(obj) // ???

from Building Scalable, Highly Concurrent & Fault Tolerant Systems - Lessons Learned
1.4.原教旨函数式 VS 温和派函数式?

说到函数式编程语言,大家的第一反应可能是 Haskell、OCaml、Lisp、Erlang、Scala、F#...

因为它们可能有以下特性:

函数是“一等公民”(first class)

不可变数据

使用递归而不是循环

柯里化

惰性求值

代数数据类型

模式匹配

...

而说到 JavaScript,很多人可能第一反应认为这是一门面向对象的语言。

但是想想前面说的:函数式编程只是一种编程范式,而编程范式就像“内功心法”,所以与以上这些语言特性不完全相关,反而与你自己的编程思维(即世界观和方法论)更加相关。

在函数式方面,由于 JavaScript 支持高阶函数、匿名函数、函数是一等公民、闭包、解构(模式匹配)等特性,所以它也能支持函数式编程范式。(虽然不是那么的原教旨函数式,但还基本够用~尤其是 ES6 新增的箭头函数等特性~还有各种类库 )

事实上 JavaScript 是一门基于原型(prototype-based)的多范式语言。

1.5.作为函数式语言 JavaScript 还差什么? 1.5.1.不可变数据结构

JavaScript 一共有 6 种原始类型(包括 ES6 新添加的 Symbol 类型),它们分别是 Boolean,Null,Undefined,Number,String 和 Symbol。 除了这些原始类型,其他的类型都是 Object,而 Object 都是可变的。

1.5.2.惰性求值

惰性(lazy)指求值的过程并不会立刻发生。

比如一些数学题,我们可能一开始并不需要把所有表达式都求值,这样可以在计算的过程中将一些表达式消掉。

惰性求值是相对于及早求值(eager evaluation)的。

比如大部分语言中,参数中的表达式都会被先求值,这也称为应用序语言。

比如看下面这样一个 JavaScript 的函数:

wholeNameOf(getFirstName(), getLastName())

getFirstNamegetLastName 会依次执行,返回值作为 wholeNameOf 函数的参数, wholeNameOf 函数最后才被调用。

另外,对于数组操作时,大部分语言也同样采用的是应用序。

[1, 2, 3, 4].map(x => x + 1)

所以,这个表达式立刻会返回结果 [2, 3, 4, 5] 。

当然这并不是说 JavaScript 语言使用应用序有问题,但是没有提供惰性序列的支持就是 JavaScript 的不对了。如果 map 一个大数组后我们发现其实只需要前 10 个元素时,去计算所有元素就显得是多余的了。

1.5.3.函数组合

面向对象通常被比喻为名词,而函数式编程是动词。面向对象抽象的是对象,对于对象的的描述自然是名词。

面向对象把所有操作和数据都封装在对象内,通过接受消息做相应的操作。比如,对象 Kitty,它们可以接受“打招呼”的消息,然后做相应的动作。

而函数式的抽象方式刚好相反,是把动作抽象出来,比如“打招呼”就是一个函数,而函数参数就是作为数据传入的 Kitty(即 Kitty 进入函数“打招呼”,出来的应该是 Hello Kitty)。

面向对象可以通过继承和组合在对象之间分享一些行为或者说属性,函数式的思路就是通过组合已有的函数形成一个新的函数。

然而 JavaScript 语言虽然支持高阶函数,但是并没有一个原生的利于组合函数产生新函数的方式。而这些强大的函数组合方式却往往被类似 Underscore,Lodash 等工具库的光芒掩盖掉(后面会说到这些库的问题)。

1.5.4.尾递归优化

函数式编程语言中因为不可变数据结构的原因,没办法实现循环。所以都是通过递归来实现循环。

然而递归使用不当很容易栈溢出(Stack Overflow),所以一般采用尾递归的方式来优化。

虽然 ES6 规范中规定了尾递归优化规范,然而提供实现的解释器还非常的少,详情可以查阅这个链接

5.代数类型系统

JavaScript 作为一种弱类型的语言,没有静态类型系统。不过使用一些 TypeScript 等预编译的语言可以作为补充~

二、声明式 VS 命令式

Declarative VS Imperative,这两者的区别简单来说其实就是 What VS How。

2.1.“意识形态”上的区别~

声明式:

程序抽象了控制流过程,代码描述的是 —— 数据流:即做什么。

更多依赖表达式。

表达式是指一小段代码,它用来计算某个值。表达式通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。

命令式:

代码描述用来达成期望结果的特定步骤 —— 控制流:即如何做。

频繁使用语句。

语句是指一小段代码,它用来完成某个行为。通用的语句例子包括 for、if、switch、throw,等等……
2.2.举一些栗子

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96874.html

相关文章

  • SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数编程

    摘要:函数式编程,一看这个词,简直就是学院派的典范。所以这期周刊,我们就重点引入的函数式编程,浅入浅出,一窥函数式编程的思想,可能让你对编程语言的理解更加融会贯通一些。但从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。 showImg(https://segmentfault.com/img/bVGQuc); 函数式编程(Functional Programming),一...

    csRyan 评论0 收藏0
  • 翻译连载 |《你不知道的JS》姊妹篇 |《JavaScript 轻量级函数编程》- 第 1 章:

    摘要:所以我觉得函数式编程领域更像学者的领域。函数式编程的原则是完善的,经过了深入的研究和审查,并且可以被验证。函数式编程是编写可读代码的最有效工具之一可能还有其他。我知道很多函数式编程编程者会认为形式主义本身有助于学习。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 关于译者:这是一个流淌着沪江血液...

    omgdog 评论0 收藏0
  • 翻译连载 | 附录 C:函数编程函数库-《JavaScript轻量级函数编程》 |《你不知道的J

    摘要:为了尽可能提升互通性,已经成为函数式编程库遵循的实际标准。与轻量级函数式编程的概念相反,它以火力全开的姿态进军的函数式编程世界。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,...

    Miracle 评论0 收藏0
  • javascript 函数编程思想

    摘要:今天这篇文章主要介绍函数式编程的思想。函数式编程通过最小化变化使得代码更易理解。在函数式编程里面,组合是一个非常非常非常重要的思想。可以看到函数式编程在开发中具有声明模式。而函数式编程旨在尽可能的提高代码的无状态性和不变性。 最开始接触函数式编程的时候是在小米工作的时候,那个时候看老大以前写的代码各种 compose,然后一些 ramda 的一些工具函数,看着很吃力,然后极力吐槽函数式...

    YPHP 评论0 收藏0
  • 函数编程JavaScript进行断舍离

    摘要:函数式编程一开始我并不理解。渐渐地,我熟练掌握了使用函数式的方法去编程。但是自从学习了函数式编程,我将循环都改成了使用和来实现。只有数据和函数,而且因为函数没有和对象绑定,更加容易复用。在函数式的中,这些问题不复存在。 译者按: 当从业20的JavaScript老司机学会函数式编程时,他扔掉了90%的特性,也不用面向对象了,最后发现了真爱啊!!! 原文: How I rediscov...

    dkzwm 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<