前端开发函数式编程入门(代码片段)

Jtag特工 Jtag特工     2022-12-23     738

关键词:

前端开发函数式编程入门

函数式编程是一门古老的技术,从上个世纪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已经将副作用摆在我们面前了,不用再解释为什么要写无副用的代码了。

无副作用的函数应该符合下面的特点:

  1. 要有输入参数。如果没有输入参数,这个函数拿不到任意外部信息,也就不用运行了。
  2. 要有返回值。如果有输入没有返回值,又没有副作用,那么这个函数白调了。
  3. 对于确定的输入,有确定的输出

做到这一点,说简单也简单,只要保持功能足够简单就可以做到;说困难也困难,需要改变写惯了命令行代码的思路。

比如数学函数一般就是这样的好例子,比如我们写一个算平方的函数:

let sqr2 = function(x)
    return x * x; 

console.log(sqr2(200));

无副作用函数拥有三个巨大的好处:

  1. 可以进行缓存。我们就可以采用动态规划的方法保存中间值,用来代替实际函数的执行结果,大大提升效率。
  2. 可以进行高并发。因为不依赖于环境,可以调度到另一个线程、worker甚至其它机器上,反正也没有环境依赖。
  3. 容易测试,容易证明正确性。不容易产生偶现问题,也跟环境无关,非常利于测试。

即使是跟有副作用的代码一起工作,我们也可以在副作用代码中缓存无副作用函数的值,可以将无副作用函数并发执行。测试时也可以更重点关注有副作用的代码以更有效地利用资源。

用函数的组合来代替命令的组合

会写无副作用的函数之后,我们要学习的新问题就是如何将这些函数组合起来。

比如上面的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);

用of来封装new

封装到对象中,看起来还不错,但是函数式编程还搞出来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 

我们来总结下前面这种容器的设计模式:

  1. 有一个用于存储值的容器
  2. 这个容器提供一个map函数,作用是map函数使其调用的函数可以跟容器中的值进行计算,最终返回的还是容器的对象

我们可以把这个设计模式叫做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.

偏函数和高阶函数

在前面各种函数式编程模式中对函数的用法熟悉了之后,回来我们总结下函数式编程与命令行编程体感上的最大区别:

  1. 函数是一等公式,我们应该熟悉变量中保存函数再对其进行调用
  2. 函数可以出现在返回值里,最重要的用法就是把输入是n(n>2)个参数的函数转换成n个1个参数的串联调用,这就是传说中的柯里化。这种减少了参数的新函数,我们称之为偏函数
  3. 函数可以用做函数的参数,这样的函数称为高阶函数

偏函数可以当作是更灵活的参数默认值。

比如我们有个结构叫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里面已经用得很熟了。但是,其实高阶函数值得学习的设计模式还不少。

比如给大家出一个思考题,如何用函数式方法实现一个只执行一次有效的函数?

rxjs入门之函数响应式编程(代码片段)

一.函数式编程1.声明式(Declarativ)和声明式相对应的编程?式叫做命令式编程(ImperativeProgramming),命令式编程也是最常见的?种编程?式。//命令式编程:functiondouble(arr)constresults=[]for(leti=0;i<arr.length;i++)results.push(arr[i]*2)returnresult... 查看详情

kotlin函数式编程①(函数式编程简介|高阶函数|函数类别|transform变换函数|过滤函数|合并函数|map变换函数|flatmap变换函数)(代码片段)

...数式编程简介1、编程范式2、高阶函数3、函数式编程4、前端开发技术二、函数类别三、变换函数四、map变换函数1、map函数原型分析2、map函数设计理念3、代码示例五、flatMap变换函数1、flatMap函数原型分析2、代码示例一、函数式... 查看详情

前端学习之函数式编程—函数式编程概念+头等函数(代码片段)

