Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell lambda scope

I'm trying to use a higher-order function to perform a certain task, but it seems that the scope isn't working as I expected. The code is as follows:

function DoSomething($scriptBlock, $message) {

    # if not only running a check, run the given code
    if ($shouldRunCheck) {
       return $scriptBlock.Invoke()
    }

    Write-Host $message -foreground cyan
    # write $message to file
}

This seems to work fine, but when I call it I don't seem to be able to save to variables outside of the script block.

$myArray = @()
$myArray += 'test 1'

DoSomething {
   write-host $myArray # test 1
   $myArray += 'test 2'

   write-host $myArray # test 2
}

write-host $myArray # test 1
$myArray += 'test 3'
write-host $myArray # test 1 test 3

Essentially I'm need to add to the array variable from within the callback function but it just seems to over-write the variable as if the variable is read only?

like image 959
Jon Allen Avatar asked Mar 24 '26 10:03

Jon Allen


2 Answers

+1 for @Theo explanation, I would although try to avoid to use the $Global: and the $Script: scopes as they might get confusing.
It is also possible to refer to the current scope of the caller by using ([Ref]$myArray).Value:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$myArray = @('test 1')
$scriptBlock = {
    ([Ref]$myArray).Value += 'test 2'
}

DoSomething $scriptBlock

$myArray += 'test 3'
Write-Host $myArray
test 1 test 2 test 3

But the explanation from Theo also implies that the use += on a PowerShell array is a bad idea as you will recreate the array each time which is just slow.

In other words for a better performance, it is better to use an ArrayList and the Add method rather than assigning items:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$myArray = New-Object System.Collections.ArrayList
$myArray.Add('test 1')
$scriptBlock = {
    $myArray.Add('test 2')
}

DoSomething $scriptBlock

$myArray.Add('test 3')
Write-Host $myArray
test 1 test 2 test 3

You might also consider to use the mighty PowerShell pipeline for something like this:

$scriptBlock = {
    'test 2'
}

$myArray = @(
    'test 1'
    DoSomething $scriptBlock
    'test 3'
)
Write-Host $myArray
test 1 test 2 test 3
like image 77
iRon Avatar answered Mar 27 '26 03:03

iRon


You are correct about the Scoping.

What happens is that inside the scriptblock, the variable $myArray is created new as type [String] when $myArray += 'test 2' is performed. This new variable does not exist outside the function, so the original $myArray is not altered at all.

To do that, you need to use scoping on the $myArray and declare it as $script:myArray = @('test 1') so it can be accessed throughout the entire script.
Then inside the scriptblock you add the new value to it using $script:myArray += 'test 2', like:

function DoSomething($scriptBlock) {
    return $scriptBlock.Invoke()
}

$script:myArray = @('test 1')                # declared in script-scope

# while inside the main script, you can simply access it as $myArray
Write-Host "MainScript:  $myArray"           # --> test 1   

$scriptBlock = {
   Write-Host "ScriptBlock: $script:myArray" # --> test 1
   $script:myArray += 'test 2'

   Write-Host "ScriptBlock: $script:myArray" # --> test 1 test 2
}

DoSomething $scriptBlock

Write-Host "MainScript:  $myArray"          # --> test 1 test 2
$myArray += 'test 3'
Write-Host "MainScript:  $myArray"          # --> test 1 test 2 test 3

Result:

MainScript:  test 1
ScriptBlock: test 1
ScriptBlock: test 1 test 2
MainScript:  test 1 test 2
MainScript:  test 1 test 2 test 3
like image 32
Theo Avatar answered Mar 27 '26 03:03

Theo