bobbicodes / bobbi-lisp

Interactive Lisp environment for learning Clojure
https://bobbicodes.github.io/bobbi-lisp/
0 stars 0 forks source link

Named lambdas #12

Open bobbicodes opened 11 months ago

bobbicodes commented 11 months ago

There is currently not the option of giving a local function a name. As a result there are certain places where a defn must be used inside another function because it is too intertwined with the rest of it to be conveniently broken out into a helper function, and must be able to call itself (see for): https://github.com/bobbicodes/bien/blob/18849ea7708176d15ab13be748c0c8de334c9af8/src/clj/core.clj#L631

Ordinarily this would be a named lambda, i.e.

(fn myfn [] ...)

It's not a huge issue other than compatibility with Clojure, but it pollutes the global scope so one must be careful to name it something sufficiently unique.

The solution is to make it behave like let*, which creates a new environment that "goes away" once it's out of scope:

case "let*":
    var let_env = new Env(env);
    for (var i = 0; i < a1.length; i += 2) {
        let_env.set(a1[i], EVAL(a1[i + 1], let_env));
    }
    ast = a2;
    env = let_env;
    break;

This is the current fn special form:

case "fn":
    if (types._list_Q(a1)) {
        return types.multifn(EVAL, Env, ast.slice(1), env);
    } else {
        return types._function(EVAL, Env, a2, env, a1, a0);
    }

All it does is return a function which has been passed the current environment, and the only logic is checking whether the second argument is a list which is assumed to be the first of one or more arity bodies and if so it is handled by a special multifn type.

I don't believe it's possible for a local function (not a defn) to both have a name and multiple arities. Even the latter case is very rare, only used in a handful of higher order functions like juxt.

Here is the types._function type:

export function _function(Eval, Env, ast, env, params) {
    var fn = function () {
        return Eval(ast, new Env(env, params, arguments));
    };
    fn.__meta__ = null;
    fn.__ast__ = ast;
    fn.__gen_env__ = function (args) { return new Env(env, params, args); };
    fn._ismacro_ = false;
    return fn;
}

As we can see the function is already evaluated (only when called) in a fresh environment so that's not the issue. And I suppose this isn't even the place to add naming logic. I think what we need is another form of def that behaves more like let. Here's the def special form:

case "def":
    var res = EVAL(a2, env);
    env.set(a1, res);
    return res

Very simple. It defines the symbol in the current env. So we could have something similar in fn that runs only in the case where the second arg is a symbol, and is given a new env instead, and sets the current env as the new one.

The only other thing is that in defn, the function is also given :name metadata so that other parts of the code can be aware of it, like the printer. So I imagine it could return a function like

var fn_env = new Env(env)
with_meta(types._function(types._function(EVAL, Env, ast.slice(2), fn_env, a2), {name: a1})
env = fn_env