munificent / craftinginterpreters

Repository for the book "Crafting Interpreters"
http://www.craftinginterpreters.com/
Other
8.84k stars 1.04k forks source link

Closures and frozen environments #730

Closed tomicapretto closed 4 years ago

tomicapretto commented 4 years ago

Hello,

I have been reading the book and implementing the Lox language in Java following it. I am really thankful because I've always wanted to see a book like this one.

My background is in statistics so everything I've learned about programming has not been very formal (blog posts, some books I've bought, documentation, a few papers, etc.) and obviously not every piece of knowledge is connected in my mind.

Currently I am on chapter 11: Resolving and binding. I would like to consult something related to the bug seen when showA() is introduced. Specifically, it is related to whether functions should remember the enclosing environment as it was when the function was created or not.

The author says: "The function should capture a frozen snapshot of the environment as it existed at the moment the function was declared."

However, I don't completely understand why this is the behavior you would expect by default.

As a person whose first language has been R, I thought it was normal to have dynamic enclosing environments. Here I share an example of what you have in R.

> # Global. Behaves as expected.
> a <- "Global"
> # Defines a function in the global environment
> f1 <- function() print(a)
> f1()
[1] "Global"
> 
> # Create a new environment whose parent is the global environment
> my_env <- new.env()
> # Bind the value "Internal" to the name "a" in this new environemnt
> my_env$a <- "Internal"
> # Create a function and bind it to `f1` in `my_env`. 
> # Important detail: the function is binded to a name in `my_env`
> # but since the code is executed from the global environment,
> # its enclosing environment is the global environment (as we'll see)
> my_env$f1 <- function() print(a)
> 
> # Result:
> my_env$f1()
[1] "Global"
> 
> # Reason:
> # The enclosing environment is the global environemnt,
> # then the function finds "a" in the global environment.
> environment(my_env$f1)
<environment: R_GlobalEnv>
> 
> # Now if we change the value binded to `a` in the global environment,
> # the output changes (no frozen enclosing environment)
> a <- "Internal"
> f1()
[1] "Internal"
> 
> # Change enclosing environment
> environment(my_env$f1) <- my_env
> environment(my_env$f1)
<environment: 0x00000221003e4bf8>
> 
> # See the effect
> my_env$f1()
[1] "Internal"
> 
> # But it is also dynamic
> my_env$a <- "Second internal"
> my_env$f1()
[1] "Second internal"

Maybe my terminology is not the best, but going straight to the point

Thank you, Tomas

munificent commented 4 years ago

I'm not very familiar with R, but my understanding from the code you have here and the relevant bit on Wikipedia is that R defaults to lexical scoping but when you interact with environments explicitly like you do here then, yes, you are opting in to dynamic scoping.

Dynamic scope can be useful (I could insert a long rant here about how dependency injection frameworks like Angular are just reinventing dynamic scope), but in general most languages do lexical scoping these days and most don't even offer a way to opt into dynamic scope. R is a little unusual in that it does.

So, yes, your expectation coming from R is totally valid but different from people coming from other languages. :)