函数式编程

v2-5072f9061e3bd1c5d8457bdeff24064c_r

有关jMiniLang的说明:http://files.cnblogs.com/files/bajdcc/jMiniLang-manual.pdf

================================

看了Haskell 这段代码该如何理解?今天突发奇想,尝试一把函数式编程的感觉~

v2-3b2c7ba9dd8d494555bd2b260c07e87f_r

最近把bajdcc/jMiniLang继续修整了一下,修了点bug,界面开了抗锯齿,顿时美观度大增哈哈。

依照【游戏框架系列】诗情画意 - 知乎专栏的惯例,增加了一言 - ヒトコト - Hitokoto.us以测试Web的效果,当前的网络线程是在main thread中操作的,以后会放在worker thread中实现异步。

本文要完成几个任务:

  1. 实现add,add接受一个数组作为参数,使命是将数组中所有元素相加
  2. 实现字符串翻转reverse,如将12345翻成54321
  3. 实现haskell中的<*> Applicative功能
  4. 实现回文数判断

预备知识:Lambda

其实lambda只是没有名字的函数(事实上我还是给它们命名了),更加关键的地方是闭包,如果没有闭包,那lambda只是一个没有卵用的语法糖罢了。

var xs = func ["数组遍历闭包"] ~(l) {
    var len = call g_array_size(l);
    var idx = 0;
    var f = func ~() {
        if (idx == len) { return g__; }
        var d = call g_array_get(l, idx);
        idx++;
        var f2 = func ~() -> d;
        return f2;
    };
    return f;
};

上面是一个例子,f以及f2是一个lambda,然而在lambda中竟然出现了外层变量的引用。没错,这就是难点所在。

看另一个:

var ff = func ~(f) {
    var fh = func ~(h) {
        return call h(h);
    };
    var fx = func ~(x) {
        var fn = func ~(n) {
            var vx = call x(x);
            var vf = call f(vx);
            return call vf(n);
        };
        return fn;
    };
    return call fh(fx);
};
var fact = func ~(f) {
    var fk = func ~(n) {
        if (n > 0) {
            return n * call f(n - 1);
        } else {
            return 1;
        };
    };
    return fk;
};
var ffact = call ff(fact);
var fact_5 = call ffact(5);
call g_printn(fact_5);

上面这个例子就比较玄乎了,有关知识在此Y Combinator - bajdcc - 博客园,用lambda实现了 个调用自身的功能。

不管那么多,这里需要实现这种闭包机制,思路也很简单,就是给lambda加一层环境,把绑定的变量塞进这环境中,那么调用执行的时候,首先从lambda绑定的环境中查找变量,找不到的话再找外层变量。经过实践,这个方法管用。

基本函数

https://github.com/bajdcc/jMiniLang/blob/master/src/priv/bajdcc/LALR1/interpret/module/ModuleFunction.java
var max = func ~(a, b) -> a > b ? a : b;
var min = func ~(a, b) -> a < b ? a : b;
var lt = func ~(a, b) -> a < b;
var lte = func ~(a, b) -> a <= b;
var gt = func ~(a, b) -> a > b;
var gte = func ~(a, b) -> a >= b;
var eq = func ~(a, b) -> a == b;
var neq = func ~(a, b) -> a != b;
var add = func ~(a, b) -> a + b;
var sub = func ~(a, b) -> a - b;
var mul = func ~(a, b) -> a * b;
var div = func ~(a, b) -> a / b;

上面定义了一些函数,全是二元的,比较简单和常用。

var curry = func ~(a, b) {
    var f = func ~(c) -> call a(b, c);
    return f;
};
var swap = func ~(a) {
    var f = func ~(b, c) -> call a(c, b);
    return f;
};

上面定义了curry和swap。curry就是先绑定函数的第一参数,调用时只需要提供第二个参数即可。swap的参数是一个二元函数,它的用处就是将两个参数互换。

初步尝试

先解决第一个问题:实现add函数。

