(翻译)JavaScript函数式编程

1,282 阅读12分钟

Cover image for Functional programming in Javascript

JavaScript函数式编程

img

JavaScript中的函数式编程

我开始javascript编程已经有很长一段时间了,我见证了这门语言的成长,从一个使用jquery验证表单的简单工具,到能够开发庞大的后台应用。自从ES6出现和增加了classes,我认为这只会引用编程方式的混乱,使javascript的编程向java靠近。

自从接触了函数式编程,我看到了它完成适应javascript的潜力,并且能够写出更加简洁的代码。

什么是函数式编程

在计算机科学中,函数式编程是一种编程范式——构建计算机程序结构和元素的一种风格——它把计算当做数学上的函数对待,避免状态的改变以及可变数据。它是一种声明式的编程范式,因为程序是用表达式或者声明而不是使用语句来完成的。在函数中,输出的值只依赖于它的参数,换句话说相同的输出总能得出相同的输出。这与命令式编程相反,在命令式编程中,除了函数参数,全局程序装填也可以影响函数最终的结果。消除副作用,即不依赖于函数输出的状态变化,这能使程序更容易理解,这是使用函数式编程开发程序的主要动机之一。

我们将要学习的概念

相关阅读

在我的笔记后面有一些非常有趣的链接

惰性求值

定义

惰性求值,或者说按需调用是一种求值策略,将表达式的求值延迟到需要它的值时才执行(非严格求值),这也能够避免重复求值。

代码示例

惰性求值是将表达式求值延迟到以后执行,这可以用thunks实现。

译者注:将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数

示例1

// 非惰性求值
var value = 1 + 1  // 立即计算得到2

// 惰性求值
var lazyValue = () => 1 + 1  // 当lazyValue被调用时得到值为2

示例2

// 非惰性求值
var add = (x, y) => x + y
var result = add(1, 2)  // 立即计算得到值为3

// 惰性求值
var addLazy = (x, y) => () => x + y;
var result = addLazy(1, 2)  // 返回一个`thunk`,当需要计算时得到结果为3

示例3

// 科里化相加(非惰性求值)
var add = x => y => x + y
var add3 = add(3)
var result = add3(7)  // 立即计算得到值为10

示例4

// 非惰性求值
var callApi = spec => fetch(spec.url, spec.options);
var result = callApi({url: '/api', options: {}});

// 惰性求值
var callApiLazy = spec => () => fetch(spec.url, spec.options);
var result = callApiLazy({url: '/api', options: {}});

