美文网首页JavaScript 进阶营工作生活
霖呆呆的函数式编程之路(四)

霖呆呆的函数式编程之路(四)

作者: LinDaiDai_霖呆呆 | 来源:发表于2019-07-04 22:59 被阅读0次

    组合函数

    到了第四章了,函数式编程的魅力似乎越来越大。

    对于函数式编程者,他们会将每个函数都当成是一个“部件”,在需要时通过组装不同的“部件”,来拼凑出一个自己想要的“模型”。

    专业点的角度来说,就是我们能够定义某种组合方式,来让它们成为一种新的组合函数,程序中不同的部分都可以使用这个函数。这种将函数一起使用的过程叫做组合。

    再介绍组合函数的概念之前,我们就已经使用过组合了。

    例如在之前我们的一个案例:

    unary(adder(3))
    

    上面的表达式,我们将两个函数整合起来,然后将第一个函数调用产生的值(输出)当成第二个函数调用的实参(输入)。画个简图,也就是这样:

    functionValue <-- unary <-- adder <-- 3
    

    3adder(..) 的输入。而 adder(..) 的输出是 unary(..) 的输入。unary(..) 的输出是 functionValue。 这就是 unary(..)adder(..) 的组合。

    Compose2函数

    为了满足上面组合函数的要求,我们可以来构造这么一个简单的函数:

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

    它能够自动创建两个函数的组合,这和我们手动做的是一模一样的。

    Words案例

    现在有这么一个需求,需要将给定的一个英文字符串,提取其中全部的英文单词,先全部转化小写,然后去除其中重复的单词。

    我们可以先来创建这么2个函数:

    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++) {
            // value not yet in the new list?
            if (uniqList.indexOf( list[i] ) === -1 ) {
                uniqList.push( list[i] );
            }
        }
    
        return uniqList;
    }
    

    接下来我们解析文本字符串:

    var text = "To compose two functions together, pass the \
    output of the first function call as the input of the \
    second function call.";
    
    var wordsFound = words( text );
    var wordsUsed = unique( wordsFound );
    
    wordsUsed;
    // ["to","compose","two","functions","together","pass",
    // "the","output","of","first","function","call","as",
    // "input","second"]
    

    在上面的例子中,我们将该过程分为了2步来做。

    并且先创建了wordsFound函数,然后将该函数的输出再传递给unique,实际上,上面的效果等同于:

    var wordsUsed = unique( words(text) )
    

    所以我们可以将其封装一层:

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

    你会发现,其实我们还可以这样写:

    var uniqueWords = compose2( unique, words )
    
    var wordsUsed = uniqueWords(text)
    

    这样我们就成功将uniqueWords转化为了无形参的函数。

    uniqueWords(..) 接收一个字符串并返回一个数组。它是 unique(..)words(..) 的组合,并且满足我们的数据流向要求:

    wordsUsed <-- unique <-- words <-- text
    

    compose函数

    在上面我们构造了compose2函数,它能接收2个函数,并将2个函数从右向左的执行。

    如果我们能够定义两个函数的组合,我们也同样能够支持组合任意数量的函数。任意数目函数的组合的通用可视化数据流如下:

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

    我们能够像这样实现一个通用 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(..) 组合例子,让我们增加一个 skipShortWords(..),它将所有单词字母数大于4的提取出来:

    function skipShortWords(list) {
        return list.filter(str => str.length > 4)
    }
    

    让我们再定义一个 biggerWords(..) 来包含 skipShortWords(..)。我们期望等价的手工组合方式是 skipShortWords(unique(words(text))),所以让我们采用 compose(..) 来实现它:

    var text = "To compose two functions together, pass the \
    output of the first function call as the input of the \
    second function call.";
    
    var biggerWords = compose( skipShortWords, unique, words );
    
    var wordsUsed = biggerWords( text );
    
    wordsUsed;
    // ["compose","functions","together","output","first",
    // "function","input","second"]
    

    现在,让我们回忆一下第 3 章中出现的 partialRight(..) 来让组合变的更有趣。我们能够构造一个由 compose(..) 自身组成的右偏函数应用,通过提前定义好第二和第三参数(unique(..)words(..));我们把它称作 filterWords(..)(如下)。

    然后,我们能够通过多次调用 filterWords(..) 来完成组合,但是每次的第一参数却各不相同。

    function skipShortWords(list) {
        return list.filter(str => str.length > 4)
    }
    function skipLongWords(list) { 
        return list.filter(str => str.length <= 4)
    }
    
    var filterWords = partialRight( compose, unique, words );
    
    var biggerWords = filterWords( skipShortWords );
    var shorterWords = filterWords( skipLongWords );
    
    biggerWords( text );
    // ["compose","functions","together","output","first",
    // "function","input","second"]
    
    shorterWords( text );
    // ["to","two","pass","the","of","call","as"]
    

    花些时间考虑一下基于 compose(..) 的右偏函数应用给了我们什么。

    甚至我们可以结合前面一章的notwhenidentity函数来重构一下:

    // 取反辅助函数
    function not(predicate) {
        return function negated(...args) {
            return !predicate(...args)
        }
    }
    // 判断某个条件成立之后执行fn
    function when(predicate, fn) {
        return function conditional(...args) {
            if (predicate(...args)) {
                return fn(...args)
            }
        }
    }
    // 传一个返回一个
    function identity(v) {
        return v;
    }
    
    var isLong = (str) => str.length > 4;
    var isShort = not(isLong)
    var returnLong = when(isLong, identity)
    var returnShort = when(isShort, identity)
    
    function skipShortWords(list) {
        return list.filter(str => returnLong(str))
    }
    function skipLongWords(list) {
        return list.filter(str => returnShort(str))
    }
    
    var filterWords = partialRight(compose, unique, words)
    
    var biggerWords = filterWords(skipShortWords)
    var shorterWords = filterWords(skipLongWords)
    
    biggerWords( text );
    // ["compose","functions","together","output","first",
    // "function","input","second"]
    
    shorterWords( text );
    // ["to","two","pass","the","of","call","as"]
    

    compose的不同实现方式

    reduce实现

    function compose(...fns) {
        return fns.reverse().reduce( function reducer(fn1,fn2){
            return function composed(...args){
                return fn2( fn1( ...args ) );
            };
        } );
    }
    
    // ES6 箭头函数形式写法
    var compose =
        (...fns) =>
            fns.reverse().reduce( (fn1,fn2) =>
                (...args) =>
                    fn2( fn1( ...args ) )
            );
    

    递归实现

    
    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 );
        };
    

    pipe函数

    我们早期谈及的是从右往左顺序的标准 compose(..) 实现。这么做的好处是能够和手工组合列出参数(函数)的顺序保持一致。

    不足之处就是它们排列的顺序和它们执行的顺序是相反的,这将会造成困扰。同时,不得不使用 partialRight(compose, ..) 提早定义要在组合过程中 第一个 执行的函数。

    相反的顺序,从右往左的组合,有个常见的名字:pipe(..)

    pipe(..)compose(..) 一模一样,除了它将列表中的函数从左往右处理。

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

    实际上,我们只需将 compose(..) 的参数反转就能定义出来一个 pipe(..)

    var pipe = reverseArgs( compose );
    

    回忆下之前的通用组合的例子:

    var biggerWords = compose( skipShortWords, unique, words );
    

    pipe(..) 的方式来实现,我们只需要反转参数的顺序:

    var biggerWords = pipe( words, unique, skipShortWords );
    

    pipe(..) 的优势在于它以函数执行的顺序排列参数,某些情况下能够减轻阅读者的疑惑

    抽象

    先来介绍2个简单且实用的函数

    prop函数

    将任意对象的任意属性通过属性名提取出来。让我们把这个实用函数称为 prop(..)

    function prop(name,obj) {
        return obj[name];
    }
    
    // ES6 箭头函数形式
    var prop =
        (name,obj) =>
            obj[name];
    

    使用:

    var obj = { x: 1, y: 2 }
    
    prop('x', obj)
    // 1
    

    setProp函数

    我们处理对象属性的时候,也需要定义下反操作的工具函数:setProp(..),为了将属性值设到某个对象上。

    function setProp(name,obj,val) {
        var o = Object.assign( {}, obj );
        o[name] = val;
        return o;
    }
    

    使用:

    var obj = { x: 1, y: 2 }
    
    var obj2 = setProp('z', obj, 3)
    // { x: 1, y: 2, z: 3 }
    

    makeObjProp函数

    function makeObjProp(name,value) {
        return setProp( name, {}, value );
    }
    
    // ES6 箭头函数形式
    var makeObjProp =
        (name,value) =>
            setProp( name, {}, value );
    

    提示: 这个实用函数在 Ramda 库中被称为 objOf(..)

    回顾ajax案例

    让我们回顾一下第二章介绍的ajax案例

    function ajax (url, data, callback) {
        // ...
    }
    var getUser = partial( ajax, "/api/user" );
    var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );
    
    var output = (str) => console.log(sgr);
    
    getLastOrder(function orderFound(order) {
      getUser({ userId: order.userId }, function userFound (user) {
        output(user.name)
      })
    })
    

    如上,我们在给getLastOrder函数传递最后一个参数(一个回调函数orderFound)

    该函数用查询到的订单信息order中的userId查询当前订单的用户,并输出用户的姓名name.

    可以看到上面的函数需要orderuser两个形参。

    我们可以用现有的函数式编程的知识将其转化为一个无形参的函数getLastOrder.

    移除user形参

    从里向外,我们先想想如何移除user这个形参。

    首先output函数是需要接收user.name这个参数的。我们可以用什么样的方式来移除这个参数呢。

    在这里我们的目的是想要获取user中的name属性:

    定义一个extractName函数:

    var extractName = partial(prop, 'name')
    

    之后我们就可以直接用:

    extractName(user)
    

    这样的方式获取到user.name

    接着你是不是也想到可以用compose了呢?

    var outputUserName = compose( output, extractName )
    

    想一下我们需要的数据流是什么样:

    output <-- extractName <-- user
    

    下一步,让我们缩小关注点,看下例子中嵌套的这块查找操作的调用:

    getLastOrder( function orderFound(order){
        getUser( { userId: order.userId}, outputUserName );
    } );
    

    我们刚刚创建的 outputUserName(..) 函数是提供给 getUser(..) 的回调。所以我们还能定义一个函数叫做 processUser(..) 来处理回调参数,使用 partialRight(..)

    var processUser = partialRight( getUser, outputUserName )
    

    让我们用新函数来重构下之前的代码:

    getLastOrder(function orderFound(order) {
        processUser({ userId: order.userId })
    })
    

    Ok,至此,user这个形参已经被我们干掉了。

    移除order形参

    接下来你可以用类似的方式移除掉order形参。

    首先是获取userId

    var extractUserId = partial(prop, 'userId')
    

    接着你需要定义一个函数来解决{ userId: order.userId }这个问题。我们可以使用上面的makeObjProp函数:

    var userData = partial(makeObjProp, 'userId')
    

    为了使用 processUser(..) 来完成通过 order 值查找一个人的功能,我们需要的数据流如下:

    processUser <-- userData <-- extractUserId <-- user
    

    所以我们只需要再使用一次 compose(..) 来定义一个 lookupUser(..)

    var lookupUser = compose( processUser, userData, extractUserId )
    

    完整流程

    然后,就是这样了!把这整个例子重新组合起来,不带任何的“形参”:

    function ajax (url, data, callback) {
        // ...
    };
    var getUser = partial( ajax, "/api/user" );
    var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );
    
    var output = (str) => console.log(sgr);
    
    var extractName = partial(prop, 'name');
    var outputUserName = compose( output, extractName );
    var processUser = partialRight( getUser, outputUserName )
    var extractUserId = partial(prop, 'userId')
    var userData = partial(makeObjProp, 'userId')
    var lookupUser = compose( processUser, userData, extractUserId )
    
    getLastOrder( lookupUser )
    

    Look!不带任何的形参。

    总结

    函数组合是一种定义函数的模式,它能将一个函数调用的输出路由到另一个函数的调用上,然后一直进行下去。

    因为 JS 函数只能返回单个值,这个模式本质上要求所有组合中的函数(可能第一个调用的函数除外)是一元的,当前函数从上一个函数输出中只接收一个输入。

    相较于在我们的代码里详细列出每个调用,函数组合使用 compose(..) 实用函数来提取出实现细节,让代码变得更可读,让我们更关注组合完成的是什么,而不是它具体做什么

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

    相关文章

      网友评论

        本文标题:霖呆呆的函数式编程之路(四)

        本文链接:https://www.haomeiwen.com/subject/tyodhctx.html