jpmc26
jpmc26

Reputation: 29934

Capture Function in PowerShell Closure

It appears that PowerShell closures do not capture the definition of functions:

PS C:\> function x() { Write-Host 'original x' }
PS C:\> function x-caller-generator() { return { Write-host 'Calling x!'; x }.GetNewClosure() }
PS C:\> $y = x-caller-generator
PS C:\> & $y
Calling x!
original x
PS C:\> function x() { Write-Host 'new x' }
PS C:\> & $y
Calling x!
new x

Is there any way to capture the definition of a function?

What I'm actually experiencing is that I create a closure, but when my closure is executed, the function is out of scope somehow. (It's some strangeness the psake module for build scripting is doing.) Something like this:

PS C:\> function closure-maker () {
>>     function x() { Write-Host 'x!' }
>>
>>     return { Write-host 'Calling x'; x }.GetNewClosure()
>> }
>>
PS C:\> $y = closure-maker
PS C:\> & $y
Calling x
The term 'x' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:3 char:39
+     return { Write-host 'Calling x'; x <<<<  }.GetNewClosure()
    + CategoryInfo          : ObjectNotFound: (x:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Note: Using PowerShell 2.0, but interested in 3.0 answers if there's something new.

Upvotes: 7

Views: 1473

Answers (2)

dmitry
dmitry

Reputation: 186

A small correction will let the first example work:

clear
function x() { Write-Host 'original x' }
function x-caller-generator() { 
      $xFunc = $function:x; # instead of Get-Item
      return { Write-host 'Calling x!'; & $xFunc }.GetNewClosure() 
}
$y = x-caller-generator
& $y
function x() { Write-Host 'new x' }
& $y

Output:

Calling x!
original x
Calling x!
original x

PowerShell has too many looks alike which actually behave differently. You can get a function object by using $function prefix. One might think it works the same way as the Get-Item, but it does not...

Upvotes: 0

jpmc26
jpmc26

Reputation: 29934

Well, I found something that works for simple functions at least. We can use Get-Item to get an object describing the function and then pull the original script off that. Like this:

function x-caller-generator() {
    $xFunc = [ScriptBlock]::Create((Get-Item function:x).Definition)
    return { Write-host 'Calling x!'; & $xFunc }.GetNewClosure()
}

If the function is never redefined (like in my example where my function is out of scope), we can avoid pulling off the definition and just use the function object directly:

function closure-maker () {
    function x() { Write-Host 'x!' }

    $xFunc = Get-Item function:x
    return { Write-host 'Calling x'; & $xFunc }.GetNewClosure()
}

This second method will not work if the function is redefined (at least in the same scope as the original function) before the closure is executed. The object is apparently dynamic; it tracks the current definition.

I seriously doubt this would work with a function that references other user defined functions that could also be out of scope, but my use case didn't require that.

Sample output:

Creating script block

PS C:\> function x() { Write-Host 'original x' }
PS C:\> function x-caller-generator() { $xFunc = [ScriptBlock]::Create((Get-Item function:x).Definition); return { Write-host 'Calling x!'; & $xFunc }.GetNewClosure() }
PS C:\> $y = x-caller-generator
PS C:\> & $y
Calling x!
original x
PS C:\> function x() { Write-Host 'new x' }
PS C:\> & $y
Calling x!
original x

Using function object

PS C:\> function closure-maker () {
>>     function x() { Write-Host 'x!' }
>>
>>     $xFunc = Get-Item function:x
>>     return { Write-host 'Calling x'; & $xFunc }.GetNewClosure()
>> }
>>
PS C:\> $y = closure-maker
PS C:\> & $y
Calling x
x!

Trying to use object with first example does not work:

PS C:\> function x() { Write-Host 'original x' }
PS C:\> function x-caller-generator() { $xFunc = Get-Item function:x; return { Write-host 'Calling x!'; & $xFunc }.GetNewClosure() }
PS C:\> $y = x-caller-generator
PS C:\> & $y
Calling x!
original x
PS C:\> function x() { Write-Host 'new x' }
PS C:\> & $y
Calling x!
new x

Upvotes: 5

Related Questions