var add = func ~(list) {
    var sum = 0;
    foreach (var i : call g_range_array(list)) {
        sum += i;
    }
    return sum;
};

先除去数组为空的例外情况,上面add的实现代码是循环结构,没有什么好斟酌的了。然而它并没有函数式编程那样的逼格,我们希望的style是:

func add(list) {
    if (call g_array_empty(list)) { return 0; }
    var head = call g_array_head(list); // 得到数组第一个元素
    var tail = call g_array_tail(list); // 得到去除第一个元素后的数组
    return head + call add(tail);
};

有了递归才有意思嘛!得到head和tail的代码就可以略去了,细心的人可能发现:得到tail数组是重建一个新数组呢还是直接在原有数组上修改?如果是原有基础上修改,那么就破坏了数组的只读性,导致麻烦;如果是new一个新数组,那么就要给GC添加更新难题了。

所以,妥协之下,只能new新数组了吗?注意到数组的只读性,其实只要获取索引就可以了!假如数组是1,2,3,4,5...那么现在给list包装一下,得到f=decorate(list),要求f()的结果是1,2,3,4,5....,到末尾给个null,这能实现吗?

以往的思想,调用一个纯函数,无论多少次、什么情况下,函数的结果是一成不变的。可是现在我却要求它每次调用的结果不一样!那么它就不再是纯函数了,它必定有所依赖。这个依赖我可以显现给出,也可以懒得去做。那么后者就是闭包。

再次尝试

下面实现的关键:

