2

I found a number of answers on how to use a script block as Cmdlet parameter, but I couldn't find an answer on how to declare and use a strongly typed predicate function, i.e. something like a .NET Func<T,TResult> delegate.

I tried the following, but to no avail:

function Test-Execution
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)][int]$myValue,
        [Parameter(Mandatory = $true, Position = 1)][Func[int, bool]]$myFunc
    )
    process
    {
        if ($myFunc.Invoke($myValue)) { 'Yes' }
        else { 'No' }
    }
}


function Test-Predicate
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, Position = 0)][int]$myValue
    )
    process
    {
        $myValue -lt 3
    }
}


1..5 | Test-Execution -myFunc Test-Predicate
AxD
  • 2,714
  • 3
  • 31
  • 53

1 Answers1

1

You don't really need a predicate for this, with just a scriptblock would do but in this case you just pass a scriptblock as the Func<T, TResult> argument instead of passing the name of your function, then that scriptblock is coerced into your predicate.

Note that your predicate as you have in your question is currently taking a bool as input and outputting an int32, I believe you're looking for the other way around, thus [Func[int, bool]] instead of [Func[bool, int]].

function Test-Execution {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [int] $myValue,

        [Parameter(Mandatory = $true, Position = 1)]
        [Func[int, bool]] $myFunc
    )

    process {
        if ($myFunc.Invoke($myValue)) { 'Yes' }
        else { 'No' }
    }
}

1..5 | Test-Execution -myFunc { $myValue -lt 3 }

Also, even though this works, you should actually evaluate using $args[0] in your predicate instead of $myValue since $args[0] represents the first argument passed to the predicate:

1..5 | Test-Execution -myFunc { $args[0] -lt 3 }

If you want to use $_ instead of $args[0] to resemble the current object passed through the pipeline you can use the call operator & but in that case your function would work only from pipeline:

function Test-Execution {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [int] $myValue,

        [Parameter(Mandatory = $true, Position = 1)]
        [scriptblock] $myFunc
    )

    process {
        if (& $myFunc) {
            return 'Yes'
        }

        'No'
    }
}

1..5 | Test-Execution -myFunc { $_ -lt 3 }

An alternative to evaluate using $_ as your argument even if the function was not receiving input from pipeline would be to use InvokeWithContext, for instance (note that here we change the input object to [int[]] $myValue and also add a loop):

function Test-Execution {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [int[]] $myValue,

        [Parameter(Mandatory = $true, Position = 1)]
        [scriptblock] $myFunc
    )

    process {
        foreach($value in $myValue) {
            if($myFunc.InvokeWithContext($null, [psvariable]::new('_', $value)[0])) {
                'Yes'
                continue
            }

            'No'
        }
    }
}

# now both ways work using this method, positional binding:
Test-Execution (1..5) -myFunc { $_ -lt 3 }

# and pipeline processing:
1..5 | Test-Execution -myFunc { $_ -lt 3 }
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Thank you for enlightening me with your very informative answer! I edited my question now to fix my typo accordingly. Thanks for the hint! – AxD Apr 25 '23 at 18:55
  • 1
    I just tried your suggestion: `$_` works, `$myValue` also works, but `$args` is always `$null` when using the pipeline and using `& $myFunc`. – AxD Apr 25 '23 at 18:56
  • Glad it was helpful @AxD. You sure it fails when you call it like this? `1..5 | Test-Execution -myFunc { $args[0] -lt 3 }` ? – Santiago Squarzon Apr 25 '23 at 19:00
  • 1
    You've been faster than I was. I just updated my comment. `$args` is only `$null` when using `& $myFunc` and the pipeline. Using `$myFunc.Invoke($myValue)`, `$args[0]` is working. – AxD Apr 25 '23 at 19:02
  • 1
    ah yes, for the second example `$args[0]` would fail because we're not actually passing any argument to the scriptblock, we're only invoking it as is. I'll add a third example to the answer that you might find useful too (in a few secs) – Santiago Squarzon Apr 25 '23 at 19:05