JoeStrout / miniscript

source code of both C# and C++ implementations of the MiniScript scripting language
MIT License
280 stars 64 forks source link

Immutable closures? #28

Closed Ilazki closed 4 years ago

Ilazki commented 4 years ago

I've been testing in the MiniScript command line interpreter and noticed a problem with using closures. They work fine if a value in the outer scope isn't changed by the inner, but attempting to update the value acts as if it's been locally defined in the inner scope rather than outer.

You can currently cheat it by using a complex type (list or map) to get the correct behaviour but this is undesirable.

Examples:

// Non-complex (map/list) closure values are incorrectly shadowed by inner scope?
closure1 = function ()
  count = 0
  return function ()
    count = count + 1
    return count
  end function
end function

c1 = closure1()
print [c1,c1,c1] // prints [1,1,1]

// Using a map or list as a workaround yields expected results.
closure2 = function ()
  count = {"v":0}
  return function ()
    count.v = count.v + 1
    return count.v
  end function
end function

c2 = closure2()
print [c2,c2,c2] // prints [1,2,3]
JoeStrout commented 4 years ago

We consider it desirable. Unqualified assignment in MiniScript always assigns to the local scope.

To update a value in the outer scope, use the outer keyword:

    outer.count = count + 1
JoeStrout commented 4 years ago

(That said, I do appreciate the input and the attention to detail! Please consider joining us in the forums or the Discord chat, both accessible via https://miniscript.org .)

Ilazki commented 4 years ago

Okay, I completely missed that because I found out about MiniScript from a game using it (Grey Hack) and was using the command-line interpreter to test things externally. The quick-ref is pretty good for getting into it quickly if you're familiar with other languages, including mentioning the @fun oddity, but says nothing about unusual scoping rules. Knowing about that helps a bit.

That said, I'm not sure that needing outer.foo is a good idea in general. It violates expectations regarding lexical scope and has a default behaviour that looks like a bug. Local assignment should be in respect to the entire lexical scope (in contrast to global scope), not the immediate block.

It also seems to have unusual behaviour if functions are nested more than two deep. A silly example in the style of the previous ones:

closure3 = function ()
  count = 0
  return function ()
    return function ()
      outer.count = count + 1
      return outer.count
    end function
  end function
end function

c3t = closure3()
c3 = c3t()
print [c3,c3,c3] // [null, null, null]
                        // Runtime Error: Undefined Identifier: 'count' is unknown in this context 

If I instead move count one scope inward, everything works again:

closure3 = function ()
  return function ()
    count = 0
    return function ()
      outer.count = count + 1
      return outer.count
    end function
  end function
end function

c3t = closure3()
c3 = c3t()
print [c3,c3,c3] // [1, 2, 3]

I can't tell if that's a bug or if there's some non-obvious thing I need to do to deliberately climb through the outer scopes, so I'm unsure if I should report that, re-open this, or if there's a trick you can name.

Interestingly enough, this all started because I ran into a bug in Grey Hack's scripting where functions created in-game lose their outer scope and reported it then decided to dig further. The behaviour I mentioned there is similar to what I just encountered here while trying outer with a depth of more than two functions, so now I'm wondering if they're connected.

JoeStrout commented 4 years ago

MiniScript's lexical scoping does not search arbitrarily deep. It only checks, in order: the local scope, the outer scope, and the global scope. Nested functions are not closures in the sense you're thinking of them, though in most real (not contrived) use cases, they can be used in the same way.

This is also by design; while there are many valid use cases for an inner function (supplying a comparison function to a sort routine, a function to apply to a list or map, etc.), if you find yourself going deeper than that, then probably a class/object would be clearer. I realize of course that fans of functional programming may disagree. But this is part of the "mini" in MiniScript, i.e., we strive to keep the language simple and easy to understand, especially for new programmers.