Have you ever being implementing Option<T>
functional type? It is discussed here. Basically, it is about using IEnumerable<T>
with no or only one element instead of potentially nullable object reference in C#. We can empower LINQ functionality to streamline processing reducing cyclomatic complexity because of no if(null)
conditions anymore.
My adoption of the idea looks this way:
public struct Optional<T> : IEnumerable<T>
where T : class
{
public static implicit operator Optional<T>(T value)
{
return new Optional<T>(value);
}
public static implicit operator T(Optional<T> optional)
{
return optional.Value;
}
Optional(T value)
: this()
{
Value = value;
}
public IEnumerator<T> GetEnumerator()
{
if (HasValue)
yield return Value;
}
T Value { get; }
bool HasValue => Value != null;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString() => Value?.ToString();
}
It is not so bad. We can use it as argument/return type with automatic conversion to/from T
when necessary.
Consumption example might look like this:
var p = new Product("Milk");
var basket = ShoppingBasket.Empty
.Place(p)
.Place(p)
.Place(null);
It creates basket with single basket entry for two milk boxes if the following is defined:
public static class ShoppingBasket
{
public static readonly IEnumerable<BasketItem> Empty =
Enumerable.Empty<BasketItem>();
public static IEnumerable<BasketItem> Place(
this IEnumerable<BasketItem> basket,
Optional<Product> product) =>
basket
.SetOrAdd(
i => product.Contains(i.Product),
i => i.OneMore(),
product.Select(p => new BasketItem(p, 1)))
.ToArray();
}
Where:
public class BasketItem
{
public BasketItem(Product product, int quantity)
{
Product = product;
Quantity = quantity;
}
public Product Product { get; }
public int Quantity { get; }
public BasketItem OneMore() =>
new BasketItem(Product, Quantity + 1);
}
public class Product
{
public Product(string name)
{
Name = name;
}
public string Name { get; }
}
I use this general IEnumerable<T>
extension:
public static class EnumerableHelper
{
public static IEnumerable<T> SetOrAdd<T>(
this IEnumerable<T> source,
Func<T, bool> predicate,
Func<T, T> set,
params T[] add)
{
return source
.SetOrAdd(predicate, set, add as IEnumerable<T>);
}
public static IEnumerable<T> SetOrAdd<T>(
this IEnumerable<T> source,
Func<T, bool> predicate,
Func<T, T> set,
IEnumerable<T> add)
{
var empty = Enumerable.Empty<T>();
foreach (var item in source)
if (predicate(item))
{
yield return set(item);
add = empty;
}
else
yield return item;
foreach (var item in add)
yield return item;
}
}
Well, it is pretty functional as far as I can see. But the question is: does it make any sense? Cyclomatic complexity is very low in ShoppingBasket
class. But the same time we still have three "virtual paths" of execution to test - for "add" and "replace" in the Place()
method + we also need to test for null product argument.
The problem is that they are almost invisible. What do you say? Would you personally prefer to maintain such kind of code in C#?