Eleanor Holley
3 Feb 2021
•
3 min read
Mutability bugs and thread-unsafety are big problems in data processing. Fortunately, the .NET Framework has strong support for immutable collections, eliminating entire categories of bugs. This post will show how to use extension methods to create even safer ways to interact with with lists in C# by building on the IMaybe monad type we created in the previous post in this series.
This post builds on the lovely Systems.Collections.Immutable library in C#. Much digital ink has already been spilt in praise of the concept of immutable collections, and I don't have anything to add, but if you're not yet familiar, I recommend reading this detailed post on the benefits thereof.
This post will also use extension methods. You don't necessarily need to already be familiar with extension methods, but if you need a supplement,I recommend this Microsoft article.
C#'s immutable collections lead us well on our way on our functional programming journey, but it gives us little protection at the margins. How do we protect ourselves in case of empty lists? How can you be sure that you have enough try
/catch
blocks to handle the dreaded ArgumentOutOfRangeException?
These issues are every bit as avoidable as NullReferenceException
, and with a little extension method magic, we'll soon forget we ever had them.
Start by instantiating a new class. Extension methods must live in static classes, and, by convention, that static class should end in "Extensions". I'm going to call mine EnumerableExtensions and apply it to the broadIEnumerable<T>
interface (of which IImmutableList<T>
is an implementation).
EnumerableExtensions.cs
public static class EnumerableExtensions
{
}
Next, let's write the signature for our extension methods. Extension method signatures use the this
keyword in the parameter list, which allows us to use it as a normal object method instead of a static method.
public static class EnumerableExtensions
{
public static IMaybe<T> MaybeFirst<T>(this IEnumerable<T> xs)
=> throw new NotImplementedException();
public static IMaybe<T> MaybeLast<T>(this IEnumerable<T> xs)
=> throw new NotImplementedException();
}
Now let's implement methods, performing checks here so we'll be sure to have safely wrapped monads.
public static class EnumerableExtensions
{
public static IMaybe<T> MaybeFirst<T>(this IEnumerable<T> xs)
=> xs.Any()
? Maybe.Factory<T>(xs.First())
: new None<T>();
public static IMaybe<T> MaybeLast<T>(this IEnumerable<T> xs)
=> xs.Any() ? Maybe.Factory<T>(xs.Last()) : new None<T>();
}
There are a few ways we could have handled these two cases. We could have usedCount()
to see if the count is greater than one, but if the IEnumerable
is very massive, it could take a long time to execute. We could also have wrapped the operation in a try
and caught System.InvalidOperationException
, but throwing an exception just to catch it is somewhat computationally expensive.
We could also have called Maybe.Factory(...)
directly using FirstOrDefault()
, but this would have produced unexpected results if the default value of the type isn't null
, as is the case with primitive-like types like DateTime
and int
.
Any()
, then, is the best practice here. So as long as we stick to our "Maybe" extension methods and avoid First()
and Last()
, we can feel confident that we are always using the best practice.We've just eliminated System.InvalidOperationException
.
System.ArgumentOutOfRangeException
from ElementAt()
While we're here, let's add one more extension method to eliminate exceptions coming from ElementAt()
. ElementAt()
fails if there are fewer elements than the requested index.
In this case, Any()
doesn't help us, and Count()
is still pretty computationally expensive, so for this, simply catching the exception will suffice.
public static class EnumerableExtensions
{
public static IMaybe<T> MaybeElementAt<T>(this IEnumerable<T> xs, int i)
{
try { return Maybe.Factory<T>(xs.ElementAt(i)); }
catch { return new None<T>(); }
}
public static IMaybe<T> MaybeFirst<T>(this IEnumerable<T> xs)
=> xs.Any() ? Maybe.Factory<T>(xs.First()) : new None<T>();
public static IMaybe<T> MaybeLast<T>(this IEnumerable<T> xs)
=> xs.Any() ? Maybe.Factory<T>(xs.Last()) : new None<T>();
}
As long as we stick to our extension methods, we always know we're using the safest, most efficient, and most correct way to get specific elements of a list.
This concludes the part of the series on IMaybe monads and eliminating exceptions relating to missing expected data. In my next post, I'll work on eliminating other kinds of runtime exceptions using a special kind of monad called a try monad.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!