在上一篇我们已经拥有了一个简单lisp 解释器 (JLispInter)它支持:

  • 变量:a
  • 绑定:(define a 5)
  • 四则运算: + - * /
  • 函数 (lambda (x) (exp))
  • 调用 (g exp)

当时我们还留下了一些问题:

如解析器还不支持字符串,高阶函数中四则运算 define 等还不能作为入参,不支持匿名函数 ,分支判断 等

在这篇文章中我们先忽略语法树解析器,专注于解释器,花了1个多小时的时间写了一个更加完善的解释器,为了方便讲解我会将代码一段一段的放出,这样可以让我们更聚焦一些,在开始前,画了一个表达式语法元素的构成图,这是抽象之后(忽略了如 Characters,Strings,Vectors , Dotted pairs, lists 等)后面如Dotted pairs 我们可以通过函数的形式实现。

基础元素

其中 var expression 是引用类型也就是可以对其进行化简,var在此处我们用symbols进行代指;number, characters, boolean 是基础类型或原子类型(atom)不能对其进行化简,其就是最简形式;functional则是介于两者之前,在作为参数时它是一个箱子,在对其进行apply(调用)时则是打开的箱子,箱子里打开后可以是atom,也可以是functional,如果是后者则是高阶函数,也就是可以再次对其进行apply(调用)直到打开后为atom为止。

在后面我们会看到 :“道生一 , 一生二, 二生三 ,三生万物。”

预备阶段 我们还需要对 Env 进行一些改造,对解析器进行的改造我们在这里先忽略掉, Cons 对应的是前文 ExpNode (此处 Cons是ExpNode 的父类)

Env

public class Env {

    private final Map<String, Object> env = new HashMap<>();
    private Env parent;

    public static Env newInstance(Env parent) {
        Env env1 = new Env();
        env1.parent = parent;
        return env1;
    }

    public void setEnv(Symbols symbols, Object val) {
       setEnv(symbols.getName(),val);
    }

    public void setEnv(String key, Object val) {
        env.put(key, val);
    }

    public Optional<Object> env(Symbols symbols) {
        String symbolsName = symbols.getName();
        return Optional.ofNullable(env.containsKey(symbolsName) ? env.get(symbolsName) : (parent != null ? parent.env(symbols).orElse(null) : null));
    }

    public boolean contains(Symbols symbols){
        return env.containsKey(symbols.getName());
    }

    public Env parent(){
        return parent;
    }
}

道生一

JLispInter2

@Slf4j
public class JLispInter2 {


    private static final Predicate<Object> IS_EXP = o -> o instanceof Cons;

    private static final Predicate<Object> IS_SYMBOLS = o -> o instanceof Symbols;

    private static final Predicate<Object> IS_FUN = o -> o instanceof Function;

    private static final Predicate<Object> IS_ATOM = o -> o instanceof Number || o instanceof String || o instanceof Boolean;

    private static final BiPredicate<Cons, Object> CAN_APPLY = (exp, v) -> IS_FUN.test(v) && exp.isExp();

    public static Object eval(String exp) {
        Env root = Env.newInstance(null);
        return eval(Parse.parseTree(exp), Env.newInstance(root));
    }

    private static Object eval(Cons exp, Env env) {
        Object car = exp.car();
        Cons cdr = exp.cdr();
        if (IS_EXP.test(car)) {
            Object v = eval(exp.carCons(), env);
            if (CAN_APPLY.test(exp, v)) {
                return apply(v, cdr, env);
            }
            return cdr.isEmpty() ? v : eval(cdr, env);
        } else if (IS_SYMBOLS.test(car)) {
            Object v = env.env(exp.carSymbols()).orElseThrow(() -> new IllegalArgumentException(exp.parent() + ": " + exp.carSymbols() + " not define"));
            if (CAN_APPLY.test(exp, v)) {
                return apply(v, cdr, env);
            }
            return cdr.isEmpty() ? v : eval(cdr, env);
        } else if (IS_ATOM.test(car)) {
            return cdr.isEmpty() ? car : eval(cdr, env);
        } else {
            return cdr.isEmpty() ? car : eval(cdr, env);
        }
    }

    private static Object apply(Object v, Cons cdr, Env env) {
        return ((Function<ApplyArgs, Object>) v).apply(ApplyArgs.of(cdr, env, () -> cdr.data().stream().map(o -> getAtom(o, cdr, env)).toArray(), JLispInter2::eval));
    }

    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(toCons(o, exp), env);
        } else {
            return o;
        }
    }

    private static Cons toCons(Object obj, Cons parent) {
        return ExpNode.one(parent, obj);
    }

    @Value(staticConstructor = "of")
    public static class ApplyArgs {
        Cons exp;
        Env env;
        Supplier<Object[]> lazyArgs;
        BiFunction<Cons, Env, Object> eval;
    }
}

ApplyArgs 最开始的时候只有exp,和env 两个参数;后面发现有的函数会需要args,然后就又定义了一个懒加载的lazyArgs;至于eval函数则是为了当自定义函数使用时能够有一个门(这个门只对自定义函数开放),它的作用是:“为了获取一些东西要去使用某些函数或取值,而这些函数或取值可以不需要自己再去实现一遍,直接使用eval便可获取。”

此时JLispInter2还没有任何内置的函数,不过我们还是可以只用atom使用JLispInter2.eval方法。

输入

(1 2 3 4 5 6)
(1 2 3)

输出

6
3

虽然还没有任何内置函数,但从输出结果来看这是一个好的开始,接下来让我们为其增加如四则运算函数,然后再看一下它的效果,让我们开始吧。

一生二

我们先定义一个用来管理内置函数的类,我们就叫它FunManager吧

FunManager

 static  class FunManager{
        private static final Map<String, Function<ApplyArgs, Object>> FUNCTIONAL = new ConcurrentHashMap<>();

        static {
            reg("+", applyArgs -> toIntStream(applyArgs.getLazyArgs()).reduce(Integer::sum).orElse(null));
            reg("-", applyArgs -> toIntStream(applyArgs.getLazyArgs()).reduce((a, b) -> a - b).orElse(null));
            reg("*", applyArgs -> toIntStream(applyArgs.getLazyArgs()).reduce((a, b) -> a * b).orElse(null));
            reg("/", applyArgs -> toIntStream(applyArgs.getLazyArgs()).reduce((a, b) -> a / b).orElse(null));
        }

        private static void reg(String optKey, Function<ApplyArgs, Object> opt) {
            FUNCTIONAL.put(optKey, opt);
        }

        private static Stream<Integer> toIntStream(Supplier<Object[]> supplierObjs) {
            return Stream.of(supplierObjs.get()).map(Object::toString).map(Integer::valueOf).collect(Collectors.toList()).stream();
        }
}

然后让我们对JLispInter2施加一些魔法,哈哈哈

JLispInter2

public class JLispInter2 {
    public static Object eval(String exp) {
        Env root = Env.newInstance(null);
        FunManager.FUNCTIONAL.forEach(root::setEnv);
        return eval(Parse.parseTree(exp), Env.newInstance(root));
    }
}

虽然只增加了一行

 FunManager.FUNCTIONAL.forEach(root::setEnv);

但现在JLispInter2已经支持四则运算了,让我们测试一下。

输入

(+ 1 2 3 4 (+ 5 6) (+ 7 8 (+ 9 0)))
(+ 1 2 3 4 (- 9 3))

输出

45
16

从输出结果来看它符合我们的预期,这说明 var (env), expression, function, apply, atom 这些功能已经可以正常工作了,接下来让我们为其增加更多的内置函数,让其可以更强大一些吧。

二生三

既然是内置函数,那用户自定义额函数要怎么办呢?这很简单,我们只需要增加一个可以返回函数的高阶函数即可,让我们开始吧。

首先在FunManager里面增加一个高阶函数

FunManager

static  class FunManager{
     static {
        ...
        reg("lambda", applyArgs -> lambda(applyArgs.getExp(), applyArgs.getEnv()));
        ...
    }

    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, "参数不一致");
                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);
            };
    }
    private static void validateTrue(boolean flag, String err) {
        if (!flag) {
            throw new IllegalArgumentException(err);
        }
    }
}

就这么简单,只需要15行代码即可,让我们来好好的看一下它。

 return (applyArgs) -> {...}

通过上面这行代码我们返回一个函数,这对应上面我们刚提到过的高阶函数。

 Object[] x = applyArgs.getLazyArgs().get();

通过上面这行代码我们可以获得入参的值。

 Env env0 = Env.newInstance(env);

通过上面这行代码我们为当前方法在调用时创建一个新的环境。

 env0.setEnv(((Symbols) argName), x[i]);

通过上面这行代码我们为当前方法入参进行了变量绑定。

return applyArgs.getEval().apply(body, env0);

通过上面这行代码我们让eval函数帮我们解释当前方法体并得到返回值。

让我们测试一下解释器是否正常工作。

输入

((lambda (x) (+ 5 x)) 5)

输出

10

让我们再测试一下是否支持高阶函数

((lambda (x) (+ 5 x)) ((lambda (y) (* y y)) 5))
(((lambda (x) (lambda (o) (o 5 x))) ((lambda (y) (* y y)) 5)) +)

输出

30
30

