JavaScript 中的函数式编程
Javascript 中的函数式编程
Javascript 中的函数式编程
我从事 JavaScript 程序员多年,见证了这门语言的成长,从一个简单的用 jQuery 验证表单的工具,到如今能够构建大型后端平台。自从 ES6 出现,以及它能够添加“类”之后,我发现它只会给编程带来一些混乱,让 JavaScript 代码看起来像是从 Java 迁移过来的。
自从我接触到函数式编程后,我看到了它如何完美地适应 JavaScript 的潜力,以及如何在编写代码的方式上实现更好的清晰度。
什么是函数式编程(FP)?
在计算机科学中,函数式编程是一种编程范式——一种构建计算机程序结构和元素的风格——它将计算视为数学函数的求值,并避免状态变化和可变数据。它是一种声明式编程范式,因为编程是用表达式或声明而不是语句来完成的。在函数式代码中,函数的输出值仅取决于其参数,因此调用具有相同参数值的函数总是会产生相同的结果。这与命令式编程形成对比,在命令式编程中,除了函数的参数之外,全局程序状态也会影响函数的结果值。消除副作用(即不依赖于函数输入的状态变化)可以使程序更容易理解,这是函数式编程发展的主要动机之一。
https://en.wikipedia.org/wiki/Functional_programming
在这个存储库中,我们将了解以下概念:
阅读材料
我的笔记后面有一些非常有趣的链接可供阅读:
- http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
- https://github.com/haskellcamargo/js-real-world- functional-programming (很棒!)
- https://leanpub.com/fljs
- https://medium.com/@lunasunkaiser/going- functional-with-javascript-3c6b2e32c5ea(不错!)
- https://medium.com/@_cyberglot/ functional- programming-should-be-your-1-priority-for-2015-47dd4641d6b9?(很好!)
惰性求值
定义:
惰性求值或按需调用是一种求值策略,它延迟表达式的求值直到需要其值(非严格求值),并且还避免重复求值。
代码示例:
惰性求值是将表达式的求值推迟到以后的过程,这可以通过 thunk 来实现。
例 1:
// Not lazy
var value = 1 + 1 // immediately evaluates to 2
// Lazy
var lazyValue = () => 1 + 1 // Evaluates to 2 when lazyValue is *invoked*
示例 2:
// Not lazy
var add = (x, y) => x + y
var result = add(1, 2) // Immediately evaluates to 3
// Lazy
var addLazy = (x, y) => () => x + y;
var result = addLazy(1, 2) // Returns a thunk which *when evaluated* results in 3.
示例 3:
// Curried add (not lazy)
var add = x => y => x + y
var add3 = add(3)
var result = add3(7) // Immediately evaluates to 10
示例 4:
// Not lazy
var callApi = spec => fetch(spec.url, spec.options);
var result = callApi({url: '/api', options: {}});
// Lazy
var callApiLazy = spec => () => fetch(spec.url, spec.options);
var result = callApiLazy({url: '/api', options: {}});
幺半群(Monoid):
定义:
它描述了一组元素,当与特定操作(通常称为concat )结合时,它们具有 3 个特殊属性。
-
该运算必须将集合中的两个值合并为同一集合中的第三个值。如果 a 和 b 是集合的一部分,则 concat(a, b) 也必须是集合的一部分。在范畴论中,这被称为“magma”。
-
运算必须满足结合律:concat(x, concat(y, z)) 必须与 concat(concat(x, y), z) 相同,其中 x、y 和 z 为集合中的任意值。无论运算如何分组,只要遵循顺序,结果都应该相同。
-
集合必须具有一个与运算无关的中性元素。如果该中性元素与任何其他值组合,则不应改变该值。concat(element, Neutral) == concat(neutral, element) == element。
代码示例:
Monoid 示例,可以是 STRING 连接、NUMBER 加法、FUNCTIONS 组合、Async 组合。
示例 1 - 字符串:
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 - 数字
(1 + 2) + 3 == 1 + (2 + 3); // true
x + 0; // x
示例 2 - 函数
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
单子:
定义:
待办事项
代码示例:
待办事项
例 1:
todo pending
函子:
定义:
函子就是可以被映射的东西。任何可以用映射的东西,实际上都表示一种可以被映射的类型。
代码示例:
换句话说,我们可以对任何对象进行 MAP并应用一个函数来生成相同类型和连接的另一个对象实例。
例 1:
[1, 2, 3].map(val => val * 2); //generates [2, 4, 6], Array is a functor.
示例 2:
// A map of Number -> String
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:
//Some examples of pure functions.
const add = (x, y) => x+y;
const sub = (x, y) => x-y;
//Not pure functions
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) The expression is add(2, mult(3, 4)).
// 2) If we replace mult(3,4) with his result 12.
// 3) We can make this add(2, 12) === add(2, mult(3, 4))
高阶函数:
定义:
高阶函数是将其他函数作为参数或返回函数作为结果的函数。
代码示例:
在 JavaScript 中,函数是值(一等公民)。这意味着它们可以赋值给变量或作为值传递。
示例 - 地图:
const double = n => n * 2
//The map function is a high order function.
[1, 2, 3, 4].map(double); // [ 2, 4, 6, 8 ]
示例 - 过滤器:
let isBoy = student => student.sex === 'M';
//Filter is high order function.
let getBoys = grades =>grades.filter(isBoy);
示例 - 自定义:
const ivaTax = (a)=> ((a*21)/100);
//Calc is high order function.
const calc = (ammount, tax) => tax(ammount);
//Use
calc(100.50,ivaTax);
一流功能:
定义:
我认为写一下这个概念很重要,虽然可能不是来自Fp,但很有趣。一等函数,意味着函数与其他一等对象一样被对待——它们可以存储在变量中,传递,从其他函数返回,甚至拥有自己的属性。有时也被称为一等公民,是指支持通常允许其他对象执行的所有操作的对象。
事实上,JavaScript 函数本身就是一种对象类型。因此,我们可以期望一个一等函数支持我们期望其他对象实现的相同操作。
- 存储在变量中。
- 作为参数传递给函数。
- 由函数返回。
- 存储在某种数据结构中。
- 拥有自己的属性和方法。
代码示例:
对于 JavaScript 程序员来说,这意味着您可以利用强大的设计模式,例如高阶函数、部分函数应用、回调等。
示例 - 存储函数:
const sayHi = (name)=>console.log('HI',name);
sayHi('Damian');
示例 - 作为参数传递:
const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);
//Use
buy(100,tax);
示例 - 存储在结构中:
const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);
const scooter = {
ammount:100,
buy,
tax
};
//Use
scooter.buy(scooter.value,scooter.tax);
递归:
定义:
递归是一种迭代操作的技术,通过让函数反复调用自身直到得出结果。大多数循环都可以用递归方式重写,在某些函数式语言中,这种循环方式是默认的。
代码示例:
JavaScript 的函数式编码风格确实支持递归函数,我们需要注意的是,大多数JavaScript 编译器目前尚未针对安全地支持它们进行优化。
我们可以使用filter,map,reduce,foreach高阶函数在 JS 中进行递归。
例 1:
const countdown = (value) => (value>0)?countdown(value-1):value;
示例 2:
const factorial = (number) => (number<=0)?1:(number*factorial(number-1));
警告:
有可能,使用此函数会导致 JS 引擎的调用堆栈中出现 stackoverflow。
RangeError:超出最大调用堆栈大小:
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)
解决方案 - 蹦床:
存在一种称为TRAMPOLINE的技术来解决这种情况。
该主题的一些链接:
https://eddmann.com/posts/recursive-functions-using-a-trampoline-in-javascript/
https://medium.com/@cukejianya/ functional-js-trampolines-tails-88723b4da320
https://en.wikipedia.org/wiki/Trampoline_%28computing%29
https://raganwald.com/2013/03/28/trampolines-in-javascript.html
代码示例:
//TRAMPOLINE
const trampoline = (fn) => {
while (typeof fn === 'function') {
fn = fn();
}
return fn;
};
//Test functions
const odd = (n) => () => n === 0 ? false : even(n - 1);
const even = (n) => () => n === 0 ? true : odd(n - 1);
//Use
trampoline(factorial(100000)) // BigInt required
关闭:
闭包是将函数与其周围状态(词法环境)的引用捆绑在一起(封装)而成的组合。换句话说,闭包允许你从内部函数访问外部函数的作用域。在 JavaScript 中,每次创建函数时都会创建闭包。https
://developer.mozilla.org/es/docs/Web/JavaScript/Closures
无国籍:
定义:
无状态编程是一种范式,其中你实现的操作(函数、方法、过程,无论你如何称呼它们)对计算状态不敏感。这意味着操作中使用的所有数据都作为输入传递给该操作,而调用该操作的任何操作所使用的所有数据都作为输出传回。https
://stephen-young.me.uk/2013/01/20/ functional-programming-with-javascript.html
代码示例:
这是一种函数组合的形式,因为这是传递给加法函数的乘法的结果。
例子
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])
撰写:
定义:
从最一般的角度来看,我们可以将整个编程学科分为几个阶段:
- 理解问题的复杂业务逻辑。
- 将问题分解为一组较小的问题。
- 一次解决一个较小的问题,然后将其重新组合起来形成一个连贯的解决方案。
代码示例:
这是一种函数组合的形式,因为这是传递给加法函数的乘法结果。在数学中,我们用同样的方式理解:f(x)0g(x) = f(g(x)),例如:f(x)=2x+5 且 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])
咖喱
定义:
柯里化是将一个具有多个参数的函数转换为每个函数只有一个参数的序列的过程。
代码示例:
柯里化函数是一次接受多个参数的函数。
例如 - Curry 1:
const notCurry = (x, y, z) => x + y + z; // a regular function
const curry = x => y => z => x + y + z; // a curry function
例如 - Curry 2:
const divisible = mod => num => num % mod;
//Call
divisible(10)(2);
//Call 2
const divisibleEn3 = divisible(3);
divisibleEn3(10)
您可以通过此链接继续阅读:https://medium.com/front-end-weekly/javascript-es6-curry-functions-with-practical-examples-6ba2ced003b1
Lambda:
定义:
Lambda 表达式存在于大多数现代编程语言(Python、Ruby、Java 等)中。它们只是创建函数的表达式。对于编程语言来说,支持“一等函数”至关重要,这意味着可以将函数作为参数传递给其他函数,或者将其赋值给变量。
什么是 Lambda?
在计算机科学中,lambda 表达式最重要的定义性特征是它被用作数据。也就是说,函数可以作为参数传递给另一个函数,作为函数的返回值,或者赋值给变量或数据结构。在编程语言中,当我们谈论 lambda 时,使用 => 或 -> 符号是一个经典用法。
Lambda 与匿名函数:
匿名函数,顾名思义,就是没有名称的函数。
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 is an anonymous function
Lambda 函数是用作参数的函数:
[1,2,3,4,5,6,7,8].map(e=>({value:e})); //e=>({value:e}) Is a LAMBDA.
$('#el').on('click',()=>{.....}); //Is a LAMBDA and anonymous and arrow function.
$('#el').on('click',function(){.....}); //Is a LAMBDA and anonymous function.
$('#el').on('click',function clickHandler()=>{.....}); //Is a LAMBDA and a named functions.
代码示例:
所以 Lambda 并不总是匿名函数,看起来有些人仍然感到困惑,因为你已经把“lambda = anonymous”当成了真理。虽然理解匿名函数有一些有趣的用途(例如点自由式编程,即隐式编程)是一个有用的概念,但这并不是 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()=>{.....}); //Is a LAMBDA and a named functions.
示例 - 匿名函数:
$('#el').on('click',()=>{.....});
资源来自这些网站和演讲
Anjana Vakil 的特别报道 - JSCONF:
来自许多网站:
- https://gist.github.com/ericelliott/414be9be82128443f6df
- https://teamtreehouse.com/community/what-is-a-lambda-in-javascript
- https://medium.com/front-end-weekly/implementing-javascript-functors-and-monads-a87b6a4b4d9a
- https://marmelab.com/blog/2018/04/18/ functional-programming-2-monoid.html
- https://codewords.recurse.com/issues/four/lazy-composable-and-modular-javascript
- https://www.tutorialspoint.com/ functional_programming/ functional_programming_lazy_evaluation.htm
- https://www.youtube.com/watch?v=-tndyOuK7z8
- https://www.youtube.com/watch?v=q2NgcoXki7o
- https://github.com/vakila/ functional-workshop
- https://speakerdeck.com/vakila/first-steps-into- functional-programming
- https://www.codingame.com/playgrounds/2980/practical-introduction-to- functional-programming-with-js
- https://hackernoon.com/function-type-signatures-in-javascript-5c698c1e9801
- https://marmelab.com/blog/2018/04/18/ functional-programming-2-monoid.html
- https://developer.mozilla.org/es/docs/Web/JavaScript/Closures
- https://www.codementor.io/agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3
- https://hackernoon.com/lazy-evaluation-in-javascript-84f7072631b7
- https://stephen-young.me.uk/2013/01/20/ functional-programming-with-javascript.html
- https://marmelab.com/blog/2018/09/26/ functional-programming-3-functor-redone.html
- https://marmelab.com/blog/2019/03/20/ functional-programming-4-art-of-chaining-monad.html