powershellpowershell-7.3

How do I declare/use a strongly typed predicate function as Cmdlet parameter?


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

Solution

  • 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 }