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)
{
    Assume.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 operation 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)
{
    Assume.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)
{
    Assume.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

I think using immutable objects wherever possible is a good idea. I like many of the FP style patterns which “wrap code” to either clean up resources, perform a function such as timing how long a piece of code took to complete, etc. At this stage, I prefer not to use Result like classes, the effects can cascade, and there’s already a Task<T> construct in C#. Overall, I think the C# team is doing a pretty good job in balancing the importing of FP principles into an OOP framework. For anyone interested in learning more, I highly recommend the book:

Functional Programming in C#

Advertisement

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 )

Facebook photo

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

Connecting to %s