In a previous article,The Maybe Monad, I talked about the Maybe Monad: a container that represents a value that may or may not exist.
In that article, I ended up with an implementation of Maybe that is a struct. Here is an excerpt from the code:
public struct Maybe<T>
{
private readonly T value;
private readonly bool hasValue;
private Maybe(T value)
{
this.value = value;
hasValue = true;
}
//...
}
I also provided a static Maybe class that makes it easier to create instances of Maybe<T>. For example, the following code creates a Maybe<string> that contains no value, and another one that contains the value “computer”:
Maybe<string> none = Maybe.None;
Maybe<string> some = Maybe.Some("computer");
I also talked about many other methods that make working with Maybe easier; for example, the Map and Bind methods.
In this article, I will talk about more useful methods that are related to Maybe.
Using ValueOr and ValueOrMaybe to handle the case where there is no value
The ValueOr method can be used to provide a default value in case the Maybe object does not contain a value. For example:
static void Test8()
{
var errorMessage =
GetErrorDescription(15)
.ValueOr("Unknown error"); //Signature: T ValueOr(T defaultValue)
}
The GetErrorDescription method (discussed in the previous article) returns a Maybe<string> representing the description of the error for the specified code. It returns None (Maybe with no value) in case there is no defined description for the specified error code. The type of the errorMessage variable here is string, not Maybe<string>.
errorMessage will always have a value. If the GetErrorDescription method returns None, the default “Unknown error” value will be returned and stored inside errorMessage.
Now consider this code:
var errorMessage =
GetErrorDescription(15)
.ValueOr(GetDefaultErrorMessage());
Here, the default value is obtained by calling a method called GetDefaultErrorMessage. The GetDefaultErrorMessage method will always be called here, even if GetErrorDescription returns a value. This could be an issue if GetDefaultErrorMessage is expensive in terms of performance or if it has side effects that we only want to have if GetErrorDescription returned None.
There is another overload of ValueOr defined that allows us to provide a default value factory function that will only be called if the Maybe has no value:
static void Test9()
{
var errorMessage =
GetErrorDescription(15)
//Signature: T ValueOr(Func<T> defaultValueFactory)
.ValueOr(() => GetDefaultErrorMessage());
}
There is another variation of ValueOr defined in the Maybe struct. I call it ValueOrMaybe. It is used to provide an alternative Maybe value if the Maybe object at hand has no value. For example:
static void Test10()
{
var errorMessage =
GetErrorDescription(15)
//Signature: Maybe<T> ValueOrMaybe(Maybe<T> alternativeValue)
.ValueOrMaybe(GetErrorDescriptionViaWebService(15))
.ValueOr("Unknown error");
}
static void Test11()
{
var errorMessage =
GetErrorDescription(15)
//Signature: Maybe<T> ValueOrMaybe(Func<Maybe<T>> alternativeValueFactory)
.ValueOrMaybe(() => GetErrorDescriptionViaWebService(15))
.ValueOr("Unknown error");
}
In Test10, we first try to get the error description via the GetErrorDescription method which tries to find the error description in some file. We invoke ValueOrMaybe on the returned Maybe<string> to obtain the error description from some web service to use it in the case where the first Maybe has no value.
The difference between the overload of ValueOrMaybe used in Test10 and the one used in Test11 is that in Test11, the GetErrorDescriptionViaWebService method will only be called if the first Maybe has no value. In Test10, it will always be called, even if we are not going to use its value.
Using ValueOrThrow to get the value or throw an exception if there is not a value
Consider this example:
static void Test12()
{
var logContents =
GetLogContents(1)
// Signature: T ValueOrThrow(string errorMessage)
.ValueOrThrow("Unable to get log contents");
}
The ValueOrThrow method above will cause an exception to be thrown if GetLogContents returns None. Otherwise, the log contents will be returned.
Other ValueOr variations
You can create different variations of ValueOr methods based on the type of the value. For example, consider the ValueOrEmptyArray extension method:
public static T[] ValueOrEmptyArray<T>(this Maybe<T[]> maybe)
{
return maybe.ValueOr(Array.Empty<T>());
}
Another example is the ValueOrEmptyString extension methods:
public static string ValueOrEmptyString(this Maybe<string> maybe)
{
return maybe.ValueOr(string.Empty);
}
Using GetItemsWithValue
Consider this example:
static void Test14()
{
List<string> multipleLogContents =
Enumerable.Range(1, 20)
.Select(x => GetLogContents(x))
//Signature: IEnumerable<T> GetItemsWithValue<T>(this IEnumerable<Maybe<T>> enumerable)
.GetItemsWithValue()
.ToList();
}
Here, we invoke GetLogContents twenty times. The Select method returns an enumerable of type IEnumerable<Maybe<string>>. GetItemsWithValue enables us to obtain an IEnumerable<string> that corresponds to the maybe objects that do have values. The ones without a value will not be included in the returned enumerable.
Using IfAllHaveValues
Consider this example:
static void Test15()
{
List<string> multipleLogContents =
Enumerable.Range(1, 20)
.Select(x => GetLogContents(x))
//Signature: Maybe<IEnumerable<T>> IfAllHaveValues<T>(this IEnumerable<Maybe<T>> enumerable)
.IfAllHaveValues()
.ValueOrThrow("Some logs are not available")
.ToList();
}
IfAllHaveValues will return None if any item in the enumerable has no value. In this example, if any of the 20 logs is unavailable, IfAllHaveValues would return None and ValueOrThrow would throw an exception.
Notice the signature of IfAllHaveValues, it takes an IEnumerable<Maybe<T>> and returns a Maybe<IEnumerable<T>>.
Using ToAddIfHasValue
Consider this example:
static void Test16()
{
Maybe<string> logMaybe = Maybe.Some("entry9");
var list = new List<string>()
{
"entry1",
logMaybe.ToAddIfHasValue(),
"entry2"
};
}
In the above method, we create a list of strings. We want the list to have “entry1”, “entry2”. Also, if logMaybe has a value, we want its value to be included between “entry1” and “entry2”.
The list variable will contain a list that will either have two or three entries inside it depending on whether logMaybe has a value. In the code we just saw, we know that it has the value “entry9”.
This is possible in C# because the list initializer syntax is extensible. You can have a value, say of type TValue, in the initialization list as long as there is a method with a signature similar to:
void Add(this TCollection collection, TValue value)
Where TCollection is the type of the list we are trying to initialize.
The following version of Test16 is equivalent to the version displayed earlier:
static void Test16()
{
Maybe<string> logMaybe = Maybe.Some("entry9");
var list = new List<string>();
list.Add("entry1"); //List<T>.Add
list.Add(logMaybe.ToAddIfHasValue()); //Our extension method
list.Add("entry2"); //List<T>.Add
}
Take a look at a method called Add I defined in the ExtensionMethods class. Here is how it looks like:
public static void Add<T>(
this ICollection<T> collection,
AddIfHasValue<T> addIfHasValue)
{
if (addIfHasValue.Maybe.TryGetValue(out var value))
{
collection.Add(value);
}
}
The ToAddIfHasValue method allows us to wrap a Maybe object inside a special type, AddIfHasValue<T>. In the first version of Test16, the value returned by logMaybe.ToAddIfHasValue() is of type AddIfHasValue<string>. Therefore, our extension method (Add) is called to potentially add the value inside the Maybe to the list.
Note that we could have defined the Add method to work on Maybe<T> instead of AddIfHasValue<T>. The code in Test16 would look like this in this case:
static void Test16()
{
Maybe<string> logMaybe = Maybe.Some("entry9");
var list = new List<string>()
{
"entry1",
logMaybe,
"entry2"
};
}
The problem would be that a reader of this code would expect that there are going to be three items in the list. Adding ToAddIfHasValue would make it easier for the reader to understand that the value will only be added if it exists.
Conclusion:
In this article, I talked about some nice methods that are designed to make it easier to deal with the Maybe type. I always find myself doing something over and over again with Maybe, and then I decide to add a special method to do it. I hope you will find these methods useful!
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software architect and works mainly on Microsoft technologies. Currently, he works at NextgenID where he uses C#, .NET, and other technologies to create identity solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX (
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler. You can follow him on twitter @
yacoubmassad.