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.
Prerequisite: System.Collections.Immutable
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.
The need for safety in data processing
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.
Set up the extension methods class
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
{
}
Extension method signatures
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();
}
The safest way to wrap methods
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
.
Eliminating 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>();
}
Never worry about missing data again.
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.
Top comments (0)