Monoid(幺半群

定义

它描述了一组元素,当这些元素与特定的操作(通常称为concat)结合使用时,这些元素具有3个特殊属性:

译者注:幺半群是一个带有二元运算 *: M × M → M 的集合 M

这个操作符必须把集合中的两个值合并为同一集合中的第三个值。假设ab都是集合的一部分,那么concat(a,b)都必须是这个集合中的一部分。在范畴论中,这被称作magma

译者注:即封闭性——对任何在 *M* 内的 *a* 、 *b* , *a***b* 也会在 *M* 内。

操作符必须满足结合律:concat(x, concat(y, z))必须等于concat(concat(x, y), z) ,这里的x,y,z可以使集合中的任意值。无论如何组织操作符,只要遵循规则,结果应该是相同的。

译者注:即结合律——对任何在 *M* 内的*a*、*b*、*c* , (*a***b*)**c* = *a**(*b***c*)

集合中应该有与操作符相关的中性元素,如果中性元素与其他任意值组合,这个值不应该被改变。 concat(element, neutral) == concat(neutral, element) == element

译者注:即单位元:存在一在 *M* 内的元素*e*,使得任一于 *M* 内的 *a* 都会符合 *a***e* = *e***a* = *a*

译者注:例如在实数这个集合中,0即是实数集合对应于加法的单位元,实数中的任何元素与0相加都不会改变元素的值

代码示例

Monoid 的例子,可以是字符串连接,数字相加,函数组合,异步组合。

示例1——Strings

const concat = (a, b) => a.concat(b);

concat("hello", concat(" ", "world")); // "hello world"
concat(concat("hello", " "), "world"); // "hello world"

concat("hello", ""); // 'hello'
concat("", "hello"); // 'hello'

示例2——Numbers

(1 + 2) + 3 == 1 + (2 + 3); // true

x + 0; // x

示例3——Functions

const compose = (func1, func2) => arg => func1(func2(arg));

const add5   = a => a + 5;
const double = a => a * 2;

const doubleThenAdd5 = compose(add5,double);
doubleThenAdd5(3); // 11

Functor(函子)

定义

函子就是可以进行映射到上面的东西。是任何可以使用映射的东西,在实践中,函子表示一种类型,可以映射...

代码示例

换句话说,就是任何对象我们都可以进行映射,并应用函数生成另一个相同类型和连接的对象实例。

示例1

[1, 2, 3].map(val => val * 2); //得到 [2, 4, 6], Array是一个函子

示例2

// 数字->字符串映射
const numberToString = num => num.toString()

示例3

const Identity = value => ({
  map: fn => Identity(fn(value)),
});
const myFunctor = Identity(1);
myFunctor.map(trace); // 1
myFunctor.map(myFunction).map(trace); // 2

纯函数

定义

函数是一个过程,它接受一些输入,称为参数,并产生一些输出,称为返回值。

代码示例

有纯函数和非纯函数,如果函数修改外部返回值,就会产生副作用。

  • 给定同样的输入,会产生同样的输出
  • 不会产生副作用

示例1

//纯函数
const add = (x, y) => x+y;
const sub = (x, y) => x-y;

//非纯函数
let tmp = 2;

const concat = (a,b)=>{
 tmp = 3;
 return a+b;
};

示例2

const double = x => x * 2;

示例3

function add(a, b) {
  return a + b;
}

function mul(a, b) {
  return a * b;
}

let x = add(2, mul(3, 4));

副作用

定义

副作用是指除了返回值之外,在被调用函数之外可以观察到的任何应用状态变化。

  • 修改任何外部变量或者全局变量(比如全局变量,或者父函数作用域链上的变量)
  • 打印到控制台
  • 输出到屏幕
  • 写入到文件
  • 写入到网络
  • 触发任何外部进程
  • 调用任何具有副作用的其他函数

代码示例

函数式编程在很大程度上避免了副作用,它使程序看起来更容易理解,并且更容易测试

示例1

let default = '';

const run = ()=>{
 default = '1111;
 return 'aaaaaaaa';
}

run();

示例2

let counter = 0;

const sum = (a,b)=>{
  counter++;
  return a+b;
}

引用透明

定义

在函数式编程中,引用透明经常被定义为这样一个事实:在程序中,表达式可以被它的值(或具有相同值的任何东西)替换,而不改变程序的结果。这意味着对于给定的参数总是返回相同的结果,而不会产生任何副作用。

代码示例

示例1

int add(int a, int b) {
    return a + b
}

int mult(int a, int b) {
    return a * b;
}

// 1) 表达式是add(2, mult(3, 4)).
// 2) 如果我们替换成mult(3,4) ,它的结果也是12.
// 3) 我们可以使add(2, 12) === add(2, mult(3, 4))

高阶函数

定义

高阶函数是将别的函数作为参数或者将其作为返回值的函数。

代码示例

在Javasript中,函数可以当做值(第一等公民)。这意味着他们可以被当做变量或者作为参数传递

示例——Map:

const double = n => n * 2

//这个map就是一个高阶函数
[1, 2, 3, 4].map(double); // [ 2, 4, 6, 8 ]

示例——Filter

let isBoy = student => student.sex === 'M';

//Filter是一个高阶函数
let getBoys = grades =>grades.filter(isBoy);

示例——Custom

const ivaTax = (a)=> ((a*21)/100);

//Calc是一个高阶函数
const calc = (ammount, tax) => tax(ammount);

//调用
calc(100.50,ivaTax);

一等函数

定义

我认为这个概念是非常重要的,可能它并不是来自于函数式编程,但它是很有趣的。一等函数意味着你可以像对待其他一等对象——他们可以被存储为变量,可以进行传递,被其他函数返回,甚至还能保存自己的属性。有时被称为一等公民,作为一个对象,它支持可以对其他对象进行的所有操作。

事实上,JavaScript对象本身就是一种对象类型。因此一等函数可以支持与其他对象相同的操作。

  • 被存储在一个变量之中
  • 传参给其他函数
  • 作为返回值给其他函数
  • 被存储在数据结构中
  • 保存他们自己的属性和方法

代码示例

对于JavaScript程序员来讲,这意味着你可以使用强大的设计模式,例如高阶函数、回调函数等。

示例——存储在函数中

const sayHi = (name)=>console.log('HI',name);
sayHi('Damian');

示例——作为参数传递

const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);

//调用
buy(100,tax);

示例——存储在数据结构中

const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);

const scooter = {
 ammount:100,
 buy,
 tax
};

//调用
scooter.buy(scooter.value,scooter.tax);

递归

定义

递归是一种迭代操作的技术,通过不断的调用它自己直到得到最终的结果。大多数的循环都可以改写为递归风格,在一些函数式语言中,这是默认的循环方式。

代码示例

JavaScript确实支持递归函数,但我们需要知道的是大多数的JavaScript编译器目前还没有优化来安全的支持它们。

在JS中,我们可以使用filtermapreduceforeach这些高阶函数来使用递归。

代码示例1

const countdown = (value) => (value>0)?countdown(value-1):value;

代码示例2

const factorial = (number) => (number<=0)?1:(number*factorial(number-1));

注意

使用此函数有可能引起JS引擎的调用堆栈中造成溢出。

RangeError: Maximum call stack size exceeded:

countdown(100000)

Uncaught RangeError: Maximum call stack size exceeded
    at countdown (<anonymous>:1:19)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)
    at countdown (<anonymous>:1:40)

解决方案——尾递归

有一种叫尾递归的技术可以解决这种情况