xs函数的使命是将作为数组的参数l进行包装,返回一个闭包closure,如果数组l没有访问到结尾,那么调用closure()就会返回一个lambda(调用 lambda()后得到数据),如果访问到结尾,返回null。
var xs = func ["数组遍历闭包"] ~(l) {
    var len = call g_array_size(l);
    var idx = 0;
    var f = func ~() {
        if (idx == len) { return g__; // =null }
        var d = call g_array_get(l, idx);
        idx++; // 索引自增
        var f2 = func ~() -> d;
        return f2;
    };
    return f;
};

xs这个闭包返回了一个可调用函数f,而f依赖的len和idx处于xs内部,外界不可能访问到,因此是安全的。

那如何使用这个闭包呢?

var g_func_1 = func ~(a) -> a; // 返回自身
var g_func_apply = func ~(name, list) {
    return call g_func_apply_arg(name, list, "g_func_1");
};
export "g_func_apply";
var g_func_apply_arg = func ~(name, list, arg) { // 带参数版本
    var len = call g_array_size(list); // 计算大小
    if (len == 0) { return g__; } // 返回空
    if (len == 1) { return call g_array_get(list, 0); } // 返回第一个
    var x = call xs(list);
    var val = call x();
    let val = call val();
    var n = name;
    let n = call arg(n); // 装饰原有函数,如调换两参数位置啊
    for (;;) {
        var v2 = call x(); // 取数组下一个数
        if (call g_is_null(v2)) { break; } // 遇到结尾
        let v2 = call v2();
        let val = call n(val, v2); // 累计处理
    }
    return val;
};
export "g_func_apply_arg";

x就是作为一个闭包,每次调用x(),返回一个lambda,执行后得到值。将值进行累加计算:call n(val, v2),其中n就是要调用的二元基本函数,在文中前面列举了。不断累计,直到闭包返回一个null,中止循环,返回计算结果。

g_func_apply(name, list)的意义:将list数组进行累计(参照C#中Aggregate函数),用name指代的二元操作作为衔接。

g_func_apply_arg(name, list, arg)的意义:将list数组进行累计(参照C#中Aggregate函数),用name指代的二元操作作为衔接,其中二元操作经arg函数修饰(如arg=swap时且结合不满足交换律时,结果就是反的)。

那么到这里,第一个任务完成了!

剩下的任务

字符串翻转

var g_string_reverse = func ~(str) -> 
  call g_func_apply_arg("g_func_add", call g_string_split(str, ""), "g_func_swap");

这里的窍门在于swap函数。

Haskell 中的 <*> Applicative

var g_func_applicative = func ~(f, a, b) -> call f(a, call b(a));

实际意义是:applicative(f, a, b) = f(a, b(a))

回文数判断

var val = call g_func_applicative("g_func_eq", str, reverse);
  • g_func_eq 等于 ==
  • str 例如 12321
  • reverse 即字符串翻转函数

所以原式即:str == reverse(str)。

总结

匆匆忙忙只是完成了函数式编程的第一步,后面会慢慢补上几个好玩的特性,如map、fold等。g_func_apply其实等同于Haskell中的foldl。

https://zhuanlan.zhihu.com/p/26804202备份。


(二)

v2-48ae07a0298a343b5869b6f7083069b5_r

写在前面

接过一番折腾,终于实现了fold,take,map,zip等函数,虽说与haskell比还差得远,我也没做啥优化,就是体会一下函数式的风格。

简而言之,就是将函数作为参数传来传去,所以除了传值之外,还可以传行为

一切的基础

分析了下haskell的主要函数,归纳起来,发现fold函数是非常基础的,它就相当于卷积,将一个表拍成一个元素。

对于表list,一般有两部分 x:xs,表头和表尾,haskell的实现是将表切开成x:xs,不过有了fold之后,就可以忽略x:xs了。

我们先设计两个list遍历函数:

var g_func_xsl = func ["数组遍历闭包-foldl"] ~(l) {
    var len = call g_array_size(l);
    var idx = 0;
    var _xsl = func ~() {
        if (idx == len) { return g__; }
        var d = call g_array_get(l, idx);
        idx++;
        var _xsl_ = func ~() -> d;
        return _xsl_;
    };
    return _xsl;
};
export "g_func_xsl";
var g_func_xsr = func ["数组遍历闭包-foldr"] ~(l) {
    var idx = call g_array_size(l) - 1;
    var _xsr = func ~() {
        if (idx < 0) { return g__; }
        var d = call g_array_get(l, idx);
        idx--;
        var _xsr_ = func ~() -> d;
        return _xsr_;
    };
    return _xsr;
};
export "g_func_xsr";

其中,xsl是从左向右遍历,xsr则是反过来。做这个闭包的好处是:不创建新的list。

再创建几个默认值:

var g_func_1 = func ~(a) -> a;
export "g_func_1";
var g_func_always_1 = func ~(a) -> 1;
export "g_func_always_1";
var g_func_always_true = func ~(a) -> true;
export "g_func_always_true";

接下来才是主角——fold函数

var g_func_fold = func 
    [
        "函数名:g_func_fold",
        "参数解释:",
        "  - name: 套用的折叠函数",
        "  - list: 需处理的数组",
        "  - init: 初始值(不用则为空)",
        "  - xs: 数组遍历方式(xsl=从左到右,xsr=从右到左)",
        "  - map: 对遍历的每个元素施加的变换",
        "  - arg: 对二元操作进行包装(默认=g_func_1,例=g_func_swap)",
        "  - filter: 对map后的元素进行过滤(true则处理)"
    ]
    ~(name, list, init, xs, map, arg, filter) {
    var len = call g_array_size(list);
    if (len == 0) { return g__; } // 肯定返回空
    var val = g__;
    var x = g__;
    if (call g_is_null(init)) { // 没初值的话,取第一个元素为初值
        if (len == 1) { return call g_array_get(list, 0); } // 只有一个元素
        let x = call xs(list); // 创建遍历闭包
        let val = call x(); // 取第一个元素
        let val = call val();
        let val = call map(val); // 对元素进行变换
    } else {
        let x = call xs(list);
        let val = init;
    }
    var n = name; // 对数组进行变换
    let n = call arg(n); // 对卷积方式进行变换
    for (;;) { // 遍历数组
        var v2 = call x(); // 取得下一元素
        if (call g_is_null(v2)) { break; } // 没有下一元素,中止
        let v2 = call v2(); // 下一元素
        let v2 = call map(v2); // 对下一元素进行变换
        if (call filter(v2)) { // 过滤控制
            let val = call n(val, v2); // 将两元素进行处理
        }
    }
    return val;
};
export "g_func_fold";

做了一个看doc的功能。我把中文和英文弄成一样大小,方便输出。

Fold的应用

var g_func_apply = func ~(name, list) ->
    call g_func_apply_arg(name, list, "g_func_1");
export "g_func_apply";
var g_func_apply_arg = func ~(name, list, arg) ->
    call g_func_fold(name, list, g__, "g_func_xsl", "g_func_1", arg, "g_func_always_true");
export "g_func_apply_arg";
var g_func_applyr = func ~(name, list) ->
    call g_func_applyr_arg(name, list, "g_func_1");
export "g_func_applyr";
var g_func_applyr_arg = func ~(name, list, arg) ->
    call g_func_fold(name, list, g__, "g_func_xsr", "g_func_1", arg, "g_func_always_true");
export "g_func_applyr_arg";
// ----------------------------------------------
var g_func_map = func ~(list, arg) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsl", arg, "g_func_1", "g_func_always_true");
export "g_func_map";
var g_func_mapr = func ~(list, arg) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsr", arg, "g_func_1", "g_func_always_true");
export "g_func_mapr";
var g_func_length = func ~(list) ->
    call g_func_fold("g_func_add", list, 0, "g_func_xsl", "g_func_always_1", "g_func_1", "g_func_always_true");
export "g_func_length";
var g_func_filter = func ~(list, filter) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsl", "g_func_1", "g_func_1", filter);
export "g_func_filter";
// ----------------------------------------------
var take_filter = func ~(n) {
    var idx = 0;
    var end = n;
    var _take_filter = func ~(a) -> idx++ <= end;
    return _take_filter;
};
var drop_filter = func ~(n) {
    var idx = 0;
    var end = n;
    var _drop_filter = func ~(a) -> idx++ > end;
    return _drop_filter;
};
var g_func_take = func ~(list, n) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsl", "g_func_1", "g_func_1", call take_filter(n));
export "g_func_take";
var g_func_taker = func ~(list, n) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsr", "g_func_1", "g_func_1", call take_filter(n));
export "g_func_taker";
var g_func_drop = func ~(list, n) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsl", "g_func_1", "g_func_1", call drop_filter(n));
export "g_func_drop";
var g_func_dropr = func ~(list, n) ->
    call g_func_fold("g_array_add", list, g_new_array, "g_func_xsr", "g_func_1", "g_func_1", call drop_filter(n));
export "g_func_dropr";

上面的代码实践证明:用fold可以实现map、filter、take、drop等功能。

Zip的实现

由于fold是对单一数组进行卷积/聚集,而zip的对象是两个数组,所以不兼容,只好另写了。

var func_zip = func ~(name, a, b, xs) {
    var val = [];
    var xa = call xs(a);
    var xb = call xs(b);
    for (;;) {
        var _a = call xa();
        var _b = call xb();
        if (call g_is_null(_a) || call g_is_null(b)) {
            break;
        }
        var c = call name(call _a(), call _b());
        call g_array_add(val, c);
    }
    return val;
};
var g_func_zip = func ~(name, a, b) ->
    call func_zip(name, a, b, "g_func_xsl");
export "g_func_zip";
var g_func_zipr = func ~(name, a, b) ->
    call func_zip(name, a, b, "g_func_xsr");
export "g_func_zipr";

总体跟fold差不多。

总结

jMiniLang我实现了闭包,就可以大搞函数式编程了。其实里面更复杂的是类型判断、代码优化等内容,所以这里也就是尝尝鲜尝了。等到把bajdcc/CParser改一下,以c语言去支持lambda就绝了。

https://zhuanlan.zhihu.com/p/26912674备份。

原文地址:https://www.cnblogs.com/bajdcc/p/8972975.html