0

Original Question Below. See Edit #2.

I'm new to powershell and am trying to figure out how to use return values from functions. I have a background in C++ so when I say "by reference", "by value", I mean it in the C++ sense.

I've read MSDN: about_return and this statement sounds straight forward:

In PowerShell, the results of each statement are returned as output, even without a statement that contains the Return keyword. Languages like C or C# return only the value or values that are specified by the return keyword.

But I don't think I understand its implications. I wrote the function below to explain what I would like my powershell function to do. Apparently, powershell has very non-intuitive return semantics.

MultBy should take $i by value, multiply it with $multiplier and return the product. MultBy should also take $j by reference, and in the same invocation, multiply $j by 200 and the product should be visible in the scope of Main(). I also want to suppress the output 3000 when calling return.

function MultBy
{
    param
    (
        [Parameter(Mandatory, Position=0)]$i, 
        [Parameter(Mandatory, Position=1)]$j,
        [Parameter(Mandatory, Position=2)]$multiplier, 
        [Parameter(Mandatory, Position=3)][string]$operation
    )

   Write-Output "Performing ""${operation}"":"
   $i *= $multiplier
   $j *= 200
   Write-Output "Inside MultBy: `$i: ${i}, `$j: ${j}"
   return $i
}

function Main()
{
    $n = 30
    $m = 40
    $mult = 100
    $operation = "Operation #1"
    
    # Q1. How to pass $m by reference and have MultBy modify it? 
    MultBy $n $m $mult $operation
    Write-Output "After first call: `$n: ${n}, `$m: ${m}" # (1)

    # Q2. How to use the return value of MultBy to reassign to $n?
    $n = MultBy $n $m $mult $operation # (2)
    Write-Output "After second call `$n: ${n}, `$m: ${m}" 
}

# Start this script
Main

Output

Performing "Operation #1":
Inside MultBy: $i: 3000, $j: 8000
3000
After first call: $n: 30, $m: 40
After second call $n: Performing "Operation #1": Inside MultBy: $i: 3000, $j: 8000 3000, $m: 40

Result from Line (1) indicates that arguments are passed by value. And the execution order around line (2) just doesn't make sense: it looks like Write-Output in the subsequent line was called before the second call to MultBy.

Some SO questions I've looked at but didn't help:

SO-1: How to return one and only one value from a PowerShell function?

SO-2: How to use a powershell function to return the expected value?

SO-2 suggests using classes, whose return semantics within class methods are more consistent with other language's usages.

EDIT #1

Per this and this discussion, the output behaves more predictably when I changed Write-Output to Write-Host.

function MultBy
{
    param
    (
        [Parameter(Mandatory, Position=0)]$i, 
        [Parameter(Mandatory, Position=1)]$j,
        [Parameter(Mandatory, Position=2)]$multiplier, 
        [Parameter(Mandatory, Position=3)][string]$operation
    )

   Write-Host "Performing ""${operation}"":"
   $i *= $multiplier
   $j *= 200
   Write-Host "Inside MultBy: `$i: ${i}, `$j: ${j}"
   return $i 
}

function Main()
{
    $n = 30
    $m = 40
    $mult = 100
    $operation = "Operation #1"
    
    # Q1. How to pass $m by reference and have MultBy modify it? 
    MultBy $n $m $mult $operation | Out-Null
    Write-Host "After first call: `$n: ${n}, `$m: ${m}"

    # Q2. (Solved)
    $n = MultBy $n $m $mult $operation
    Write-Host "After second call `$n: ${n}, `$m: ${m}"
}

# Start this script
Main

Output

Performing "Operation #1":
Inside MultBy: $i: 3000, $j: 8000
After first call: $n: 30, $m: 40
Performing "Operation #1":
Inside MultBy: $i: 3000, $j: 8000
After second call $n: 3000, $m: 40

EDIT #2

Adding [ref] to $j declaration results in an error:

function MultBy
{
    param
    (
        [Parameter(Mandatory, Position=0)]$i, 
        [Parameter(Mandatory, Position=1)][ref]$j,
        [Parameter(Mandatory, Position=2)]$multiplier, 
        [Parameter(Mandatory, Position=3)][string]$operation
    )

   Write-Host "Performing ""${operation}"":"
   $i *= $multiplier
   $j *= 200
   Write-Host "Inside MultBy: `$i: ${i}, `$j: ${j}"
   return $i 
}

function Main()
{
    $n = 30
    [ref]$m = 40
    $mult = 100
    $operation = "Operation #1"
    
    # Q1. How to pass $m by reference and have MultBy modify it? 
    MultBy $n $m $mult $operation | Out-Null
    Write-Host "After first call: `$n: ${n}, `$m: ${m}"

    # Q2. (Solved)
    $n = MultBy $n $m $mult $operation
    Write-Host "After second call `$n: ${n}, `$m: ${m}"
}

# Start this script
Main

Error Output

