JS轻量级函数式编程实现XDM三


Posted in Javascript onJune 16, 2022

前言

这是【JS如何函数式编程】系列文章第三篇。点赞?关注?,持续追踪?

前两篇传送门:

《XDM,JS如何函数式编程?看这就够了!(一)》

《XDM,JS如何函数式编程?看这就够了!(二)》

在第二篇,我们谈了基础之基础,重要之重要——“偏函数”,偏函数通过函数封装,实现了减少传参数量的目的,解决了手动指定实参的麻烦。

更具重要意义的是:

当函数只有一个形参时,我们能够比较容易地组合它们。这种单元函数,便于进行后续的组合函数;

没错,本篇就是谈关于 “组合函数”。它是函数编程的重中之重之重之重重重!

组合函数

含义

函数编程就像拼乐高!

乐高有各式各样的零部件,我们将它们组装拼接,拼成一个更大的组件或模型。

函数编程也有各种功能的函数,我们将它们组装拼接,用于实现某个特定的功能。

下面来看一个例子,比如我们要使用这两个函数来分析文本字符串:

function words(str) {
    return String( str )
        .toLowerCase()
        .split( /\s|\b/ )
        .filter( function alpha(v){
            return /^[\w]+$/.test( v );
        } );
}
function unique(list) {
    var uniqList = [];
    for (let i = 0; i < list.length; i++) {
        if (uniqList.indexOf( list[i] ) === -1 ) {
            uniqList.push( list[i] );
        }
    }
    return uniqList;
}
var text = "To compose two functions together";
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
wordsUsed;
//  ["to", "compose", "two", "functions", "together"]

不用细看,只用知道:我们先用 words 函数处理了 text,然后用 unique 函数处理了上一处理的结果 wordsFound;

这样的过程就好比生产线上加工商品,流水线加工。

JS轻量级函数式编程实现XDM三

想象一下,如果你是工厂老板,还会怎样优化流程、节约成本?

这里作者给了一种解决方式:去掉传送带!

JS轻量级函数式编程实现XDM三

即减少中间变量,我们可以这样调用:

var wordsUsed = unique( words( text ) );
wordsUsed

确实,少了中间变量,更加清晰,还能再优化吗?

我们还可以进一步把整个处理流程封装到一个函数内:

function uniqueWords(str) {
    return unique( words( str ) );
}
uniqueWords(text)

这样就像是一个黑盒,无需管里面的流程,只用知道这个盒子输入是什么!输出是什么!输入输出清晰,功能清晰,非常“干净”!如图:

JS轻量级函数式编程实现XDM三

与此同时,它还能被搬来搬去,或再继续组装。

我们回到 uniqueWords() 函数的内部,它的数据流也是清晰的:

uniqueWords <-- unique <-- words <-- text

封装盒子

上面的封装 uniqueWords 盒子很 nice ,如果要不断的封装像 uniqueWords 的盒子,我们要一个一个的去写吗?

function uniqueWords(str) {
    return unique( words( str ) );
}
function uniqueWords_A(str) {
    return unique_A( words_A( str ) );
}
function uniqueWords_B(str) {
    return unique_B( words_B( str ) );
}
...

所以,一切为了偷懒,我们可以写一个功能更加强大的函数来实现自动封装盒子:

function compose2(fn2,fn1) {
    return function composed(origValue){
        return fn2( fn1( origValue ) );
    };
}
// ES6 箭头函数形式写法
var compose2 =
    (fn2,fn1) =>
        origValue =>
            fn2( fn1( origValue ) );

JS轻量级函数式编程实现XDM三

接着,调用就变成了这样:

var uniqueWords = compose2( unique, words );
var uniqueWords_A = compose2( unique_A, words_A );
var uniqueWords_B = compose2( unique_B, words_B );

太清晰了!

任意组合

上面,我们组合了两个函数,实际上我们也可以组合 N 个函数;

finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue

JS轻量级函数式编程实现XDM三

比如用一个 compose 函数来实现(敲重点):

function compose(...fns) {
    return function composed(result){
        // 拷贝一份保存函数的数组
        var list = fns.slice();
        while (list.length > 0) {
            // 将最后一个函数从列表尾部拿出
            // 并执行它
            result = list.pop()( result );
        }
        return result;
    };
}
// ES6 箭头函数形式写法
var compose =
    (...fns) =>
        result => {
            var list = fns.slice();
            while (list.length > 0) {
                // 将最后一个函数从列表尾部拿出
                // 并执行它
                result = list.pop()( result );
            }
            return result;
        };

基于前面 uniqueWords(..) 的例子,我们进一步再增加一个函数来处理(过滤掉长度小于等于4的字符串):