效果还是很不错的,接下来让我们再增加 let cond set! apply 内置函数。

在开始前看看他们都长什么样

  • let:
    (let ((p1 v1) (p2 v2)) body)
  • cond:
    (cond
    (predicate_1 clauses_1)
    (predicate_2 clauses_2)
    ......
    (predicate_n clauses_n)
    (else clauses_else))
  • set!:
    (set! p1 v1)
  • apply:
    (apply f x y)

开始吧!

FunManager.let

static  class FunManager{
...
         private static Object let(Cons cdr, Env env, BiFunction<Cons, Env, Object> eval) {
            Object car0 = cdr.car();
            validateTrue(car0 instanceof Cons && cdr.data().size() == 2, "please check" + car0);
            Env env0 = Env.newInstance(env);
            Cons car = cdr.carCons();
            Cons body = cdr.cdr().carCons();
            for (Object con : car) {
                validateTrue(IS_EXP.test(con), "please check" + con);
                Cons item = (Cons) con;
                Symbols var = item.carSymbols();
                validateTrue(!env0.contains(var), "Do not repeat the let " + var);
                env0.setEnv(var, eval.apply(item.cdr(), env));
            }
            return eval.apply(body, env0);
        }
...
}

FunManager.cond

static  class FunManager{
...
        private static Object cond(Cons cdr, Env env, BiFunction<Cons, Env, Object> eval){
            Cons car = cdr.carCons();
            Cons predicateExp = car.carCons();
            Cons body = car.cdr();
            if(isaBoolean(eval.apply(predicateExp, env))){
                return eval.apply(body, env);
            }else{
                Cons elseCdr = cdr.cdr();
                if(elseCdr.data().size()==1){
                    // 去掉括號
                    while (IS_EXP.test(elseCdr.car())&&elseCdr.data().size()==1){
                        elseCdr = elseCdr.carCons();
                    }
                    validateTrue(IS_SYMBOLS.test(elseCdr.car())&&elseCdr.carSymbols().getName().equals("else"),"cond last item not else key");
                    return  eval.apply(elseCdr.cdr(), env);
                }
                return cond(elseCdr, env, eval);
            }
        }
...
}

FunManager.set!

static  class FunManager{
...
        private static Object set(Cons cdr,Env env, BiFunction<Cons, Env, Object> eval) {
            Symbols var = cdr.carSymbols();
            Object val = eval.apply(cdr.cdr(),env);
            validateTrue(env.env(var).isPresent(), " not definition set! error " + var);
            Env envParent = env;
            while (!envParent.contains(var)) {
                envParent = envParent.parent();
            }
            envParent.setEnv(var, val);
            return null;
        }
...
}

FunManager.apply0

static  class FunManager{
...
        private static Object apply0(Cons cdr, Env env) {
            Object f = cdr.car();
            f = getAtom(f, cdr, env);
            if (IS_FUN.test(f)) {
                return apply(f, cdr.cdr(), env);
            } else {
                throw new IllegalArgumentException("apply " + cdr);
            }
        }

...
}

接下来让我们将他们注册进去

FunManager

static  class FunManager{
...
       static {
            ...
            reg("let", applyArgs -> let(applyArgs.getExp(), applyArgs.getEnv(), applyArgs.getEval()));
            reg("set!", applyArgs -> set(applyArgs.getExp(), applyArgs.getEnv(), applyArgs.getEval()));
            reg("apply", applyArgs -> apply0(applyArgs.getExp(), applyArgs.getEnv()));
            reg("cond", applyArgs -> cond(applyArgs.getExp(), applyArgs.getEnv(), applyArgs.getEval()));
        }
...
}

貌似还少点什么?是的 boolean逻辑

FunManager.regBooleanFun

