zoukankan      html  css  js  c++  java
  • javascript 函数式编程(2)

    (以下大部分内容翻译自 eloquent javascript 第7章)

    来看一个寻路算法的例子:

    地图 

    如图所示,连线上的数字表示这两点之间的距离,现在要写一个算法能够计算出两点间的最短路径。

    别急着往下看,认真地想一下有哪几种可行方案,有哪些可能会碰到的问题。在阅读技术文章时,经常是快速翻阅一遍内容,若有所悟地点点头,然后马上就忘了。如果你真正努力地去解决某个问题,那它就会变成你的问题,它的解决方案也更有意义。

    首先,用数据来描述这个地图,我们使用一个object:

     

    这个roads包含了小岛上所有的路径信息,当我们从某处出发,想要知道有哪些路时,用roads[place]就可以得到,但如果万一打错地名,把place拼错了,就可能意外地返回一个undefined,这可能造成一些很隐蔽的奇怪错误,所以我们用一个函数来获取它

    function roadsFrom(place) {  
        var found = roads[place];  
        if (found == undefined) throw new Error("No place named '" + place + "' found.");  
        else return found;  
    }  
    show(roadsFrom("Puamua"));  
    

    现在开始尝试一种老老实实的做法,"生成,测试",就像这样:

      1. 生成所有可行的路径。

      2. 在其中找出最短的路径。

    对于一个小型的地图来说,生成所有路径的做法是可行的。

     在这之前,我们需要一些新的工具函数,第一个是member,用来检查一个数组中是否包含某个元素。路径是由很多地名组成的数组,当我们到达一个新地方时,我们会调用member来检查这个地方是否已经到过,像这样:

    function member(array, value) {  
        var found = false;  
        forEach(array, function(element) {  
            if (element === value) found = true;  
        });  
        return found;  
    }  
    print(member([6, 7, "Bordeaux"], 7)); // true  
    

    然而,即使我们要找的值出现在数组的第一项,它也会继续遍历完整个数组,这是没有必要的。当使用for循环时候,我们可以用break来跳出循环,但在forEach中这将不管用,因为循环体是一个函数,break不能跳出函数。解决方法是把forEach改造成能够知道什么时候该停止遍历。

    function forEach(array, action) {  
        for (var i = 0; i < array.length; i++) {  
            if(action(array[i]) === false) {  
                return;  
            }  
        }  
    }  

    现在,一旦action函数返回falseforEach就会停止遍历。即使member函数使用了forEach,还是显得臃肿且不够通用,它需要在内部维护一个found,最终还要返回它,使得这个函数显得太有针对性和过于麻烦。member实现的是:判断在一组数据中是否有任何满足条件的元素,这个任何可以被抽象出来,成为any函数

    function any(test, array) {  
        for (var i = 0; i < array.length; i++) {  
            var found = test(array[i]);  
            if (found) return found;  
        }  
        return false;  
    }  
    function member(array, value) {  
        return any(partial(op["==="], value), array);  
    }  
    print(member(["Fear", "Loathing"], "Denial")); 

    any遍历整个数组,对每个元素应用test函数,一旦有测试结果为真值,就返回那一项,如果都不满足,返回false。使用any(test, array)就相当于test(array[0]) || test(array[1]) || ...

    就像&&有个||为伴, any也有一个与之对应的every

    function every(test, array) {  
        for (var i = 0; i < array.length; i++) {  
            var found = test(array[i]);  
            if (!found) return found;  
        }  
        return true;  
    }  
    show(every(partial(op["!="], 0), [1, 2, - 1]));  
    

    我们还需要另一个函数flatten,它接受一个二维数组,将其合并到一个一维数组中:

    function flatten(arrays) {  
        var result = [];  
        forEach(arrays, function(array) {  
            forEach(array, function(element) {  
                result.push(element);  
            });  
        });  
        return result;  
    }  
    

    当然flattern也可以这样实现:

    function flattern2(arrays) {  
        return reduce(function(base, array) {  
            return base.concat(array);  
        }, [], arrays);  
    }  
    console.info(flattern2([[1, 3, 5], [2, 4]]));  
    // 结果为: [1, 3, 5, 2, 4]  
    

    但这种做法效率较低,就像反复连接字符串,不如把字符串都加入到数组中然后join起来更高效一样,重复地concat数组会产生很多没必要的临时数组。

    在生成路径之前,我们还需要一个函数filter,就像map一样,它接收2个参数,test函数和一个数组array,用于过滤掉array中不满足test的元素。

    function filter(test, array) {  
        var result = [];  
        forEach(array, function(element) {  
            if (test(element)) result.push(element);  
        });  
        return result;  
    }  
    show(filter(partial(op[">"], 5), [0, 4, 8, 12]));  
    // 结果为: [0, 4]  
    

    (上面的例子中filter的返回值是否令人不解?记住传给partial的参数,将被用在函数的第一个参数,这里传给partial5,将被作用在op[">"]的第一个参数,所以上面的表达式意思并不是 "所有大于5的值",而是 "5大于的值",即 "比5小的值")

    现在,整个生成路径的算法看上去应该是这样 -- 从出发点开始,对每一条离开那里的路生成路径,我们使用递归来做:

    function possibleRoutes(from, to) {  
        function findRoutes(route) {  
            function notVisited(road) {  
                return ! member(route.places, road.to);  
            }  
            function continueRoute(road) {  
                return findRoutes({  
                    places: route.places.concat([road.to]),  
                    length: route.length + road.distance  
                });  
            }  
            var end = route.places[route.places.length - 1];  
            if (end == to)   
                return [route];  
            else   
                return flatten(map(continueRoute, filter(notVisited, roadsFrom(end))));  
        }  
        return findRoutes({  
            places: [from],  
            length: 0  
        });  
    }  
    show(possibleRoutes("Point Teohotepapapa", "Point Kiukiu").length); // 11  
    show(possibleRoutes("Hanapaoa", "Mt Ootua")); // [{ places: ["Hanapaoa", "Mt Ootua"], length: 3 }]
    

    这个函数返回一个路径数组,每一条路径都包含一个所通过节点的数组,和一个总长度。findRoutes递归循环一条路径,返回一个路径的所有延伸。当一条路径的末尾地点是我们的目的地时,它将返回那条路径,因为继续在那条路径上走是没有意义的。如果末尾是其他的地点,我们就继续。包含了flatten/map/filter的那行可能是最难读的,它描述的是:得到所有从当前节点出发的临近节点,从其中过滤掉那些已经访问过的,然后对每一个节点继续进行continueRoute,由于每一个节点都将返回一个路径数组,多个节点map后将返回一个二维数组(第一维对应应节点,第二维对应该节点的路径数组),所以需要用flatten把这二维数组并成一维数组(因为我们并不关心节点,只需要所有的路径数组)。

     这一行做了很多事,这就是抽象带来的好处:不用打满屏幕的代码就能做很复杂的事。

     现在我们有了所有可行的路径,开始尝试找出其中最短的那条。需要一个函数shortestRoute
     

    function shortestRoute(from, to) {  
        var currentShortest = null;  
        forEach(possibleRoutes(from, to), function(route) {  
            if (!currentShortest || currentShortest.length > route.length)   
                currentShortest = route;  
        });  
        return currentShortest;  
    }  
    

    也许你已经想到了,最短的可以被抽象出来 

    function minimise(func, array) {  
        var minScore = null;  
        var found = null;  
        forEach(array, function(element) {  
            var score = func(element);  
            if (minScore == null || score < minScore) {  
                minScore = score;  
                found = element;  
            }  
        });  
        return found;  
    }  
    function getProperty(propName) {  
        return function(object) {  
            return object[propName];  
        };  
    }  
    function shortestRoute(from, to) {  
        return minimise(getProperty("length"), possibleRoutes(from, to));  
    }  
    

    这种最xx的形式还可以抽象出most函数:

    function most(arr, comp) {  
        var min = arr[0];  
        forEach(arr, function(a) {  
            if(comp(min, a)) {  
                min = a;  
            }  
        });  
        return min;  
    }  
    function getProperty(p) {  
        return function(o) {  
            return o[p];  
        };  
    }  
    function minimal(arr, getter) {  
        return most(arr, function(a, b) {  
            return getter(b) < getter(a);  
        });  
    }  
      
    function shortestRoute(from, to) {  
        return minimal(possibleRoutes(from, to), getProperty('length'));  
    }  
    console.info(shortestRoute("Point Teohotepapapa", "Point Kiukiu"));  
    

    看上去为一个shortestRoute写了这么多行代码,比起第一个版本长了3倍,但这些代码是可重用的。

    注意其中的getProperty,这种做法在函数式编程里经常是很有用的。

  • 相关阅读:
    lower_bound &&upper_bound
    二分/三分
    $qsort$
    define
    typedef
    string
    queue
    nyoj Arbitrage (Bellman-Ford)
    nyoj 谍战 (最小割最大流)
    nyoj 网络的可靠性(。。。)
  • 原文地址:https://www.cnblogs.com/aj3423/p/3150515.html
Copyright © 2011-2022 走看看