function skipShortWords(list) {
    var filteredList = [];
    for (let i = 0; i < list.length; i++) {
        if (list[i].length > 4) {
            filteredList.push( list[i] );
        }
    }
    return filteredList;
}
var text = "To compose two functions together";
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
wordsUsed;
// ["compose", "functions", "together"]

这样 compose 函数就有三个入参且都是函数了。我们还可以利用偏函数的特性实现更多:

function skipLongWords(list) { /* .. */ }
var filterWords = partialRight( compose, unique, words ); // 固定 unique 函数 和 words 函数
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
shorterWords( text );

filterWords 函数是一个更具有特定功能的变体(根据第一个函数的功能来过滤字符串)。

compose 变体

compose(..)函数非常重要,但我们可能不会在生产中使用自己写的 compose(..),而更倾向于使用某个库所提供的方案。了解其底层工作的原理,对我们强化理解函数式编程也非常有用。

我们理解下 compose(..) 的另一种变体 —— 递归的方式实现:

function compose(...fns) {
    // 拿出最后两个参数
    var [ fn1, fn2, ...rest ] = fns.reverse();
    var composedFn = function composed(...args){
        return fn2( fn1( ...args ) );
    };
    if (rest.length == 0) return composedFn;
    return compose( ...rest.reverse(), composedFn );
}
// ES6 箭头函数形式写法
var compose =
    (...fns) => {
        // 拿出最后两个参数
        var [ fn1, fn2, ...rest ] = fns.reverse();
        var composedFn =
            (...args) =>
                fn2( fn1( ...args ) );
        if (rest.length == 0) return composedFn;
        return compose( ...rest.reverse(), composedFn );
    };

通过递归进行重复的动作比在循环中跟踪运行结果更易懂,这可能需要更多时间去体会;

基于之前的例子,如果我们想让参数反转:

var biggerWords = compose( skipShortWords, unique, words );
// 变成
var biggerWords = pipe( words, unique, skipShortWords );

只需要更改 compose(..) 内部实现这一句就行:

...
        while (list.length > 0) {
            // 从列表中取第一个函数并执行
            result = list.shift()( result );
        }
...

虽然只是颠倒参数顺序,这二者没有本质上的区别。

抽象能力

你是否会疑问:什么情况下可以封装成上述的“盒子”呢?

这就很考验 —— 抽象的能力了!

实际上,有两个或多个任务存在公共部分,我们就可以进行封装了。

比如:

function saveComment(txt) {
    if (txt != "") {
        comments[comments.length] = txt;
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        events[evt.name] = evt;
    }
}

就可以抽象封装为:

function storeData(store,location,value) {
    store[location] = value;
}
function saveComment(txt) {
    if (txt != "") {
        storeData( comments, comments.length, txt );
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        storeData( events, evt.name, evt );
    }
}

