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 相关文章推荐
jQuery插件bxSlider实现响应式焦点图
Apr 12 Javascript
全系IE支持Bootstrap的解决方法
Oct 19 Javascript
javascript运动框架用法实例分析(实现放大与缩小效果)
Jan 08 Javascript
javascript中call apply 与 bind方法详解
Mar 10 Javascript
js原型链与继承解析(初体验)
May 09 Javascript
jQuery简单验证上传文件大小及类型的方法
Jun 02 Javascript
JavaScript正则替换HTML标签功能示例
Mar 02 Javascript
Node.js  事件循环详解及实例
Aug 06 Javascript
微信小程序实现刷脸登录
May 25 Javascript
浅谈在react中如何实现扫码枪输入
Jul 04 Javascript
vue多层嵌套路由实例分析
Mar 19 Javascript
html-webpack-plugin修改页面的title的方法
Jun 18 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 Session_Regenerate_ID函数双释放内存破坏漏洞
2011/01/27 PHP
php之Memcache学习笔记
2013/06/17 PHP
PHP连接MySQL的2种方法小结以及防止乱码
2014/03/11 PHP
使用Yii2实现主从数据库设置
2016/11/20 PHP
thinkPHP5框架闭包函数与子查询传参用法示例
2018/08/02 PHP
jQuery AJAX实现调用页面后台方法和web服务定义的方法分享
2012/03/01 Javascript
扒一扒JavaScript 预解释
2015/01/28 Javascript
jQuery实现自动切换播放的经典滑动门效果
2015/09/12 Javascript
对jquery的ajax进行二次封装以及ajax缓存代理组件:AjaxCache详解
2016/04/11 Javascript
JS产生随机数的用法小结
2016/12/10 Javascript
从零开始学习Node.js系列教程一:http get和post用法分析
2017/04/13 Javascript
Angular获取手机验证码实现移动端登录注册功能
2017/05/17 Javascript
vue仿淘宝订单状态的tab切换效果
2020/06/23 Javascript
关于Mac下安装nodejs、npm和cnpm的教程
2018/04/11 NodeJs
详解js中let与var声明变量的区别
2020/04/05 Javascript
JS控制GIF图片的停止与显示
2019/10/24 Javascript
Vue项目结合Vue-layer实现弹框式编辑功能(实例代码)
2020/03/11 Javascript
package.json中homepage属性的作用详解
2020/03/11 Javascript
vue v-on:click传递动态参数的步骤
2020/09/11 Javascript
Python实现的一个自动售饮料程序代码分享
2014/08/25 Python
使用Python3 编写简单信用卡管理程序
2016/12/21 Python
python xlsxwriter库生成图表的应用示例
2018/03/16 Python
python pandas 如何替换某列的一个值
2018/06/09 Python
python tkinter界面居中显示的方法
2018/10/11 Python
关于python之字典的嵌套,递归调用方法
2019/01/21 Python
python实现雪花飘落效果实例讲解
2019/06/18 Python
Python3 mmap内存映射文件示例解析
2020/03/23 Python
解决Windows下python和pip命令无法使用的问题
2020/08/31 Python
canvas生成带二维码海报的踩坑记录
2019/09/11 HTML / CSS
实习自我鉴定范文
2013/10/30 职场文书
网络研修随笔感言
2014/02/17 职场文书
经济管理专业求职信
2014/06/09 职场文书
2014年学生党支部工作总结
2014/12/20 职场文书
2015年高中生国庆节演讲稿
2015/07/30 职场文书
创业计划书之孕婴生活馆
2019/11/11 职场文书
nodejs利用readline提示输入内容实例代码
2021/07/15 NodeJs