什么是函数式编程函数式编程(functionprogrammingFP)FP是编程范式之一,我们常听说的还有,面向过程编程,面向对象编程函数式编程的思维方式把现实世界的事物和事物之间的联系抽象到程序世界(对运算... 查看详情

编程范式——函数式编程入门(代码片段)

该系列会有3篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在React中的应用,欢迎关注我的blog命令式编程和声明式编程拿泡茶这个事例进行区分命令式编程和声明式编程命令式编程1.烧开水(为第一人... 查看详情

前端框架bootstrap(响应式布局)入门(代码片段)

  Bootstrap,是基于HTML,CSS.javascript的前端框架  该框架已经预定义了一套CSS样式和与样式相对应的js代码(对应的样式有对应的特效.)  开发人员只需要编写HTML结构,添加bootstrap固定的class样式,就可以轻松完成指定效果的实现.... 查看详情

前端学习之函数式编程—高阶函数(代码片段)

 什么是高阶函数(Higher-orderfunction)    可以把函数作为参数传递给另一个函数    可以把函数作为另一个函数的返回结果Part1可以把函数作为参数传递给另一个函数实现forEach函数//高阶函数-函数作为参数functionforEach(array,fn)for(... 查看详情

前端学习之函数式编程—闭包(代码片段)

闭包Part01闭包的概念闭包(closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包. 可以在另一个作用域中调用一个函数的内部函数并访问到该函数作用域中的成员//函数作为返回值functionmakeFn()letmsg='hellofunction'... 查看详情

前端学习之函数式编程—柯里化(代码片段)

part01--柯里化概念柯里化是为了解决函数中不纯函数或函数硬编码的问题什么是硬编码functiongetAge(age)letmin=18returnage>min 函数式编程是保证相同的调用总能得到相同的结果,但当相同的调用有可能不能得到相同的结果时则... 查看详情

python入门--04函数式编程(代码片段)

...,抽象程度高,执行效率低,比如Lisp语言。函数式编程就是一种抽象程度很高的编程范式,纯 查看详情

python入门--04函数式编程(代码片段)

...,抽象程度高,执行效率低,比如Lisp语言。函数式编程就是一种抽象程度很高的编程范式,纯 查看详情

用函数式编程,从0开发3d引擎和编辑器:函数式编程准备(代码片段)

...介绍了本系列涉及到的函数式编程的主要知识点,为正式开发做好了准备。函数式编程的优点1.粒度小相比面向对象编程以类为单位,函数式编程以函数为单位,粒度更小。正所谓:我只想要一个香蕉,而面向对象却给了我整个... 查看详情

前端学习之函数式编程—函数组合(代码片段)

Part01函数组合纯函数和柯里化很容易写出洋葱代码=====>h(g(f(x)))一层一层套起来不宜阅读例如:获取数组最后的一个元素再转大写字母_.toUpper(_.first(_.reverse(array)))函数组合可以让我们把细粒度的函数重新组成... 查看详情

前端进击的巨人:略知函数式编程(代码片段)

系列更文前三篇文章,围绕了一个重要的知识点:"函数"。函数调用栈、函数执行上下文、函数作用域到闭包。可见不理解函数式编程,代码都撸不好。 函数是一等公民函数与其它数据类型一样,可以作为值赋给变量,作为... 查看详情

前端学习之函数式编程—纯函数(代码片段)

Part01纯函数概念纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用纯函数就类似数学中的函数,用来描述输入与输出之间的关系,例如:y=f(x)Part02案例:slice和splice函数 数组的slice和splice分别是纯函数和不纯... 查看详情

python入门到精通python函数式编程与应用详解(代码片段)

...欢迎小伙伴们点赞👍、收藏⭐、留言💬目录python函数式编程lambda表达式的用法及其使用场景什么是匿名函数?ambda表达式的基本格式lambda表达式的使用场景Python中的高阶函数之map函数中带两个参数的map 查看详情

scalaidea安装配置入门helloworld(代码片段)

...门多范式的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala是一门以java虚拟机(JVM)为目标运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言。Scala是一门多范式(multi-paradi... 查看详情

什么是函数式编程(代码片段)

导读建议先阅读一下这几篇博客:函数式编程初探函数式编程入门教程图解Monad什么是函数式编程函数式编程中的函数指的并不是编程语言中的函数(或方法),它指的是数学意义上的函数,即映射关系(如:y=f(x)),就是y和x... 查看详情

函数式编程java函数式编程学习(代码片段)

...;主要关注的是对数据进行了什么操作优点代码简洁,开发快;接近自然语言,易于理解;易于进行“并发编程”;Lambda表达式概念Lambda是JDK8之后的一个语法躺, 查看详情