函数式编程思想,前端同学的必选项。
函数式编程是一门古老的技术,从上个世纪60年代Lisp语言诞生开始,各种方言层出不穷。各种方言带来欣欣向荣的生态的同时,也给兼容性带来很大麻烦。于是更种标准化工作也在不断根据现有的实现去整理,比如Lisp就定义了Common Lisp规范,但是一大分支scheme是独立的分支。另一种函数式语言ML,后来也标准化成Standard ML,但也拦不住另一门方言ocaml。后来的实践干脆成立一个委员会,定义一个通用的函数式编程语言,这就是Haskell。后来Haskell被函数式原教旨主义者认为是纯函数式语言,而Lisp, ML系都有不符合纯函数式的地方。
不管纯不纯,函数式编程语言因为性能问题,一直影响其广泛使用。直到单核性能在Pentium 4时代达到顶峰,单纯靠提升单线程性能的免费午餐结束,函数式编程语言因为其多线程安全性再次火了起来,先有Erlang,后来还有Scala, Clojure等。
比如Java 8开始支持lambda表达式,而函数式编程的大厦最初就是基于lambda计算构建起来的。
不过比起后端用Java的同学对于函数式编程思想是可选的,对于前端同学变成了必选项。
React框架的组件从很早开始就是不仅支持类式组件,也支持函数式的组件。
比如下面的类继承的方式更符合大多数学过面向对象编程思想同学的心智:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
但是,完全可以写成下面这样的函数式的组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
从React 16.8开始,React Hooks的出现,使得函数式编程思想越来越变得不可或缺。
比如通过React Hooks,我们可以这样为函数组件增加一个状态:
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
同样我们可以使用useEffect来处理生命周期相关的操作,相当于是处理ComponentDidMount:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
那么,useState, useEffect之类的API跟函数式编程有什么关系呢?
我们可以看下useEffect的API文档:
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
Instead, use useEffect
. The function passed to useEffect
will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
所有的可变性、消息订阅、定时器、日志等副作用不能使用在函数组件的渲染过程中。useEffect就是React纯函数世界与命令式世界的通道。
当我们用React写完了前端,现在想写个BFF的功能,发现serverless也从原本框架套类的套娃模式变成了一个功能只需要一个函数了。下面是阿里云serverless HTTP函数的官方例子:
var getRawBody = require('raw-body')
module.exports.handler = function (request, response, context) {
// get requset header
var reqHeader = request.headers
var headerStr = ' '
for (var key in reqHeader) {
headerStr += key + ':' + reqHeader[key] + ' '
};
// get request info
var url = request.url
var path = request.path
var queries = request.queries
var queryStr = ''
for (var param in queries) {
queryStr += param + "=" + queries[param] + ' '
};
var method = request.method
var clientIP = request.clientIP
// get request body
getRawBody(request, function (err, data) {
var body = data
// you can deal with your own logic here
// set response
var respBody = new Buffer('requestHeader:' + headerStr + '\n' + 'url: ' + url + '\n' + 'path: ' + path + '\n' + 'queries: ' + queryStr + '\n' + 'method: ' + method + '\n' + 'clientIP: ' + clientIP + '\n' + 'body: ' + body + '\n')
response.setStatusCode(200)
response.setHeader('content-type', 'application/json')
response.send(respBody)
})
};
虽然没有需要关注副作用之类的要求,但是既然是用函数来写了,用函数式思想总比命令式的要好。
如果在网上搜“如何学习函数式编程”,十有八九会找到要学习函数式编程最好从学习Haskell开始的观点。
然后很可能你就了解到那句著名的话”A monad is just a monoid in the category of endofunctors, what's the problem?“。
翻译过来可能跟没翻译差不多:”一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已“。
别被这些术语吓到,就像React在纯函数式世界外给我们提供了useState, useEffect这些Hooks,就是帮我们解决产生副作用操作的工具。而函子Functor,单子Monad也是这样的工具,或者可以认为是设计模式。
Monad在Haskell中的重要性在于,对于IO这样虽然基础但是有副作用的操作,纯函数的Haskell是无法用函数式方法来处理掉的,所以需要借助IO Monad。大部分其它语言没有这么纯,可以用非函数式的方法来处理IO之类的副作用操作,所以上面那句话被笑称是Haskell用户群的接头暗号。
有范畴论和类型论等知识做为背景,当然会有助于从更高层次理解函数式编程。但是对于大部分前端开发同学来讲,这笔技术债可以先欠着,先学会怎么写代码去使用可能是更好的办法。前端开发的计划比较短,较难有大块时间学习,但是我们可以迭代式的进步,最终是会殊途同归的。
先把架式练好,用于代码中解决实际业务问题,比被困难吓住还停留在命令式的思想上还是要强的。
前端同学学习函数式编程的优势是React Hooks已经将副作用摆在我们面前了,不用再解释为什么要写无副用的代码了。
无副作用的函数应该符合下面的特点:
做到这一点,说简单也简单,只要保持功能足够简单就可以做到;说困难也困难,需要改变写惯了命令行代码的思路。
比如数学函数一般就是这样的好例子,比如我们写一个算平方的函数:
let sqr2 = function(x){
return x * x;
}
console.log(sqr2(200));
无副作用函数拥有三个巨大的好处:
即使是跟有副作用的代码一起工作,我们也可以在副作用代码中缓存无副作用函数的值,可以将无副作用函数并发执行。测试时也可以更重点关注有副作用的代码以更有效地利用资源。
会写无副作用的函数之后,我们要学习的新问题就是如何将这些函数组合起来。
比如上面的sqr2函数有个问题,如果不是number类型,计算就会出错。按照命令式的思路,我们可能就直接去修改sqr2的代码,比如改成这样:
let sqr2 = function(x){
if (typeof x === 'number'){
return x * x;
}else{
return 0;
}
}
但是,sqr2的代码已经测好了,我们能不能不改它,只在它外面进行判断?
是的,我们可以这样写:
let isNum = function(x){
if (typeof x === 'number'){
return x;
}else{
return 0;
}
}
console.log(sqr2(isNum("20")));
或者是我们在设计sqr2的时候就先预留出来一个预处理函数的位置,将来要升级就换这个预处理函数,主体逻辑不变:
let sqr2_v3 = function(fn, x){
let y = fn(x);
return y * y;
}
console.log((sqr2_v3(isNum,1.1)));
嫌每次都写isNum烦,可以定义个新函数,把isNum给写死进去:
let sqr2_v4 = function(x){
return sqr2_v3(isNum,x);
}
console.log((sqr2_v4(2.2)));
现在,我们想重用这个isNum的能力,不光是给sqr2用,我们想给其它数学函数也增加这个能力。
比如,如果给Math.sin计算undefined会得到一个NaN:
console.log(Math.sin(undefined));
这时候我们需要用面向对象的思维了,将isNum的能力封装到一个类中:
class MayBeNumber{
constructor(x){
this.x = x;
}
map(fn){
return new MayBeNumber(fn(isNum(this.x)));
}
getValue(){
return this.x;
}
}
这样,我们不管拿到一个什么对象,用其构造一个MayBeNumber对象出来,再调用这个对象的map方法去调用数学函数,就自带了isNum的能力。
我们先看调用sqr2的例子:
let num1 = new MayBeNumber(3.3).map(sqr2).getValue();
console.log(num1);
let notnum1 = new MayBeNumber(undefined).map(sqr2).getValue();
console.log(notnum1);
我们可以将sqr2换成Math.sin:
let notnum2 = new MayBeNumber(undefined).map(Math.sin).getValue();
console.log(notnum2);
可以发现,输出值从NaN变成了0.
封装到对象中的另一个好处是我们可以用"."多次调用了,比如我们想调两次算4次方,只要在.map(sqr2)之后再来一个.map(sqr2)
let num3 = new MayBeNumber(3.5).map(sqr2).map(sqr2).getValue();
console.log(num3);
使用对象封装之后的另一个好处是,函数嵌套调用跟命令式是相反的顺序,而用map则与命令式一致。
如果不理解的话我们来举个例子,比如我们想求sin(1)的平方,用函数调用应该先写后执行的sqr2,后写先执行的Math.sin:
console.log(sqr2(Math.sin(1)));
而调用map就跟命令式一样了:
let num4 = new MayBeNumber(1).map(Math.sin).map(sqr2).getValue();
console.log(num4);
封装到对象中,看起来还不错,但是函数式编程还搞出来new对象再map,为什么不能构造对象时也用个函数呢?
这好办,我们给它定义个of方法吧:
MayBeNumber.of = function(x){
return new MayBeNumber(x);
}
下面我们就可以用of来构造MayBeNumber对象啦:
let num5 = MayBeNumber.of(1).map(Math.cos).getValue();
console.log(num5);
let num6 = MayBeNumber.of(2).map(Math.tan).map(Math.exp).getValue();
console.log(num6);
有了of之后,我们也可以给map函数升升级。
之前的isNum有个问题,如果是非数字的话,其实没必要赋给个0再去调用函数,直接返回个0就好了。
之前我们一直没写过箭头函数,顺手写一写:
isNum2 = x => typeof x === 'number';
map用isNum2和of改写下:
map(fn){
if (isNum2(this.x)){
return MayBeNumber.of(fn(this.x));
}else{
return MayBeNumber.of(0);
}
}
我们再来看下另一种情况,我们处理返回值的时候,如果有Error,就不处理Ok的返回值,可以这么写:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
console.log(Result.of(1.2,undefined).map(sqr2));
输出结果为:
Result { Ok: 1.44, Err: undefined }
我们来总结下前面这种容器的设计模式:
我们可以把这个设计模式叫做Functor函子。
如果这个容器还提供一个of函数将值转换成容器,那么它叫做Pointed Functor.
比如我们看下js中的Array类型:
let aa1 = Array.of(1);
console.log(aa1);
console.log(aa1.map(Math.sin));
它支持of函数,它还支持map函数调用Math.sin对Array中的值进行计算,map的结果仍然是一个Array。
那么我们可以说,Array是一个Pointed Functor。
有了上面的Result结构了之后,我们的函数也跟着一起升级。如果是数值的话,Ok是数值,Err是undefined。如果非数值的话,Ok是undefined,Err是0:
let sqr2_Result = function(x){
if (isNum2(x)){
return Result.of(x*x, undefined);
}else{
return Result.of(undefined,0);
}
}
我们调用这个新的sqr2_Result函数:
console.log(Result.of(4.3,undefined).map(sqr2_Result));
返回的是一个嵌套的结果:
Result { Ok: Result { Ok: 18.49, Err: undefined }, Err: undefined }
我们需要给Result对象新加一个join函数,用来获取子Result的值给父Result:
join(){
if (this.isOk()) {
return this.Ok;
}else{
return this.Err;
}
}
我们调用的时候最后加上调用这个join:
console.log(Result.of(4.5,undefined).map(sqr2_Result).join());
嵌套的结果变成了一层的:
Result { Ok: 20.25, Err: undefined }
每次调用map(fn).join()两个写起来麻烦,我们定义一个flatMap函数一次性处理掉:
flatMap(fn){
return this.map(fn).join();
}
调用方法如下:
console.log(Result.of(4.7,undefined).flatMap(sqr2_Result));
结果如下:
Result { Ok: 22.090000000000003, Err: undefined }
我们最后完整回顾下这个Result:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));
}
join(){
if (this.isOk()) {
return this.Ok;
}else{
return this.Err;
}
}
flatMap(fn){
return this.map(fn).join();
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
不严格地讲,像Result这种实现了flatMap功能的Pointed Functor,就是传说中的Monad。
在前面各种函数式编程模式中对函数的用法熟悉了之后,回来我们总结下函数式编程与命令行编程体感上的最大区别:
偏函数可以当作是更灵活的参数默认值。
比如我们有个结构叫spm,由spm_a和spm_b组成。但是一个模块中spm_a是固定的,大部分时候只需要指定spm_b就可以了,我们就可以写一个偏函数:
const getSpm = function(spm_a, spm_b){
return [spm_a, spm_b];
}
const getSpmb = function(spm_b){
return getSpm(1000, spm_b);
}
console.log(getSpmb(1007));
高阶函数我们在前面的map和flatMap里面已经用得很熟了。但是,其实高阶函数值得学习的设计模式还不少。
比如给大家出一个思考题,如何用函数式方法实现一个只执行一次有效的函数?
不要用全局变量啊,那不是函数式思维,我们要用闭包。
once是一个高阶函数,返回值是一个函数,如果done是false,则将done设为true,然后执行fn。done是在返回函数的同一层,所以会被闭包记忆获取到:
const once = (fn) => {
let done = false;
return function() {
return done ? undefined : ((done=true), fn.apply(this,arguments));
}
}
let init_data = once(
() => {
console.log("Initialize data");
}
);
init_data();
init_data();
我们可以看到,第二次调用init_data()没有发生任何事情。
前面介绍了这么多,但是函数编程其实还蛮复杂的,比如说涉及到递归。
递归中最简单的就是阶乘了吧:
let factorial = (n) => {
if (n===0){
return 1;
}
return n*factorial(n-1);
}
console.log(factorial(10));
但是我们都知道,这样做效率很低,会重复计算好多次。应该采用动态规划的办法。
那么如何在函数式编程中使用动态规划,换句话说我们如何保存已经计算过的值?
想必经过上一节学习,大家肯定想到要用闭包,没错,我们可以封装一个叫memo的高阶函数来实现这个功能:
const memo = (fn) => {
const cache = {};
return (arg) => cache[arg] || (cache[arg] = fn(arg));
}
逻辑很简单,返回值是lamdba表达式,它仍然支持闭包,所以我们在其同层定义一个cache,然后如果cache中的某项为空则计算并保存之,如果已经有了就直接使用。
这个高阶函数很好用,阶乘的逻辑不用改,只要放到memo中就好了:
let fastFact = memo(
(n) => {
if (n<=0){
return 1;
}else{
return n * fastFact(n-1);
}
}
);
在本文即将结尾的时候,我们再回归到前端,React Hooks里面提供的useMemo,就是这样的记忆机制:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
综上,我们希望大家能记住几点:
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/u7okHiX3ZgctK5XUEAljfQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。