Hacker News new | past | comments | ask | show | jobs | submit login

> 1. The persistent approach is not right for global variables, which are dynamically bound. So I ended up needing two Environment classes, a Map-based one for the global scope, and then a persistent one for locals. That, of course, also requires an interface so that most code can work polymorphically with both types.

Can you give an example of what you mean?

"Global variables" are simply variables declared at a top-level scope. They don't need to be "dynamically bound" if you unravel the top-level into a sequences of statements that thread their environment (which is a bog-standard practice for interpreters, and how you should handle blocks, too), unless you'd like to be able to reference variables declared below you in the global scope. And that can be resolved by passing a dynamic environment in addition to the lexical one. Or by making a single pass over the global space, grabbing all the declared names, and making that your initial lexical environment. But, of course, this is all speculation without an example.

> I considered having a problem exercise to do effectively that, but I felt like it might be reaching a little too far for a first-time language implementer.

Uniquely renaming variables is, in my opinion, a far simpler concept to grasp than nesting scope resolution. It would also be far easier to implement, with notably less code change, and continue to allow you to add another compiler pass and allow the reader to cover ground. The actual downside is that it achieves your goal without needing to explain the whole mess, which means the chapter doesn't get to spend time explaining how to think about resolving variables.

> code like this is not common:

Your toy example is, obviously, a situation where you should reconsider what you're doing. However, a program such as

    lookup :: Env -> Ear -> Value
    lookup = ...

    f :: Env -> Expr -> Expr
    f env exp =
      let lookup = lookup env
      in ...
is a fairly common pattern in automatically curried languages, or languages with functional-style loops (see [0] for such a usage of shadowing). I don't think that your stance is wrong, but I think decrying other stances isn't particularly fair to the language design world, where there are many languages where such shadowing is neither rare nor often an error. Of course, there is some play here insofar as treating function declarations differently from other variables, which is not the case in many languages where this behavior is accepted.

Also, for what it's worth, translating the above code to JavaScript yields a stack overflow error if you invoke lookup inside of `f`.

0. https://github.com/cisco/ChezScheme/blob/06f858f9a505b9d6fb6...




> unless you'd like to be able to reference variables declared below you in the global scope.

It's this part. Unlike ML which uses Queinnec calls a "hyperstatic" top level environment, Lox follows Scheme where the top level environment is dynamic. This gives you a nice way to support mutual recursion at the top level.

> Or by making a single pass over the global space

That works when running from a file, but not in a REPL session. Lox also supports REPL sessions like:

    > fun foo() { return bar; }
    > foo();
    Undefined variable 'bar'.
    [line 1] in foo()
    [line 1] in script
    > var bar = "ok";
    > print foo();
    ok
> And that can be resolved by passing a dynamic environment in addition to the lexical one.

True, that would avoid the need for a single polymorphic environment type, but it's again still more code complexity.

> is a fairly common pattern in automatically curried languages, or languages with functional-style loops (see [0] for such a usage of shadowing).

Interesting, I wasn't aware of that. I can reword it by saying something more like "shadowing is usually in error in imperative languages".

Thanks!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: