Gose

I hope to leave you refreshingly sour and salty

Gose

Warning! Reading this post may leave you with a sour and salty taste in your mouth.

A phrase I hear a lot is "leave exceptions for the truly exceptional". I like the phrase, it has the ring of simple and sound programming advice. Unfortunately, it's been anything but simple to implement.

The problem is that C# doesn't have an easy way to describe "if successful, expect a TSuccess, otherwise expect a TError". In "This is your App on Scala 3", John De Goes criticizes many popular typed programming languages with having a "Record Overload" problem. The problem being that the only way to describe a "thing" in these programming languages is with a record or class. Better options like unions exist in other languages like Scala. C# is one of these limited languages. The only programming construct available to us in C# is a record, we have no form of union type in C# as of this writing, so our modes of expression are limited. We have to get "creative" when we want more expression.

Go Style Error Handling

There are ways to make something like a union type in C#. I even made my own Either type at one point. The problem I have with all of the Result and Either types I've seen ported to C# is that they aren't native to the platform and they all differ slightly in their API. The result is you need to recreate the type in every project, or create a library around it. Even if one of those options is acceptable, people who use your libraries will likely be forced into your error handling package. If other libraries are using a different package, you end up with type gymnastics in order to convert between the two. I think we need something different. Something useable by non-power users. One standard to rule them all.

While I would prefer to use a programming language with unions over one without them any day of the week, sometimes I don't have a choice. If I have to use a record to represent success or error, I think Go style error handling is a nice way to handle it.

Gose, or Go Style Errors, is an experiment targeting this problem in C#. And like the beer of the same name, it might sound really gross.

Errors in Go

Go has no concept of "throwing" exceptions and "catching" them. If an error occurs, it's either handled immediately, or returned by the function where the error occurred.

If a function can return a value or an error, a tuple like construct is used where the left hand side is the possible successful result, and the right hand side is the possible error. If the result is an error, the left (success) side is nil and the right (error) side contains data. If the result is a success, the right (error) side is nil and the left (success) side contains data.

For example:

func canErr() (data int, err error) {
    result, err := someFunctionThatCanError()
    if err != nil {
        return nil, err
    }
    return result, nil
}
hopefully this is valid go ;)

No try/catch no throw. In some ways it reminds me of a less robust Either or Result type.

Go is able to enforce the convention with a great suite of tools. I'm wondering if we could do something similar in C#.

The C# Convention

In C#, we have access to tuples. Much like Go, we could use them for handling our "non-exceptional" errors.

I don't write a lot of Go, or any really. My experience with it is primarily for study. Because of that I can never remember which side the error goes on in the tuple. Sometimes it's the same for Result in F#. Haskell's Either, when used for errors, has "Success" on the right and the "Error" on the left. It's easy for me to remember "right is right" since "right" is analogous to "success". I'm hoping it's as easy to remember for others, so I propose a convention to use the left side as the error side, and the right as the success side - (error, success).

Unfortunately this proposal differs from Go's convention. But what could go wrong with a change from one system's standard?

What I like about this convention over custom error handling types is that everyone has access to the type out of the box. If you write code with a modern version of C# you have tuples. Because of that, people should already be familiar with them. It might not be elegant, but it's native and recognizeable. It requires no extra effort by a C# programmer to learn how to use it. Power users can extend the tuple convention with their own sets of methods. Developers who just want to get data and get on with it can use tranditional null checking and move on.

Where Gose Fits in

I created a tiny library called gose to experiment with some of these "power user" features on top of the convention. I know this won't sit well with many developers, but for some reason I like to do heretical things.

With the convention in place, we can add a few extension methods to the structure - Select<Err, Data, Result>, SelectMany<Err, Data, Result>, and SelectMany<Err, Data, Result, Select> and get access to C#'s query expression syntax.

With these methods in place, we can act like the like the type has a monadic structure, and use it just like we might use a functional Result:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Gose {

    public static class GoseTupleExtensions {

        public static (Err? Error, Result? Data) Select<Err, Data, Result>(
            this (Err? Error, Data? Data) @this, 
            Func<Data, Result> fn) =>
            @this switch {
                (null, Data d) => (default, fn(d)),
                _ => (@this.Error, default)
            };


        public static (Err? Error, Result? Data) SelectMany<Err, Data, Result>(
            this (Err? Error, Data? Data) @this, 
            Func<Data, (Err?, Result?)> fn) =>
            @this switch {
                (null, Data d) => fn(d),
                _ => (@this.Error, default)
            };


        public static (Err? Error, Select? Data) SelectMany<Err, Data, Result, Select>(
            this (Err? Error, Data? Data) @this,
            Func<Data, (Err?, Result?)> fn,
            Func<Data, Result, Select> selector) =>
            @this switch {
                (null, Data d) => fn(d).Select(data => selector(d, data)),
                _ => (@this.Error, default)
            };

        
        public static (Result? Error, Data? Data) SelectErr<Err, Data, Result>(
            this (Err? Error, Data? Data) @this,
            Func<Err, Result> fn) =>
            @this switch {
                (Err e, null) => (fn(e), default),
                _ => (default, @this.Data)
            };

        public static (Err? Error, Data? Data) Select<Err, Data>(
            this (Err? Error, Data? Data) @this,
            (Err? Error, Data? Data) other) =>
            @this switch {
                (Err _e, null) => other,
                _ => @this
            };
    }
}

With that code alone, we gain access to a Railway Oriented Programming style of error handling. Here's how to use it:

// Error in first result retains error through the expression
(string? Error, int? Data) first = ("no data available", null);
(string? Error, int? Data) second = (null, 20);

var (error, data) =
    (from x in first
     from y in second
     select x + y);

Assert.Equal("no data available", error);
Assert.Null(data);

// Error in second result
(string? Error, int? Data) first = (null, 10);
(string? Error, int? Data) second = ("divide by zero", null);

var (error, data) =
    (from x in first
     from y in second
     select x + y);

Assert.Equal("divide by zero", error);
Assert.Null(data);

// Success in both results allows calculation
(string? Error, int? Data) first = (null, 10);
(string? Error, int? Data) second = (null, 20);

var (error, data) =
    (from x in first
     from y in second
     select x + y);

Assert.Equal(30, data);
Assert.Null(error);

I'm hoping that Rosalyn Analyzers can give us hints in our editors to help enforce the convention. With hints from the compiler I think this could be a nice, minimally invasive, way to handle those errors that aren't truly exceptional.

Will you join me in a toast? Or am I just out of good ideas?

Reference:

Railway Oriented Programming, Deep Heresy in C#, XKCD Standards, gose repo, my Either type,  The Go programming language, Rosalyn Analyzers, Query Expression Basics, Haskell Either, F# Result