Result or no Result

Recently I spent a couple of hours building a few classes to support some functional programming constructs in C#. At the end of the experiment, doubts have me backing out mostly.

In certain use cases, the new constructs are a nice fit, but they can easily be overused. And their usage has a cascading effect on the API design. I think OOP paradigms can benefit from the principles of FP, as long as they are judiciously applied. In fact, since C# 3.0 many functional programming constructs have been added to the language to good effect.

In functional programming you aim to write pure functions:

  • It returns the same result if given the same arguments (deterministic)
  • It does not cause any observable side effects

Considering the second point, you should avoid:

  • mutating state
  • leaking exceptions

Result

The main focus of my effort was a class to handle function return values:

  • Result (success and failure, with error details)
  • Result<T> (extends Result to contain a value)
  • ResultExt (extension methods for Result)

The classes facilitate conditional chaining, for example:

public static Result Save(string filepath, object obj)
{
    Expect.IsTrue(Files.IsValid(filepath), $"invalid path: {filepath}");

    return AsString(obj).OnSuccess(xml =>
        Try(() => File.WriteAllText(filepath, xml)));
}

If the object is successfully serialized to a string, the code then tries to write the information to a text file. The result of the operations is returned. If the serialization fails or the writing to text file, the failed result is returned. No exceptions are thrown. If all goes well, a success result it returned.

Note: AsString() in the above example returns a Result<string>.

You can chain as many methods as you like using:

  • OnSuccess
  • OnFailure
  • Then

This is the same code, but without chaining:

public static Result Save(string filepath, object obj)
{
    Expect.IsTrue(Files.IsValid(filepath), $"invalid path: {filepath}");

    var result = AsString(obj);
   
    return result.IsFailure
        ? result
        : Try(() => File.WriteAllText(filepath, result.Value)));
}

Again, no mutations, no side effects.

Try is one of the supporting methods from another class. Using is another which creates a resource, executes a function which consumes the resource, then cleans up the resource:

public static Result<T> Using<T, TResource>(Func<TResource, T> func) 
    where TResource : IDisposable, new()
{
    try
    {
        using var r = new TResource();
        return Result.Ok(func(r));
    }
    catch (Exception e)
    {
        return Result.Fail<T>(e.ToString());
    }
}

You would use it like this:

var result = Using<Customer, CustomerRepository>(r => r.FindById(customerId));

There’s no end to possibilities, for example if you wanted a parameterized resource:

Personally, I prefer the simple approach, even at the risk of leaking exceptions:

public static void Save(string filepath, object obj)
{
    Expect.IsTrue(Files.IsValid(filepath), $"invalid path: {filepath}");
    
    var xml = AsString(obj);
    File.WriteAllText(filepath, xml);
}

Note: AsString() here simply returns a string.

Result is certainly suitable in other use cases. For example, this function searches registered assemblies for the specified type:

public Result<Type> FindType(string fullname)
{
    foreach (var assembly in _assemblies)
    {
        var type = assembly.GetType(fullname);
        if (type != null) return Result.Ok(type);
    }

    return Result.Fail<Type>($"Unable to locate type: {fullname}");
}

Or a simpler case:

Testing

One of the benefits of using FP style pure functions is that it simplifies unit testing.

A lot of what we do to support unit testing involves design tweaks and scaffolding code to support state injection. We inject our mock state so we can validate the actual results.

In FP you try not to mutate state, so you avoid much of this overhead.

Concurrency

Not mutating state is also great for concurrent programming, there’s no need for thread synchronization, code can be isolated and executed on a CPU, less cache misses, etc.

Conclusion

The jury is still out on this one, I’ll see how it evolves 🙂

I think C# and Kotlin have done a good job in balancing the importing of FP principles into an OOP framework, but the paradigms are fundamentally different. To get the real benefit of FP you need to use an FP language like F#, Scala etc (just my opinion).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s