PowerShell sits in quite an unusual place in terms of classifying programming languages. Most languages can be pretty easily categorised; C# is an object-oriented programming language. Haskell is the classic example of a functional programming language.
PowerShell sits in a strange in-between, a bit like F# does; it has strong object-oriented roots being built upon C# and the .NET Framework (or .NET Core) platform. However, it is also very much a functional language.
PowerShell as a Functional Language
- Any uncaptured data is treated as output; explicit
return
statements are not required. - PowerShell's pipeline is solidly reminiscent of F#'s similar pipe operator.
- Use of lambda (unnamed functions) is incredibly easy.
However, just like F#, you can quite easily ignore PowerShell's functional programming behaviour and treat it just like any old object-oriented language, relying on .NET features rather than some of the more interesting native language features.
What is an Anonymous Function in PowerShell
Well, for all intents and purposes, your { basic script block }
is the de facto anonymous function
in PowerShell. In fact, even a named function can be separated from its script block, and the script
block can be used independently:
$FunctionObject = Get-Item 'Function:\mkdir'
$ScriptBlock = $FunctionObject.Scriptblock
# The oneliner version, using variable-provider-access syntax:
$ScriptBlock = ${function:mkdir}
You can look at the contents of the script block, examine parameters, and even manually invoke the
script block with a simple $ScriptBlock.Invoke()
and pass arguments to the script block by placing
them positionally inside the parentheses of the Invoke()
method.
Where Anonymous Functions are Used
One common place you will actually need to use an anonymous function is when calling .NET methods
that require delegates. My favourite example is a lesser-known method on the List<T>
class called
FindAll()
.
using namespace System.Collections.Generic
$List = [List[int]](1..100)
$List.FindAll(
{
param($Item)
$Item % 4 -eq 0
}
) -join ', '
# Output
4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100
As you can see, the FindAll()
method takes a… script block? That might not seem odd to some of
you, but for those of you coming from C#, this code looks a little more like this:
List<int> myList = new List<int>();
for (int i = 1; i <= 100; i++)
{
myList.Add(i);
}
int[] matchingItems = myList.FindAll(x => x % 4 == 0);
Not only is it much more code to get the same result, but that is not a script block. That's a C# lambda function. PowerShell actually translates the script block into a pseudo-delegate for the method to use.
For things like this, the script block or delegate must have a boolean end result to work correctly,
but not all C# delegates require such simple (or any) output. Similar constructs are used when
working with UWP, Forms, or WPF GUI scripts from PowerShell; many of their Add_Click()
and similar
interaction events use script blocks in PowerShell as well.
Anonymous Functions in the PowerShell Pipeline
I'm sure many of you are familiar with ForEach-Object
. It's the pipeline equivalent to the
familiar foreach ($i in $Collection) {}
loop construct.
What I think far fewer people are familiar with is that you can do something eerily similar to this with a simple script block.
1..100 | & {
process {
if ($_ % 2 -eq 0) { $_ }
}
}
Wait, what? Really? That's a bit… odd. The &
operator is used to tell PowerShell that, yes, we
are deliberately invoking the script block as a command, not simply placing it as an object. This
can also be done with script blocks stored in variables.
There are actually two modes of doing this, as well. The above construction will not allow you to define variables within it that can be used outside the script block. In other words, this will give you no result:
1..100 | & {
begin { $a = 0 }
process {
if ($_ % 2 -eq 0) { $a += $_ }
}
}
$a
However, make one small change, and this suddenly becomes very curiously similar to
ForEach-Object
itself:
1..100 | . {
begin { $a = 0 }
process {
if ($_ % 2 -eq 0) { $a += $_ }
}
}
$a
Thanks to the scope-merging capabilities of the dot-sourcing operator, variables that are defined inside the script block are also now available to the wider script.
ForEach-Object
does have some additional capabilities that make it worth the while, and it will
also handle errors more effectively than a nameless script block. For a script block to do that, you
would have to define a param()
block, at least one parameter, and the [CmdletBinding]
attribute
would be required. Without those, the script block will be tricky to work with in terms of error and
stream handling.
However, for simple things or where speed is the primary concern, a script block can be far faster
than your usual ForEach-Object
command. Just mind the rough edges.
Thanks for reading!