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 相关文章推荐
页面加载完成后再执行JS的jquery写法以及区别说明
Feb 22 Javascript
jQuery实现购物车数字加减效果
Mar 14 Javascript
javascript处理a标签超链接默认事件的方法
Jun 29 Javascript
JavaScript中的await/async的作用和用法
Oct 31 Javascript
谈谈第三方App接入微信登录 解读
Dec 27 Javascript
vue之数据交互实例代码
Jun 20 Javascript
使用Bootrap和Vue实现仿百度搜索功能
Oct 26 Javascript
VUE 全局变量的几种实现方式
Aug 22 Javascript
Vue 路由切换时页面内容没有重新加载的解决方法
Sep 01 Javascript
Node.js 深度调试方法解析
Jul 28 Javascript
JavaScript array常用方法代码实例详解
Sep 02 Javascript
vue.js页面加载执行created,mounted的先后顺序说明
Nov 07 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
精致的人儿就要挑杯子喝咖啡
2021/03/03 冲泡冲煮
php中jpgraph类库的使用介绍
2013/08/08 PHP
php+mysql不用递归实现的无限级分类实例(非递归)
2014/07/08 PHP
PHP输入输出流学习笔记
2015/05/12 PHP
js电信网通双线自动选择技巧
2008/11/18 Javascript
遨游,飞飞,IE,空中网 浏览器无提示关闭方法
2011/07/11 Javascript
jquer之ajaxQueue简单实现代码
2011/09/15 Javascript
使用jsonp完美解决跨域问题
2014/11/27 Javascript
浅谈js基本数据类型和typeof
2016/08/09 Javascript
js鼠标按键事件和键盘按键事件用法实例汇总
2016/10/03 Javascript
js实现加载更多功能实例
2016/10/27 Javascript
Sequelize中用group by进行分组聚合查询
2016/12/12 Javascript
JS实现多张图片预览同步上传功能
2017/06/23 Javascript
JavaScript编写棋盘覆盖代码详解
2017/08/28 Javascript
vue多种弹框的弹出形式的示例代码
2017/09/18 Javascript
vue-router实现组件间的跳转(参数传递)
2017/11/07 Javascript
详解原生JS回到顶部
2019/03/25 Javascript
vue遍历生成的输入框 绑定及修改值示例
2019/10/30 Javascript
原生JS实现顶部导航栏显示按钮+搜索框功能
2019/12/25 Javascript
Vue实现圆环进度条的示例
2021/02/06 Vue.js
[07:09]2014DOTA2国际邀请赛-Newbee再次发威成功晋级决赛
2014/07/19 DOTA
python动态加载包的方法小结
2016/04/18 Python
python实现求最长回文子串长度
2018/01/22 Python
pandas DataFrame 删除重复的行的实现方法
2019/01/29 Python
浅析python的Lambda表达式
2019/02/27 Python
Python3 pickle对象串行化代码实例解析
2020/03/23 Python
PyCharm Anaconda配置PyQt5开发环境及创建项目的教程详解
2020/03/24 Python
浅谈python opencv对图像颜色通道进行加减操作溢出
2020/06/03 Python
一篇文章教你用python画动态爱心表白
2020/11/22 Python
系统管理员的职责包括那些?管理的对象是什么?
2016/09/20 面试题
小学生爱国演讲稿
2014/04/25 职场文书
考试诚信承诺书
2014/05/23 职场文书
法人委托书范本
2014/09/15 职场文书
可怜妈妈观后感
2015/06/09 职场文书
2016年党员读书月活动总结
2016/04/06 职场文书
微软Win11有哪些隐藏功能? windows11多个功能汇总
2021/11/21 数码科技