Method invocation failed because [System.Management.Automation.PSReference`1[[System.Int32, mscorlib, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089]]] does not contain a method named 'op_Multiply'.
At D:\...\Code\Powershell\ex_Functions\ex_Functions.ps1:13 char:4
+    $j *= 200
+    ~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (op_Multiply:String) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound
MTV
  • 91
  • 9
  • 2
    I think you’re conflating Powershell concepts - it *does* have a ```[ref]``` keyword that passes function parameters by reference. What it also has is an independent “output stream” - any uncaptured output from expressions inside the function body are automatically emitted as output - e.g. ```function f{ 1; write-output 2; (2+2); [int]::TryParse(“100”); return “aaa” }; … ; $x = f;``` - the variable ```$x```would be equivalent to ```@( 1, 2, 4, $true; “aaa” }```. This is different to, e.g. C# where uncaptured expression values are “swallowed” and only an explicit ```return``` emits a value… – mclayton Jun 13 '23 at 18:26
  • @mclayton I've addressed the 'uncaptured output' issue (the result of first invocation to MultBy is piped to `Out-Null`. I did come across `[ref]` while googling but get an error when I declare `$j` parameter with `[ref]`, see Edit #2. – MTV Jun 13 '23 at 18:52
  • did you *save* your edit #2? I can only see #1… :-) – mclayton Jun 13 '23 at 18:53
  • 1
    @mclayton Just saved edit #2. I declared `$j` and the argument `$m` to be `[ref]` but the error complains it can't multiply the reference by 200 (a constant). I tried replacing `$j *= 200` with `$j *= $j`, thinking it might work if I multiplied a reference-type by another reference-type, but I observed the same issue. – MTV Jun 13 '23 at 19:02
  • 2
    @MTV `$j.Value *= 200` - the `[ref]` cast wraps the value in a reference type object with a Value property. – Mathias R. Jessen Jun 13 '23 at 19:05
  • 2
    So ```[ref]``` changes the type of the ```$j``` parameter inside the body - instead of an ```int``` it's now a ```PSReference[int]``` (paraphrased) which you can see if you add some debugging like ```write-host $j.GetType().FullName``` in your ```MultBy```. To reference the actual value use ```$j.Value``` instead of ```$j```. – mclayton Jun 13 '23 at 19:06
  • why not return an object with 2 properties or a hashtable with 2 keys instead of passing one of the variables by ref which is pretty expensive in pwsh? – Santiago Squarzon Jun 13 '23 at 20:41
  • @SantiagoSquarzon I didn't want to try anything fancy and you'd be surprised how difficult it was to find a simple example of powershell code that return variables by value and by reference. Wasn't mentioned in the books I looked at nor in Microsoft's PowerShell documentation. `MultBy` was a contrived example. – MTV Jun 13 '23 at 20:45
  • 1
    That's because you would very rarely have a by ref argument in your pwsh function. There is no need for it really, only when working with .NET types that take a ref argument is it really needed. you could just return `@{ i = $i * $multiplier; j = $j * 200 }` then use dot notation. – Santiago Squarzon Jun 13 '23 at 20:49
  • 1
    @SantiagoSquarzon I see - because Hash Tables are object types and are passed by reference by default. I agree, that is a more pleasant idiom for returning multiple values than by `ref`. – MTV Jun 14 '23 at 12:26
  • 1
    That's correct. Also as mentioned, updating a value by ref is pretty expensive in PowerShell depending on the use case mind as well use a reference type like a hashtable that already allows you to do that – Santiago Squarzon Jun 14 '23 at 12:35

1 Answers1

0

Thanks Mclayton and Mathias R. Jessen for pointing me in the right direction.

Here is what works:

function MultBy
{
    param
    (
        [Parameter(Mandatory, Position=0)]$i, 
        [Parameter(Mandatory, Position=1)][ref]$j,  # 1. Declare parameter as ref. 
        [Parameter(Mandatory, Position=2)]$multiplier, 
        [Parameter(Mandatory, Position=3)][string]$operation
    )

   Write-Host "Performing ""${operation}"":"
   $i *= $multiplier
   $j.Value *= 200  # 2. Use the reference type's Value property.

   # Note: Have to use $($j.Value) and not ${j.Value} because Value is a property of the PSReference.
   Write-Host "Inside MultBy: `$i: ${i}, `$j: $($j.Value)"
   return $i 
}

function Main()
{
    $n = 30
    $m = 40
    $mult = 100  # Note: The variable needn't be declared as a [ref] type.
    $operation = "Operation #1"
    
    # Q1. Resolved. 
    # 3. The [ref] and enclosing paretheses is necessary when passing the argument by reference.
    MultBy $n ([ref]$m) $mult $operation | Out-Null
    Write-Host "After first call: `$n: ${n}, `$m: ${m}"

    # Q2. Resolved
    $n = MultBy $n ([ref]$m) $mult $operation
    Write-Host "After second call `$n: ${n}, `$m: ${m}"
}

# Start this script
Main

Output

Performing "Operation #1":
Inside MultBy: $i: 3000, $j: 8000
After first call: $n: 30, $m: 8000
Performing "Operation #1":
Inside MultBy: $i: 3000, $j: 1600000
After second call $n: 3000, $m: 1600000

Sources:

SO-3: How to define named parameter as [ref] in PowerShell

SO-4: powershell: how to write-host value from [ref] variable

MSDN: about_ref

MSDN: about_Functions

MTV
  • 91
  • 9