Functional VB.NET You Are Already UsingPart 1 of a three-part series on functional programming in VB.NET. What’s already in the box. |
|||
| Version | v1792077374 | ||
|---|---|---|---|
| Updated | |||
Part 1 of a three-part series on functional programming in VB.NET. These notes cover the patterns built into the language. Part 2 covers monads and bind chains. Part 3 covers domain modeling with records and type classes.
last updated: 2026-07-20 · part 1 of 3
I have written VB.NET for years and only recently realized I had been
writing functional code the whole time. LINQ, extension methods,
lambdas, Iterator/Yield, generic constraints:
all functional, all already in the box, all in use before I had ever
heard the term “monad.”
These notes document the patterns I actually use day to day. The goal is to record what works, not to sell the language. If it helps someone else who got stuck in a VB.NET codebase the way I did, even better.
The .NET team did not talk about it this way, but LINQ (Language
Integrated Query) is a functional programming library bolted onto C# and
VB.NET. Every chain I write, every Where,
Select, Aggregate, GroupBy, is a
composition of pure functions over a sequence.
The pattern, in case it has been a while:
Dim activeOrders = orders _
.Where(Function(o) o.Status = OrderStatus.Active) _
.Select(Function(o) New With {
.Id = o.Id,
.Total = o.LineItems.Sum(Function(li) li.Price * li.Quantity)
}) _
.OrderBy(Function(x) x.Total) _
.ToList()
That is a functional pipeline. Where filters.
Select projects. Aggregate folds.
GroupBy partitions. They chain like bash pipes. They do not
mutate the input. They return new collections.
The trick is realizing you can write LINQ over anything that
implements IEnumerable(Of T). Not just
collections. Database query results. CSV rows parsed line by line. Web
API responses. Generated sequences from Iterator functions.
Anything.
When I catch myself writing nested For loops to compute
things, the first question is: could this be a
Where/Select/Aggregate chain?
Almost always yes, and the LINQ version is shorter, faster to write, and
easier to test.
VB.NET gives you two ways to write the same LINQ:
' Method syntax (chain of extension methods)
Dim result = items _
.Where(Function(x) x.IsActive) _
.Select(Function(x) x.Name) _
.ToList()
' Query syntax (SQL-like comprehension)
Dim result = (From x In items
Where x.IsActive
Select x.Name).ToList()
Both compile to the same thing. The convention I default to:
Join,
Group Join, Let, Group By,
anything that gets unreadable as nested lambdasIf I mix the two in the same query, the VB.NET compiler gets confused and I waste twenty minutes figuring out why. Pick one per query and stay consistent.
This pattern shows up everywhere:
Dim s$ = foo().Replace("foo", "bar").Capitalize()
That Capitalize() is not a built-in method on
String. It is an extension method someone wrote. In VB.NET,
you declare one like this:
<Extension>
Module StringExtensions
<Extension>
Public Function Capitalize(s As String) As String
If String.IsNullOrEmpty(s) Then Return s
Return Char.ToUpper(s(0)) & s.Substring(1)
End Function
End Module
The <Extension> attribute on the module is
optional but conventional. The attribute on the function is mandatory.
The first parameter is the receiver, which is what
s.Capitalize() passes in.
Once a few extension methods exist, they start composing:
Dim cleanName = rawName _
.Trim() _
.Capitalize() _
.Replace(" ", " ")
This reads like a recipe. Each step does one thing. The data flows left to right. To add a step, add a line. To remove a step, delete a line.
The discipline: each extension method should do one
transformation and return a new value. Do not write extensions that
mutate state. Do not write extensions that have side effects unless they
are named like they do (LogTo, SendTo,
WriteTo).
Not every method should be an extension. The filter I apply:
| Good candidate | Bad candidate |
|---|---|
| Operations you do repeatedly on a type | One-off logic specific to your app |
| Transformations that read naturally chained | Operations that need lots of parameters |
| Methods that fit the type’s conceptual API | Methods that are really just helpers |
| Composable pieces (output is the same type) | Methods that take input you do not have access to |
The smell test: would myString.MyExtension() read like a
method someone would have written on String itself? If yes,
it is a good extension. If I am fighting the type system, it is not.
Iterator
functions: lazy sequences for freeMost VB.NET developers learn about For Each and never
think about how sequences get built. The Iterator keyword
lets you write a function that produces a sequence lazily, one element
at a time, on demand.
Public Iterator Function EvenNumbersUpTo(n As Integer) As IEnumerable(Of Integer)
For i = 2 To n Step 2
Yield i
Next
End Function
' Usage
Dim evens = EvenNumbersUpTo(10) ' [2, 4, 6, 8, 10]
The body looks like a normal For loop. The
Yield statement produces the next element. The function
returns when control falls off the end (or hits
Return).
Why this matters:
Lazy evaluation. The sequence is not built until
something consumes it. If you only .Take(3), the rest never
gets computed.
Readable data sources. If you need to generate a
test fixture, parse a custom format, or walk a tree,
Iterator is usually cleaner than building a
List(Of T) and returning it.
LINQ interop. The result is an
IEnumerable(Of T), so it chains with everything LINQ
has:
Dim firstFiveEvensSquared = EvenNumbersUpTo(100) _
.Select(Function(x) x * x) _
.Take(5) _
.ToArray()
' [4, 16, 36, 64, 100]
Public Iterator Function WalkTree(node As TreeNode) As IEnumerable(Of TreeNode)
Yield node
For Each child In node.Children
For Each descendant In WalkTree(child)
Yield descendant
Next
Next
End Function
That recursive walk would be ten lines of imperative code with manual stack management. Here it is six lines and obviously correct.
Func(Of), and method referencesVB.NET has had lambdas since 2010. The syntax is verbose by modern standards but it works:
' Lambda assigned to a variable
Dim isPositive = Function(x As Integer) x > 0
' Lambda passed directly to LINQ
Dim positives = numbers.Where(Function(x) x > 0).ToList()
' Lambda stored as a field
Public ReadOnly Add As Func(Of Integer, Integer, Integer) =
Function(x%, y%) x + y
The % suffix on parameters is a type character
shorthand, Integer instead of As Integer.
Common in VB.NET, especially in tight code.
If you already have a function that does what you want, pass the method reference directly instead of wrapping it in a lambda:
' Lambda wrapping an existing function
Dim upper = words.Select(Function(w) w.ToUpper()).ToList()
' Method reference — same thing, no wrapping
Dim upper = words.Select(AddressOf String.ToUpper).ToList()
AddressOf String.ToUpper is the same as
Function(w) w.ToUpper() but the compiler does the wrapping
for you. The code is shorter, the intent is clearer.
| Pattern | Use when |
|---|---|
| Lambda inline | You need a one-off transformation |
| Method reference | A function already exists that does the work |
Stored Func(Of) field |
The function is part of your domain (e.g., a validator, a calculator) |
This is the trick I reach for most when I need to add behavior to existing types without modifying them. Almost nobody talks about it in VB.NET, and it is the single most useful pattern for domain modeling in the language.
Interface IExpirable
ReadOnly Property ExpiresAt As Date
End Interface
Public Function IsExpired(Of T As IExpirable)(item As T) As Boolean
Return item.ExpiresAt < Date.Now
End Function
' Works on any type that implements IExpirable
Dim cert = GetCertificate()
If IsExpired(cert) Then ...
The Of T As IExpirable constraint says “this generic
only accepts types that implement IExpirable.” Inside the
function, the compiler knows item.ExpiresAt exists, so it
is type-safe.
This is essentially a type class from Haskell. The constraint defines a contract. The generic function works on anything that satisfies it.
Func(Of)The constraint approach requires the type to implement your
interface. Sometimes you do not control the type. It might be from a
third-party library, or it might be a primitive like Date.
In that case, use Func(Of):
<Extension>
Public Function IsExpired(Of T)(x As T, getExpiry As Func(Of T, Date)) As Boolean
Return getExpiry(x) < Date.Now
End Function
' Usage
Dim orderDate As Date = ...
Dim isOld = orderDate.IsExpired(Function(d) d.AddYears(2)) ' "expires" 2 years after order
Dim cert = GetCertificate()
Dim isCertOld = cert.IsExpired(Function(c) c.ExpiresAt)
The caller provides the function that extracts the relevant field.
The IsExpired extension does not care about the source
type. It works on anything.
Most VB.NET codebases model domain concepts with inheritance:
Animal
├── Dog
├── Cat
└── Fish
That is rigid. If you want to add a “swimmable” predicate, you either
edit the base class (breaks open-closed) or add an interface
(everywhere). The Of T As IExpirable pattern lets you add
behavior to existing types without modifying them:
' Add "expirable" behavior to ANY type that has a Date field
' without modifying the type itself
It is the open-closed principle, expressed in VB.NET’s type system instead of through inheritance hierarchies.
Interface IHasUpdatedAt
ReadOnly Property UpdatedAt As Date
End Interface
Public Function WasUpdatedSince(Of T As IHasUpdatedAt)(item As T, since As Date) As Boolean
Return item.UpdatedAt >= since
End Function
' Both Order and Customer implement IHasUpdatedAt
Dim recentOrders = allOrders.Where(Function(o) WasUpdatedSince(o, lastWeek)).ToList()
Dim recentCustomers = allCustomers.Where(Function(c) WasUpdatedSince(c, lastWeek)).ToList()
One generic function, used on two completely unrelated types. No base class needed. No interface-inheritance hierarchy to maintain.
With blocksThis one is smaller but I use it constantly. Inside a
With block, you can omit the variable name and access
properties directly:
With order.Customer.Address
city = .City
state = .State
zip = .ZipCode
End With
The leading . is an anonymous variable. It refers to the
With target but does not give it a name.
When you combine it with If for null-safety, you get
something close to a null-safe navigation operator:
Dim cityName = order?.Customer?.Address?.City
' Equivalent to:
If order IsNot Nothing AndAlso
order.Customer IsNot Nothing AndAlso
order.Customer.Address IsNot Nothing Then
cityName = order.Customer.Address.City
End If
The ?. operator is C# syntax. VB.NET does not have it
directly. The closest equivalent is either the chained
AndAlso (verbose) or a With block wrapped in
If:
Dim cityName As String = Nothing
With order
If .Customer IsNot Nothing Then
With .Customer.Address
If .City IsNot Nothing Then cityName = .City
End With
End If
End With
Not as clean as C#’s ?., but it works. For deeper null
safety, you would want a Maybe(Of T) type. That is covered
in Part 2.
Here is a real function that uses several of these patterns:
<Extension>
Public Function FindOverdueInvoices(Of T As IHasDueDate)(
invoices As IEnumerable(Of T),
asOf As Date
) As IEnumerable(Of T)
Return invoices _
.Where(Function(i) i.DueDate < asOf) _
.Where(Function(i) Not i.IsPaid) _
.OrderBy(Function(i) i.DueDate)
End Function
' Usage
Dim overdue = allInvoices _
.FindOverdueInvoices(asOf:=Date.Now) _
.Select(Function(i) i.CustomerName) _
.ToList()
Of T As IHasDueDate) for type
safetyi.DueDate etc. (could go further
with AddressOf if there is a helper)That is a 10-line function that replaces what would be 30 lines of imperative filtering, with the type system guaranteeing you can only call it on things that have a due date.
The functional VB.NET already available, with what I use each for:
| Pattern | What it does | When to use |
|---|---|---|
| LINQ | Pipeline-style data transformation | Whenever I have a collection |
| Extension methods | Add methods to existing types | Repeated transformations on a type |
Iterator + Yield |
Lazy sequences | Generating sequences without building lists |
Lambdas + AddressOf |
Functions as values | Passing behavior to LINQ or storing it |
| Generic constraints | Type classes for domain modeling | Operations across unrelated types |
Anonymous With vars |
Cleaner nested access | Deep property chains |
I have been writing VB.NET for years and use at least three of these daily. The goal is to use them deliberately, to see a function and know which pattern fits before I start writing.
Part 2 — Bind Chains in VB.NET. When LINQ is not
enough. Maybe(Of T), Result(Of T, F),
discriminated unions via OneOf, railroad programming for
error handling. See vbnet-monads-bind-chains.md.
Part 3 — Domain Modeling in VB.NET. Records, type
classes, discriminated unions. A worked example pulling it all together.
See vbnet-domain-modeling-records.md.
| Home • Blog • 爪闩ㄒ尺工乂 • ©Copyright 2026 • |