这里有一些相关的文章:

eddmann.com/posts/recur… medium.com/@cukejianya… en.wikipedia.org/wiki/Trampo… raganwald.com/2013/03/28/…

代码示例

//尾递归
const trampoline = (fn) => {
    while (typeof fn === 'function') {
        fn = fn();
    }
    return fn;
};

//测试
const odd  = (n) => () => n === 0 ? false : even(n - 1);
const even = (n) => () => n === 0 ? true : odd(n - 1);

//调用
trampoline(factorial(100000)) // 

闭包

闭包是捆绑在一起(封闭)的函数与对其周围状态(词法环境)的引用的组合。换句话说,闭包可以使你从内部函数访问外部函数的作用域。在JavaScript中,每当函数创建时,闭包都会被创建。

无状态

定义

无状态编程是一种范式,在这种范式中,您实现的操作(函数、方法、过程,无论怎么称呼它们)对计算的状态不敏感。这意味着操作中使用的所有数据都将作为操作的输入传递,而调用该操作的任何操作所使用的所有数据都将作为输出返回。

代码示例

这是函数组合的一种形式,因为这是传递给add函数的乘法的结果。

示例

const isEven = x => x % 2 === 0
const filterOutOdd = collection => collection.filter(isEven)

const add = (x, y) => x + y
const sum = collection => collection.reduce(add)

const sumEven = collection => compose(sum, filterOutOdd)(collection)

sumEven([1, 2, 3, 4])

组合

定义

用最一般的术语来说,我们可以把整个编程原则分成几个阶段:

  • 理解复杂问题的逻辑
  • 将复杂问题分解为几个小的问题
  • 一次解决一个小问题,再把它们组合起来,形成一个连贯的解决方案。

代码示例

这是函数组合的一种形式,因为这是传递给add函数的乘法的结果。在数学中,我们也是这样理解的:f(x)0g(x) = f(g(x)), example. f(x)=2x+5 and g(X)=1x+2 => f(g(x))=2.(1x+2)+5

示例——函数组合

const isEven = x => x % 2 === 0
const filterOutOdd = collection => collection.filter(isEven)

const add = (x, y) => x + y
const sum = collection => collection.reduce(add)

const sumEven = collection => compose(sum, filterOutOdd)(collection)

sumEven([1, 2, 3, 4])

科里化

定义

科里化是将具有多个参数的函数转换为单个参数的函数的过程。

代码示例

科里化函数是一次接受多个参数的函数。

示例——科里化1

const notCurry = (x, y, z) => x + y + z; // 常规函数
const curry    = x => y => z => x + y + z; // 科里化函数

示例——科里化2

const divisible = mod => num => num % mod;

//调用
divisible(10)(2);

//调用
const divisibleEn3 = divisible(3);
divisibleEn3(10)

Lambda

定义

Lambda表达式出现在大多数现代编程语言中(Python, Ruby, Java...),它们是创建函数的简单表达式。这对编程语言是非常重要的,因为它支持一级函数,这基本上意味着将函数作为参数传递给其他函数或将其分配给变量。

什么是Lambda?

在计算机科学中,lambda表达式最重要的特性是它被用作数据。也就是说函数能够作为参数被传递给另一个函数,作为函数的返回值,或者被分配给变量或者数据结构。这是编程语言中的一个经典,当我们谈论lambda时使用=>->simbols

Lambda vs 匿名函数

匿名函数顾名思义就是没有名称的函数。

const hello = function(name){console.log('Hi '+name);}

Or using arrow functions
const hello = (name) => {console.log('Hi '+name);}

Other example:
[1,2,3,4,6].filter((num)=>num>=3); //(num)=>num>=3 是一个匿名函数

lambda函数是作为参数使用的函数:

[1,2,3,4,5,6,7,8].map(e=>({value:e})); //e=>({value:e})这是一个Lambad

$('#el').on('click',()=>{.....});       //是一个lambda,是一个匿名函数,也是一个箭头函数
$('#el').on('click',function(){.....}); //是一个lambda,也是一个匿名函数
$('#el').on('click',function clickHandler()=>{.....}); //是一个lambda,也是一个具名函数

代码示例

Lambda不总是匿名函数,这看上去会让人疑惑,因为你将lambda等于于匿名函数当做真理。虽然这是一个有用的概念,匿名函数有它有趣的用法,比如points-free-style是一个有用的概念,但这并不是lambda表达式的主要的地方,它主要是一个语法糖。

示例——箭头函数

const isChildren = (person)=>person.age<18;
const isTeen     = (person)=>person.age>18 and person.age<25;

const course = [{name:'bob',age:18},{name:'Damian',age:32},{name:'Alexander',age:19}];

//Get childrens.
course.filter(person=>isChildren(person));

//Get teens.
course.filter(person=>isTeen(person));

示例——具名函数

$('#el').on('click',function clickHandler()=>{.....}); //Lambda表达式

示例——匿名函数

$('#el').on('click',()=>{.....}); 

参考资料

原文链接

Functional programming in Javascript