在上文我们定义了if函数还存在一个问题,那就是当参数是atom或简单的不涉及递归的函数时一切正常,如果涉及到递归函数就出问题了,这里有一个经典的例子

(define fact (lambda ( n)
  (if (= n 1)
      1
      (* n (fact (- n 1))))))
(fact 5)

然后会出现StackOverflowError的异常,让我们分析一下为什么会出现异常呢?
首先我们先看一下我们自定义的if函数
if

 (define if (lambda (p then_v else_v)
     ((or (and p car) cdr) (cons then_v else_v))))

问题是怎么产生的呢?

原因在于 if函数的 p、then_v、 else_v的这三个入参对应的表达式,作为入参时会在方法被调用时解开(执行获得结果 (getAtom)),那为什么 atom 或 不涉及递归的函数正常呢?
原因在于getAtom这个方法: 在 o 也就是入参 是 表达式或变量时会执行eval,而入参是atom 或 function 则直接返回 o

private static Object getAtom(Object o, Cons exp, Env env) {
    if (IS_EXP.test(o)) {
        return eval((Cons) o, env);
    } else if (IS_SYMBOLS.test(o)) {
        return eval(toSubCons(o, exp), env);
    } else {
        return o;
    }
}

当执行 fact 函数时,我们会发现 fact 中 if函数 入参 else_v 对应的是表达式 (* n (fact (- n 1))) 会被 getAtom eval 解释执行 ,当 else_v 被 eval 解释时 fact再次被执行,于是陷入了循环中从而出现了StackOverflowError的异常。
就像这样

(fact -> if -> else_v(* n (fact (- n 1))) -> getAtom -> eval ->
    (fact -> if -> else_v(* n (fact (- n 1))) -> getAtom -> eval ->
        (fact -> ...)))

怎么解决

从刚才的分析我们知道了else_v对应的表达式在getAtom方法中执行eval导致的问题出现,那么只要不触发 getAtom eval即可,这里对应的是 else_v不被执行,恰好我们的lambda可以做到这点(懒加载 延迟执行),让我们改造一下 fact函数

(define fact (lambda ( n)
  (if (= n 1)
      1
     (lambda () (* n (fact (- n 1)))))))

然后执行 (fact 5) 得到了 120 的结果,一切正常了,(还是通过一个中间层即可解决的问题)然而这样写也太麻烦了吧, 我们可以写一个 函数简化 (lambda () ()) 这步操作,于是我们有了 lazyfun 这个函数

(define lazy-fun (lambda (exp) (
    lambda () (exp))))

然后我们给它起个别名 持有表达式引用的函数

(define quote lazy-fun)

我们fact 函数变成了这样

(define fact (lambda ( n)
  (if (= n 1)
      1
     ( quote (* n (fact (- n 1)))))))

执行 (fact 5)后又出现了 StackOverflowError, 奇怪了难道

(define a (lambda () 
    (* n (fact (- n 1)))))
(define quote (lambda (exp) (
    lambda () (exp))))
(define b (quote (* n (fact (- n 1)))))

a b 不等价吗?从数学上来看是等价的,但从我们程序上来看他们并不是一样的表达式结构,
首先让我们看一下 a,在看a 之前先放上我们解释器解释lambda 关键字的源码

private static Function<ApplyArgs, Object> lambda(Cons cdr, Env env) {
    return (applyArgs) -> {
        Object[] x = applyArgs.getLazyArgs().get();
        Cons args = cdr.carCons();
        Cons body = cdr.cdr();
        validateTrue(args.data().size() == x.length, cdr.parent()+"参数不一致");
        Env env0 = Env.newInstance(env);
        int i = 0;
        for (Object argName : args) {
            env0.setEnv(((Symbols) argName), x[i]);
            i++;
        }
        return applyArgs.getEval().apply(body, env0);
    };
}

接着看a ,a 在上面源码中cdr对应的是 (() (* n (fact (- n 1)))),x 对应的是一个空数组;

而b 在定义时会先解释 quote, quote cdr对应的部分是((exp) (lambda () (exp))), x对应的是 需要 eval 解释的 (* n (fact (- n 1))) 的一个元素的数组;

问题便是出在了 quote 的 x 这里 他和 "问题是怎么产生的呢?" 原因是一样的 参数x 中表达式 (* n (fact (- n 1))) 作为入参会被eval解释,而在被解释时 陷入了 fact 的循环中了,于是出现了StackOverflowError。

一般问题到这里就告一段落,但 (lambda () (exp)) 写还是太啰嗦了 能不能 简单些 像 (quote exp) 这样呢? 答案是有的

我们只需要自定义一个内置函数即可

quote

 private static Function<ApplyArgs, Object> quote(Cons cdr, Env env) {
    return (applyArgs) -> applyArgs.getEval().apply(cdr.carCons(), env);
}

还有一个小问题 懒加载 延迟执行 是怎么做到的呢?

答案便是高阶函数,问题高阶函数是怎么做到的呢?

答案是 apply(v, cdr, env)

当 apply 执行时 内置的lambda函数返回了一个函数(叫r吧),r函数做为结果被返回了,并没有再次被 apply ,这个时候因为r还是函数不会被getAtom 解开,直到r作为变量或表达式时被 apply(v, cdr, env) 到才会执行,从而将r解开,
r解开的过程 applyArgs.getEval().apply(body, env0) (等价于 eval(body, env0)) 这行代码会被执行到于是又进入了下一个循环中,直到程序执行完。

总结

又是一个中间层即可解决的问题,好像也能理解函数编程普及度没有面向对象普及度高的原因了,太多的递归中产生了过多的心智负担,apply 和 eval 是一对互相调用的好兄弟,但要记得有退出条件哦,不要陷入无限互相调用的中去从而产生StackOverflowError。


yangrd
1.3k 声望225 粉丝

代码改变世界,知行合一。