3

The following PowerShell code displays unexpected scoping behavior for functions called from closures. Can you explain whether this is "by design", or is a defect?

function runblock($block) {
    $x = 4
    & $block
}

function printx() {
    "  in printx: x=" + $x
}

"PSVersion $($PSVersionTable.PSVersion)"

$x = 1
$b = {"In block x=" + $x ; $x = 3 ; printx}
$x = 2
runblock $b

$x = 1
$b = {"In closure x=" + $x ; $x = 3 ; printx}.GetNewClosure()
$x = 2
runblock $b

Output from the above is

PSVersion 3.0
In block x=4
  in printx: x=3
In closure x=1
  in printx: x=4

Most of the output makes sense to me:

The script block outputs In block x=4 since its parent scope is the runblock function. The printx function outputs x=3 since its parent scope is the script block scope.

The closure outputs In closure x=1 since the value of $x was captured by GetNewClosure call. All as expected.

BUT: The call to printx from the closure outputs in printx: x=4. So the scope that printx executes within is unaffected by the scope of the closure where $x = 3.

It seems strange to me that a function called from a normal script block does see the variables in the script block scope, but a function called from a closure does not see the variables in the closure.

John Rees
  • 1,553
  • 17
  • 24

1 Answers1

13

Consider following code:

function Run {
    param($ScriptBlock)
    $a = 3
    # Picture 4
    & $ScriptBlock
}
function Print {
    # Picture 6
    "Print `$a=$a"
    "Print `$b=$b"
}
$a = 1
$b = 1
# Picture 1
$SB = {
    # Picture 5
    "Closure `$a=$a"
    "Closure `$b=$b"
    Print
}.GetNewClosure()
# Picture 2
$a = 2
$b = 2
# Picture 3
Run $SB

It prints:

Closure $a=1
Closure $b=1
Print $a=3
Print $b=2

Picture 1: you start from global scope, where you define some variables.
Picture 1
Picture 2: GetNewClosure() create new module and copy variables to it. (red arrow show parent scope relationship)
Picture 2
Picture 3: you change value of variables. Module scope not affected.
Picture 3
Picture 4: function Run called. It create local variable $a. (blue arrow show call direction)
Picture 4
Picture 5: $SB script block called. Script block bound to module session state, so you transit to it.
Picture 5
Picture 6: function Print called. Function bound to global session state, so you return to it.
Picture 6

user4003407
  • 21,204
  • 4
  • 50
  • 60
  • 1
    This is an awesome answer! – Mike Shepard Jan 26 '16 at 15:58
  • Thanks for very clear answer. Your explanation certainly fits the facts, but it uses terms I am not familiar with. Are Global and Module "session state" documented PowerShell terms, or are they of your own invention? Finally, I have updated my question to better focus on the real issue: since PowerShell is fundamentally dynamically scoped, it seems weird that that breaks down for function calls made from within a closure. I expected closures to change the parent scope of the closure itself, but not to affect the way scoping works WITHIN the closure. – John Rees Jan 26 '16 at 20:26
  • Here is an explanation of closures and modules http://blogs.technet.com/b/heyscriptingguy/archive/2013/04/05/closures-in-powershell.aspx – Nat Jan 26 '16 at 21:06
  • Thanks. I needed better understanding of modules to actually appreciate the @PetSerAl answer. Particularly important quote: _When a new closure is created, a new dynamic module is created, and then all the variables in the caller’s scope are copied into this new module_ – John Rees Jan 26 '16 at 21:15
  • @JohnRees By session state I mean [`SessionState`](https://msdn.microsoft.com/library/system.management.automation.sessionstate.aspx) class. You can get current session state by `$ExecutionContext.SessionState`. You can get module session state by `$Module.SessionState` where `$Module` is [`PSModuleInfo`](https://msdn.microsoft.com/library/system.management.automation.psmoduleinfo.aspx) instance. Although, I does not know how to get global session state, if it is not current. – user4003407 Jan 27 '16 at 09:18
  • I actually find current behavior useful. If you call function from module and provide script block (callback) to it, you likely expect that callback will be executed in your scope with your variables, not in scope of a module with no access to non-global variables. If you want some other behavior, then you better to ask new question with example of desired behavior. Possible you want to create function, which not bound to any sessions state and always work in current session state. – user4003407 Jan 27 '16 at 09:27
  • @PetSerAl I originally expected functions called from a closure to see the same scope as the closure. I now understand that the called function is actually in a different module to the closure, which by design has a separate scope "stack". I also see that by having the `Run` or `runblock` helper functions in their own module, I could protect callbacks from seeing the helper function's local variables. Do you agree? – John Rees Feb 04 '16 at 03:23
  • @JohnRees Yes, if `runblock` will belong to its own module, then callbacks will not see its scope, unless user explicitly bind callback to that module. – user4003407 Feb 04 '16 at 08:11