tabs ↹ over ␣ ␣ ␣ spaces

by Jiří {x2} Činčura

Head and Tail using list patterns in C#

17 Jan 2023 2 mins C#

Some time ago I wrote blog posts playing with head- and tail-like functions and implementing sum function in C# using these. With the recent addition of list patterns into C# 11, I revisited that topic.

In fact, the list pattern makes it very easy to implement. And also looks, in my opinion, most slick from all the previous.

int Sum(int[] list) => list switch
{
    [] => 0,
    [var head, .. var tail] => head + Sum(tail),
};

But I’m cheating here a bit. In my previous posts I used List<int> as my datatype, while here I’m using int[]. The problem is that .. var tail needs indexer with support for Range and List<T> does not have support for that. I smell some challenge ahead.

So, can I make it work with vanilla List<T>? I need to somehow add this[Range range] into existing type. This would be a great place of extension everything in C#, but we’re not there yet. Time for ListRangeWrapper<T>.

class ListRangeWrapper<T>
{
    readonly List<T> _list;

    ListRangeWrapper(List<T> list)
    {
        _list = list;
    }

    public int Count => _list.Count;
    public T this[Index index] => _list[index];
    public ListRangeWrapper<T> this[Range range] => _list.Take(range).ToList();
    public static implicit operator ListRangeWrapper<T>(List<T> list) => new ListRangeWrapper<T>(list);
    public static implicit operator List<T>(ListRangeWrapper<T> wrapper) => wrapper._list;
}

This wrapper (ab)uses implicit operators to get from List<T> into itself (and back to List<T>) and introduces the already mentioned this[Range range]. It also has this[Index index] and Count to make our pattern work completely.

With that I can create the method and feed in list.

int Sum(ListRangeWrapper<int> list) => list switch
{
    [] => 0,
    [var head, .. var tail] => head + Sum(tail),
};

var list = new List<int>() { 1, 2, 3 };
Sum(list);

Almost as nice as the array version. In the wild the ListRangeWrapper<T> would probably be confusing to consumers. Maybe having the operators explicit and preparing overload with casting would help. But is it then still slick? You can comment below.

Furthermore, as I was writing the code, I realized, there’s another new feature in C# 11, that would allow me to have the method fully generic. Yes, it’s generic math. I’ll leave that for next blog post.

Profile Picture Jiří Činčura is .NET, C# and Firebird expert. He focuses on data and business layers, language constructs, parallelism, databases and performance. For almost two decades he contributes to open-source, i.e. FirebirdClient. He works as a senior software engineer for Microsoft. Frequent speaker and blogger at www.tabsoverspaces.com.