Functional PowerShell with Classes
Object-Oriented Programming and Functional Programming paradigms may seem at odds with each other, but they are really just two language paradigms supported by PowerShell. Virtually all programming languages, functional or otherwise, have a means of extensibly binding names to values; Classes, like
records, are simply one approach to doing so. As long as we restrict our usage of Classes primarily to binding names and values and avoid heavy object-oriented programming concepts like inheritance, polymorphism, or mutability, we can leverage Classes without complicating our code. Further, by adding immutable type conversion methods, we can elevate our functional code with Classes.
Casting is one of the most powerful features in PowerShell. When you cast a value, you can trigger abstracted initialization and validation code in your application. For example, just casting a string with
[xml] will trigger code to parse the string into a complete xml tree. We can leverage classes to implement the same features in our own code.
If you don’t have a constructor, you can cast hashtables to your class type without any modification to your class. Be sure to add validation attributes to fully leverage this pattern. We can also use custom types for the type of our class properties, to trigger even more validation and initialization logic.
Casting also enables cleaner output. Compare the output from an array of
Cluster hashtables piped to
Format-Table, versus casting those hashtables to
[Cluster] before for piping to
Format-Table. The properties are always listed in the order they are defined on the class. Be sure to include the
hiddenkeyword before any properties that should not be visible when outputting the table.
If you have a single-argument constructor, casting a value to your class type will pass the value to your single-argument constructor, where you can initialize your class instance.
Casting to string
You can also override the
[string] ToString() method on the class to define the conversion logic for converting the object to a string, such as when including the object in string interpolation.
Casting serialized instances
Casting enables safe input from serialization formats. The examples below will fail if the data does not meet our specifications in
Casting in your Functional code
Functional programs define data structures first, then implement the program as a series of transformations upon the immutable data structures. While it may seem contradictory, classes, thanks to type conversion methods, actually facilitate functional code in PowerShell.
Is my PowerShell functional?
Many people coming from a C# or similar background write PowerShell that resembles C#. If you do this, you are not leveraging functional concepts and would probably benefit from doubling down on object-oriented programming in PowerShell or learning more about functional programming.
If you rely heavily on transforming immutable data using pipelines (
Sort-Object, etc, you have a more functional PowerShell style and will benefit from using PowerShell classes in a functional way.
Using classes functionally
Casting, though it uses different syntax, is just a mapping function between two domains. We can map values of an array in a pipeline using
In the example below, the
Node constructor is run every time we cast to
Datum, allowing us to abstract away a fair amount of code. As a result, our pipeline only focusses on declarative data querying and aggregation while our classes focus on data parsing and validation.
Packaging the class for reuse
Nothing is as good as it seems
Unfortunately, classes cannot be exported from modules in the same way as functions or variables; however, there are some workarounds. Assuming your classes are defined in a file
- You can dot-source the file containing the classes:
. ./my-classes.ps1. This will execute
my-classes.ps1in your current scope, defining all the classes in your file.
- You can create a PowerShell module that exports all your user-facing cmdlets and set
ScriptsToProcess = "./my-classes.ps1"in your module manifest file, which will similarly run
./my-classes.ps1in your environment.
Whichever approach you take, keep in mind that the PowerShell type system cannot resolve types if they come from two separate places. Even though you have two identical classes with the same names and all the same properties, if they are loaded from two separate locations, you might find yourself facing confusing type issues.
The path forward
The best way to avoid type resolution issues is to never expose your classes to users. Rather than expect your user to import the class type, instead export a function from your module that abstracts away the need for directly accessing the class. For example, for
Cluster, we would export a function
New-Cluster that supports user-friendly parameter sets and returns a
To join our community Slack 🗣️ and read our weekly Faun topics 🗞️, click here⬇