在做这类抽象时,有一个原则是,通常被称作 DRY(don't repeat yourself),即便我们要花时间做这些非必要的工作。

抽象能让你的代码走得更远! 比如上例,还能进一步升级:

function conditionallyStoreData(store,location,value,checkFn) {
    if (checkFn( value, store, location )) {
        store[location] = value;
    }
}
function notEmpty(val) { return val != ""; }
function isUndefined(val) { return val === undefined; }
function isPropUndefined(val,obj,prop) {
    return isUndefined( obj[prop] );
}
function saveComment(txt) {
    conditionallyStoreData( comments, comments.length, txt, notEmpty );
}
function trackEvent(evt) {
    conditionallyStoreData( events, evt.name, evt, isPropUndefined );
}

这样 if 语句也被抽象封装了。

抽象是一个过程,程序员将一个名字与潜在的复杂程序片段关联起来,这样该名字就能够被认为代表函数的目的,而不是代表函数如何实现的。通过隐藏无关的细节,抽象降低了概念复杂度,让程序员在任意时间都可以集中注意力在程序内容中的可维护子集上。—— 《程序设计语言》

我们在本系列初始提到:“一切为了创造更可读、更易理解的代码。”

从另一个角度,抽象就是将命令式代码变成声命式代码的过程。从“怎么做”转化成“是什么”。

命令式代码主要关心的是描述怎么做来准确完成一项任务。声明式代码则是描述输出应该是什么,并将具体实现交给其它部分。

比如 ES6 增加的结构语法:

function getData() {
    return [1,2,3,4,5];
}
// 命令式
var tmp = getData();
var a = tmp[0];
var b = tmp[3];
// 声明式
var [ a ,,, b ] = getData();

开发者需要对他们程序中每个部分使用恰当的抽象级别保持谨慎,不能太过,也不能不够。

阶段小结

函数组合是为了符合“声明式编程风格”,即关注“是什么”,而非具体“做什么”。

它能将一个函数调用的输出路由跳转到另一个函数的调用上,然后一直进行下去,它借助 compose(..) 或它的变体实现。。

我们期望组合中的函数是一元的(输入输出尽量是一个),这个也是前篇有提到的很重要的一个点。

组合 ———— 声明式数据流 ———— 是支撑函数式编程其他特性的最重要的工具之一!

以上就是JS轻量级函数式编程实现XDM三的详细内容,更多关于JS轻量级函数式编程XDM的资料请关注三水点靠木其它相关文章!


Tags in this post...

Javascript 相关文章推荐
Flash对联广告的关闭按钮讨论
Jan 30 Javascript
理解Javascript_01_理解内存分配原理分析
Oct 11 Javascript
通过Javascript创建一个选择文件的对话框代码
Jun 16 Javascript
javascript实现判断鼠标的状态
Jul 10 Javascript
json+jQuery实现的无限级树形菜单效果代码
Aug 27 Javascript
使用JS实现图片展示瀑布流效果(简单实例)
Sep 06 Javascript
Bootstrap CSS组件之导航(nav)
Dec 17 Javascript
浅谈JS如何实现真正的对象常量
Jun 25 Javascript
angular4 JavaScript内存溢出问题
Mar 06 Javascript
详解.vue文件解析的实现
Jun 11 Javascript
JavaScript实现京东放大镜效果
Dec 03 Javascript
autojs 蚂蚁森林能量自动拾取即给指定好友浇水的实现方法
May 03 Javascript
JS轻量级函数式编程实现XDM二
Jun 16 #Javascript
JS函数式编程实现XDM一
Jun 16 #Javascript
正则表达式基础与常用验证表达式
Jun 16 #Javascript
使用compose函数优化代码提高可读性及扩展性
html中两种获取标签内的值的方法
Jun 16 #jQuery
JavaScript前端面试扁平数据转tree与tree数据扁平化
Jun 14 #Javascript
vue如何在data中引入图片的正确路径
Jun 05 #Vue.js
You might like
PHP脚本数据库功能详解(中)
2006/10/09 PHP
php中判断文件存在是用file_exists还是is_file的整理
2012/09/12 PHP
input file获得文件根目录简单实现
2013/04/26 PHP
php 注册时输入信息验证器的实现详解
2013/07/05 PHP
php实现的zip文件内容比较类
2014/09/24 PHP
php面向对象中static静态属性与方法的内存位置分析
2015/02/08 PHP
解决AJAX中跨域访问出现'没有权限'的错误
2008/08/20 Javascript
学习ExtJS table布局
2009/10/08 Javascript
JS面向对象编程之对象使用分析
2010/08/19 Javascript
JavaScript初学者需要了解10个小技巧
2010/08/25 Javascript
用JQuery在网页中实现分隔条功能的代码
2012/08/09 Javascript
js判断浏览器类型的方法
2013/08/07 Javascript
Jquery使用Firefox FireBug插件调试Ajax步骤讲解
2013/12/02 Javascript
JavaScript数据结构和算法之图和图算法
2015/02/11 Javascript
JavaScript禁止复制与粘贴的实现代码
2016/05/16 Javascript
Node.js中文件操作模块File System的详细介绍
2017/01/05 Javascript
浅谈angular.js跨域post解决方案
2017/08/30 Javascript
js+html获取系统当前时间
2017/11/10 Javascript
初识 Vue.js 中的 *.Vue文件
2017/11/22 Javascript
基于vue+canvas的excel-like组件实例详解
2017/11/28 Javascript
将jquery.qqFace.js表情转换成微信的字符码
2017/12/01 jQuery
angular json对象push到数组中的方法
2018/02/27 Javascript
vue实现弹幕功能
2019/10/25 Javascript
微信小程序跨页面传递data数据方法解析
2019/12/13 Javascript
python快速排序代码实例
2013/11/21 Python
django创建超级用户过程解析
2019/09/18 Python
Python jieba库用法及实例解析
2019/11/04 Python
Python Websocket服务端通信的使用示例
2020/02/25 Python
美国生日蛋糕店:Bake Me A Wish!
2017/02/08 全球购物
澳洲CFL商城:CHEMIST FOR LESS(中文)
2021/02/28 全球购物
什么是Rollback Segment
2013/04/22 面试题
行政前台岗位职责
2013/12/04 职场文书
教师个人鉴定材料
2014/02/08 职场文书
贪污受贿检讨书范文
2014/11/19 职场文书
土木工程毕业答辩开场白
2015/05/29 职场文书
校友会致辞
2015/07/30 职场文书