Sunday, 22 January 2023

Functional Programming - Error Type

In my functional library post, Functional Library Project, I mentioned the error type. The error type is a simple type that allows one to convery errors throughout the system.

Using errors versus exceptions is not an easy choice. Exceptions yield a call stack, useful for debugging. Errors do not yield a call stack. Still, with enough context information the error type is useful.

Exceptions are similar to using a goto statement. Normal program flow is lost and the exception must be handled higher upon the call stack.

Conversely, errors are returned by class methods/functions. Calling functions can catch the error and take appropriate action. The same can also be said about exceptions.

See my post, exceptions versus errors. for an insight into exception/error usage.


Top

In this post...

Top

Error Type

The error type is simply a class that maintains an error message. The constructor is protected as the idea is to create custom errors. The code is as follows...

public static partial class Fun
{
  public class Error
  {
    public string Message { get; }

    protected Error(string message)
    {
      this.Message = message;
    }

    public override string ToString() =>
      $"{this.Message}";
  }
}

Note, the class has no namespace and falls under the partial Fun class. This ensures that functional library code can be included by simply using the following include...

using static Fun
Top

Declare A New Error Type

Error types can be declared as nested classes or within the global (namespace) area. Personally, I prefer to make error classes (and exceptions generally) within the class. The stack trace picks up on this and can make debugging much easier. The following code illustrates the idea for a nested error type.

using static Fun;

namespace TestApp.Domain
{
  public class Customer
  {
    public class EmptyError : Error
    {
      public EmptyError(string varName) : base($"{varName} cannot be empty.") { }
    }

    public string FirstName { get; }
    public string LastName { get; }

    public static (Customer Customer, Error error) Create(string firstName, string lastName)
    {
      if (string.IsNullOrEmpty(firstName))
        return (null, new EmptyError("First Name"));

      if (string.IsNullOrEmpty(lastName))
        return (null, new EmptyError("Last Name"));
      return (new Customer(firstName, lastName), null);
    }


    private Customer(string firstName, string lastName)
    {
      this.FirstName = firstName;
      this.LastName = lastName;
    }
  }
}

Note the private constructor. The only way to create a new customer is to use the static Create method. The only downside is that the caller might ignore the error. In subsequent posts I will improve upon this by using Monads to ensure the caller must respond to errors.

Top

Improved Error Type Usage

In the last section, declare error type, I detailed how one might use the Error class to help signify incorrect class creation. The code allows callers to ignore the error, not great, so a more robust soluition is required.

The following code introduces a Customer result class. A modified customer class uses the new customer result class.

using System;
using static Fun;
namespace TestApp.Domain
{
  public class CustomerResult
  {
    private readonly Error _error;
    private readonly Customer _customer;
    
    public static implicit operator CustomerResult(Customer customer) =>
      new CustomerResult(customer);
      
    public static implicit operator CustomerResult(Error error) =>
      new CustomerResult(null, error);
      
    public CustomerResult(Customer customer, Error error = null)
    {
      _error = error;
      _customer = customer;
    }
      
    public R Match<R>(Func<Error, R> onError, Func<Customer, R> onOK) =>
      null == _error ? onOK(_customer) : onError(_error);
  }
}
Top

The revised customer code follows...

using static Fun;
namespace TestApp.Domain
{
  public class Customer
  {
    public class EmptyError : Error
    {
      public EmptyError(string varName) : base($"{varName} cannot be empty.") { }
    }    
    
    //public static (Customer Customer, Error error) Create(string firstName, string lastName)
    //{
    //  if (string.IsNullOrEmpty(firstName))
    //    return (null, new EmptyError("First Name"));
    //  if (string.IsNullOrEmpty(lastName))
    //   return (null, new EmptyError("Last Name"));
    // return (new Customer(firstName, lastName), null);
    //}
    
    public string FirstName { get; }
    public string LastName { get; } 	
  
    public static CustomerResult Create(string firstName, string lastName)
    {
      if (string.IsNullOrEmpty(firstName))
        return new EmptyError("First Name");
      if (string.IsNullOrEmpty(lastName))
        return new EmptyError("Last Name");
      return new Customer(firstName, lastName);
    }
  
    private Customer(string firstName, string lastName)
    {
      this.FirstName = firstName;
      this.LastName = lastName;
    }
  }
}
Top

Testing Error Type

Using the above code, we can try a fail and a pass scenario. Is always wise to test code, even if you are 100% sure it will work anyway. Before writing test code, it is useful to override some ToString methods.

// Add the following to the customer class
public override string ToString() =>
  $"{LastName}, {FirstName}";
  
// Add the following to the customer result class
public override string ToString() =>
  null == _error ? $"{_customer}" : $"{_error}";

Finally, the test/driver program can be coded. My tests include trying the following...

  • Creating a customer first name with a NULL value.
  • Creating a customer last name with a NULL value.
  • Creating a customer with a VALID first and last name.

The test code is as follows...

using System;
using TestApp.Domain;

namespace TestApp
{
  internal class Program
  {
    static void Main(string[] args)
    {
      var c1 = Customer
        .Create(null, "Bloggs");
      Console.WriteLine(c1);

      var c2 = Customer
        .Create("Fred", null);
      Console.WriteLine(c2);

      var c3 = Customer
        .Create("Fred", "Bloggs");
      Console.WriteLine(c3);
    }
  }
}

The following results should be presented by the console.

Top

No comments:

Post a Comment