PoshCode Logo PowerShell Code Repository

partial application by Oisin Grehan 7 years ago
embed code: <script type="text/javascript" src="http://PoshCode.org/embed/1687"></script>download | new post

A proof of concept module implementing partial application (not currying) of functions and cmdlets in powershell. This is a functional language technique often used in languages like Haskell, ML etc.

  1. Set-StrictMode -Version 2
  2.  
  3. $commonParameters = @("Verbose",
  4.                       "Debug",
  5.                       "ErrorAction",
  6.                       "WarningAction",
  7.                       "ErrorVariable",
  8.                       "WarningVariable",
  9.                       "OutVariable",
  10.                       "OutBuffer")
  11.  
  12. <#
  13. .SYNOPSIS
  14.     Support function for partially-applied cmdlets and functions.
  15. #>
  16. function Get-ParameterDictionary {
  17.     [outputtype([Management.Automation.RuntimeDefinedParameterDictionary])]
  18.     [cmdletbinding()]
  19.     param(
  20.         [validatenotnull()]
  21.         [management.automation.commandinfo]$CommandInfo,
  22.         [validatenotnull()]
  23.         [management.automation.pscmdlet]$PSCmdletContext = $PSCmdlet
  24.     )
  25.    
  26.     # dictionary to hold dynamic parameters
  27.     $rdpd = new-object Management.Automation.RuntimeDefinedParameterDictionary
  28.  
  29.     try {
  30.         # grab parameters from function
  31.         if ($CommandInfo.parametersets.count > 1) {
  32.             $parameters = $CommandInfo.ParameterSets[[string]$CommandInfo.DefaultParameterSet].parameters
  33.         } else {
  34.             $parameters = $CommandInfo.parameters.getenumerator() | % {$CommandInfo.parameters[$_.key]}
  35.         }        
  36.                
  37.         $parameters | % {
  38.            
  39.             write-verbose "testing $($_.name)"
  40.                                    
  41.             # skip common parameters        
  42.             if ($commonParameters -like $_.Name) {                                  
  43.                
  44.                 write-verbose "skipping common parameter $($_.name)"
  45.                
  46.             } else {
  47.                
  48.                 $rdp = new-object management.automation.runtimedefinedparameter
  49.                 $rdp.Name = $_.Name
  50.                 $rdp.ParameterType = $_.ParameterType
  51.                
  52.                 # tag new parameters to match this function's parameterset
  53.                 $pa = new-object system.management.automation.parameterattribute
  54.                 $pa.ParameterSetName = $PSCmdletContext.ParameterSetName
  55.                 $rdp.Attributes.Add($pa)
  56.                
  57.                 $rdpd.add($_.Name, $rdp)
  58.             }
  59.            
  60.         }
  61.     } catch {
  62.    
  63.         Write-Warning "Error getting parameter dictionary: $_"
  64.     }
  65.    
  66.     # return
  67.     $rdpd
  68. }
  69.  
  70. <#
  71. .SYNOPSIS
  72.     Function that accepts a FunctionInfo or CmdletInfo reference and one or more parameters
  73.     and returns a FunctionInfo bound to those parameter(s) and their value(s.)
  74. .DESCRIPTION
  75.     Function that accepts a FunctionInfo or CmdletInfo reference and one or more parameters
  76.     and returns a FunctionInfo bound to those parameter(s) and their value(s.)
  77.    
  78.     Any parameters "merged" into the function are removed from the available parameters for
  79.     future invocations. Multiple chained merge-parameter calls are permitted.
  80. .EXAMPLE
  81.  
  82.     First, we define a simple function:
  83.    
  84.     function test {
  85.         param($a, $b, $c, $d);
  86.         "a: $a; b: $b; c:$c; d:$d"
  87.     }
  88.    
  89.     Now we merge -b parameter into functioninfo with the static value of 5, returning a new
  90.     functioninfo:
  91.    
  92.     ps> $x = merge-parameter (gcm test) -b 5
  93.    
  94.     We execute the new functioninfo with the & (call) operator, passing in the remaining
  95.     arguments:
  96.    
  97.     ps> & $x -a 2 -c 4 -d 9
  98.     a: 2; b: 5; c: 4; d: 9
  99.    
  100.     Now we merge two new parameters in, -c with the value 3 and -d with 5:
  101.    
  102.     ps> $y = merge-parameter $x -c 3 -d 5
  103.    
  104.     Again we call $y with the remaining named parameter -a:
  105.    
  106.     ps> & $y -a 2
  107.     a: 2; b: 5; c: 3; d: 5
  108. .EXAMPLE
  109.  
  110.     Cmdlets can also be subject to partial application. In this case we create a new
  111.     function with the returned functioninfo:
  112.    
  113.     ps> si function:get-function (merge-parameter (gcm get-command) -commandtype function)
  114.     ps> get-function
  115.     <lists all commands of commandtype "function">            
  116. .PARAMETER _CommandInfo
  117.     The FunctionInfo or CmdletInfo into which to merge (apply) parameter(s.)
  118.    
  119.     The parameter is named with a leading underscore character to prevent parameter
  120.     collisions when exposing the targetted command's parameters and dynamic parameters.
  121. .INPUTS
  122.     FunctionInfo or CmdletInfo
  123. .OUTPUTS
  124.     FunctionInfo
  125. #>
  126. function Merge-Parameter {    
  127.     [OutputType([Management.Automation.FunctionInfo])]
  128.     [CmdletBinding()]
  129.     param(
  130.         [parameter(position=0, mandatory=$true)]
  131.         [validatenotnull()]
  132.         [validatescript({
  133.             ($_ -is [management.automation.functioninfo]) -or `
  134.             ($_ -is [management.automation.cmdletinfo])
  135.         })]
  136.         [management.automation.commandinfo]$_Command
  137.     )
  138.    
  139.     dynamicparam {
  140.         # strict mode compatible check for parameter
  141.         if ((test-path variable:_command)) {
  142.             # attach input functioninfo's parameters to self
  143.             Get-ParameterDictionary $_Command $PSCmdlet
  144.         }
  145.     }
  146.  
  147.     begin {
  148.         write-verbose "merge-parameter: begin"
  149.        
  150.         # copy our bound parameters, except common ones              
  151.         $mergedParameters = new-object 'collections.generic.dictionary[string,object]' $PSBoundParameters
  152.        
  153.         # remove our parameters, leaving only target function/CommandInfo's args to curry in
  154.         $mergedParameters.remove("_Command") > $null
  155.        
  156.         # remove common parameters
  157.         $commonParameters | % {
  158.             if ($mergedParameters.ContainsKey($_)) {
  159.                 $mergedParameters.Remove($_)  > $null
  160.             }
  161.         }
  162.     }
  163.    
  164.     process {
  165.         write-verbose "merge-parameter: process"
  166.        
  167.         # temporary function name
  168.         $temp = [guid]::NewGuid()
  169.  
  170.         $target = $_Command
  171.  
  172.         # splat our fixed named parameter(s) and then splat remaining args
  173.         $partial = {
  174.             [cmdletbinding()]
  175.             param()
  176.            
  177.             # begin dynamicparam
  178.             dynamicparam
  179.             {                
  180.                 $targetRdpd = Get-ParameterDictionary $target $PSCmdlet
  181.        
  182.                 # remove fixed parameters
  183.                 $mergedParameters.keys | % {
  184.                     $targetRdpd.remove($_) > $null
  185.                 }
  186.                 $targetRdpd
  187.             }
  188.             begin {
  189.                 write-verbose "i have $($mergedParameters.count) fixed parameter(s)."
  190.                 write-verbose "i have $($targetrdpd.count) remaining parameter(s)"
  191.             }
  192.             # end dynamicparam
  193.             process {
  194.                 $boundParameters = $PSCmdlet.MyInvocation.BoundParameters
  195.                
  196.                 # remove common parameters (verbose, whatif etc)
  197.                 $commonParameters | % {
  198.                     if ($boundParameters.ContainsKey($_)) {
  199.                         $boundParameters.Remove($_)  > $null
  200.                     }
  201.                 }
  202.                
  203.                 # invoke command with fixed parameters and passed parameters (all named)
  204.                 . $target @mergedParameters @boundParameters
  205.                
  206.                 if ($args) {
  207.                     write-warning "received $($args.count) arg(s) not part of function."
  208.                 }
  209.             }
  210.         }
  211.        
  212.         # emit function/CommandInfo
  213.         new-item -Path function:$temp -Value $partial.GetNewClosure()
  214.     }
  215.    
  216.     end {
  217.         # cleanup
  218.         rm function:$temp
  219.     }    
  220. }
  221.  
  222. new-alias papp Merge-Parameter -force
  223.  
  224. Export-ModuleMember -Alias papp -Function Merge-Parameter, Get-ParameterDictionary

Submit a correction or amendment below (
click here to make a fresh posting)
After submitting an amendment, you'll be able to view the differences between the old and new posts easily.

Syntax highlighting:


Remember me