static  class FunManager{
...
        private static void regBooleanFun() {
            reg("and", applyArgs -> {
                Object[] ts = applyArgs.getLazyArgs().get();
                return Stream.of(ts).allMatch(FunManager::isaBoolean) ? ts[ts.length - 1] : false;
            });
            reg("or", applyArgs -> Stream.of(applyArgs.getLazyArgs().get()).filter(FunManager::isaBoolean).findFirst().orElse(false));
            reg("not", applyArgs -> {
                Object[] ts = applyArgs.getLazyArgs().get();
                validateTrue(ts.length == 1, applyArgs.getExp() + "not args only one");
                return !isaBoolean(ts[0]);
            });
            reg("<", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), (x, y) -> (x instanceof Integer && y instanceof Integer) ? (Integer) x < (Integer) y : x.toString().length() < y.toString().length()));
            reg("<=", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), (x, y) -> (x instanceof Integer && y instanceof Integer) ? (Integer) x <= (Integer) y : x.toString().length() <= y.toString().length()));
            reg("=", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), Object::equals));
            reg("!=", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), (x, y) -> !x.equals(y)));
            reg(">", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), (x, y) -> (x instanceof Integer && y instanceof Integer) ? (Integer) x > (Integer) y : x.toString().length() > y.toString().length()));
            reg(">=", applyArgs -> predicate(applyArgs.getExp(), applyArgs.getLazyArgs(), (x, y) -> (x instanceof Integer && y instanceof Integer) ? (Integer) x >= (Integer) y : x.toString().length() >= y.toString().length()));
        }

        private static Object predicate(Cons exp, Supplier<Object[]> lazyArgs, BiPredicate<Object, Object> predicates) {
            Object[] objs = lazyArgs.get();
            validateTrue(objs.length > 1, exp + " args qty > 1 ");
            Object o = objs[0];
            for (int i = 1; i < objs.length; i++) {
                Object o1 = objs[i];
                boolean b = predicates.test(o, o1);
                if (!b) {
                    return b;
                }
                o = o1;
            }
            return true;
        }
...
}

这里也要记得注册哦。

FunManager

static  class FunManager{
...
       static {
            ...
             regBooleanFun();
        }
...
}

继续我们的测试
为了方便这里不再分两行

// let test
<= (let ((a 5) (b 5) (g (lambda (x y) (+ x y)))) (g a b))
=> 10
// cond test
<= ((lambda (x) (cond ((< 5 x) 1) ((> 5 x) -1) (else 0) )) 6)
=> 1
// let and cond test 
<= (let ((a 5) (g (lambda (x) (cond ((< 5 x) 1) ((> 5 x) -1) (else 0) )))) (g a))
=> 0
// set! test 
<= (let ((a 5) (b 6)) ((set! a 6) (+ a b)))
=> 12
// apply test
<= (let ((a 5) (g (lambda (x) (cond ((< 5 x) 1) ((> 5 x) -1) (else 0) )))) (apply g a))
=> 0

结果符合我们的预期,接下来让我们开始自定义些函数吧。

三生万物

回到最开头,我们提到过的Dotted pairs ,这里我们实现三个函数 分别是 cons,car,cdr

<= (let (
            (cons (lambda (x y) (lambda (g) (g x y)))) 
            (car (lambda (f) (f (lambda (x y) (x))))) 
            (cdr (lambda (f) (f (lambda (x y) (y)))))
        )
        (car (cons 1 2))
    )
=> 1
<= (let (
            (cons (lambda (x y) (lambda (g) (g x y)))) 
            (car (lambda (f) (f (lambda (x y) (x))))) 
            (cdr (lambda (f) (f (lambda (x y) (y)))))
        )
        (cdr (cons 1 2))
    )
=> 2

接下来我们再增加两个函数 set-car!, set-cdr!

<= (let (
            (cons (lambda (x y) (lambda (g) (g x y (lambda (a) (set! x a)) (lambda (a) (set! y a))))))
            (car (lambda (f) (f (lambda (x y sx sy) (x)))))
            (cdr (lambda (f) (f (lambda (x y sx sy) (y)))))
            (set-car! (lambda (f a) (f (lambda (x y sx sy) ((sx a) f)))))
            (set-cdr! (lambda (f a) (f (lambda (x y sx sy) ((sy a) f)))))
        ) 
    (cdr (set-cdr! (cons 1 2) 3))
)
=> 3

在这里我们可以看到过程(函数)和数据的区分没有那么明显;有时过程即是数据,数据即是过程。

后记

JLispInter2 还有很多lisp语法特性还没有实现,但大多数都可以通过注册内置函数的方式来实现,如实现display, if, define 等。
我们可以看到解释器的核心函数代码其实并不多,60行左右,但正是这不多的代码让整个程序可以正常运转,重要的是我们还可以理解它;很多时候随着代码行数的增加我们并不能编写出完全无bug的代码,这时候有些方法可以帮助我们减少bug,如控制复杂度将程序编写的足够简单,代码具备正交性等。

写解释器的过程对自己还是很有帮助的,如通过一个最初版的程序将思维过程具象化,在具象化中寻找与审视触发某种感觉(这个程序就这样了吗?哪些地方可以在一步调整),从而为后面的版本提供一些想法与思路,也许最初的版本很不完善,但它像一个照明弹一样给我们点亮了前方行进的路;还有就是一定要有退出条件和容错机制,它们会帮助我们减轻思维负担,很多时候我们可以通过递归来将重复的过程简化;最后要熟悉你编写的程序的形状他们是你面对这个问题时思维过程具象化,从中也能看出是条理清晰还是一团乱麻。


yangrd
1.3k 声望225 粉丝

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