Showing posts with label Function. Show all posts
Showing posts with label Function. Show all posts

Wednesday, 23 November 2022

Functional Library - Errors

The success path for any given application is an easy path follow. Problems arise when errors need to be dealt with. I am of the mindset that exceptions are significantly different to errors. An exception, is typically a showstopper, null pointer exception, stack-overflow exception, out of memory exceptions. These type of exceptions usually result in a program crash and graceful handling can be difficult.

The other end of the scale consists of errors that one can typically handle without detrimental effect to the application. Examples include incorrect input, failure to connect to a database, a socket etc. The latter examples might be down to network failures so retries may be possible. In contrast one cannot perform a retry if an exception is raised (assuming the above exceptions).

Like I say this is not easy! There are other issues. Exceptions create a stack trace, extremely useful for debugging. Manual error handling offers no such gifts and can make debugging much harder.

In this post...

Exceptions vs Manual Error Handling

To avoid complexity, let's keep things simple. Exceptions denote exceptional events, null pointers, stack overflows, out of memory, etc. So, essentially, system errors. Typically, exceptions of the aforementioned results in program failure.

In contrast errors denote problems that do NOT necessarily result in program failure. Examples and possible solutions follow...

  • Entering an invalid email address. In this scenario, one could just prompt user to enter a new (valid) email address.
  • Database connection failure. The database might be offline, network maybe down. One could save transactional data to a file to be run later. This is a tricky problem and depends upon the database transaction that was about to occur. Context is the key, one needs to view the impact on the application.
  • Socket connection failure. Same techniques for database failure might be applied. Retry strategies in both scenarios will be required.
Top

Functions

Functions generate results or perform an action. Typically, in OO solutions, actions do not yield a result and may, instead throw an exception upon failure. Whilst throwing an exception might be the way forward, function signatures typically do not state that an exception may be thrown. Typically, one must read supporting documentation to reveal if a function may throw an exception. Throughout my career I have encountered code that calls a function and ignores any exceptions that might be thrown. This especially tends to be the case if said function can throw any number of exceptions.

I do not think exceptions are a bad thing, however, I do believe they are misused and a better mechanism is required.

Top

Function Honesty

Consider the following function...

int Divide(int number, int divisor)
{
  return number / divisor;
}

Clearly, if the divisor is zero a divide by zero exception will be thrown. Of course the function signature does not convey this. How can we deal with this type of scenario? I believe there are three ways...

  • Return a sentinel value
    In this case NaN.
  • Throw a divide by zero exception.
  • Return a result which either contains a valid value or a reason as to why the function failed.

Analysing the above I would come to the following conclusions...

  • The NaN informs me that the function resulted in a non-number, doesn't tell me why, but, at least I know the function failed. In this simple example one could probably fathom that the function failed as the divisor was zero. Functions exist in numerous libraries that use sentinel values where usage may not be so obvious.
  • The exception informs me why the function failed. However, I might not be in the correct place to catch and respond to the exception. Also, the function does not state that it might throw an exception. So, why would I be inclined to respond to exceptions.
  • The result solution, in this case, might be best. I can call the function and act accordingly upon the result. If the result denotes success, on my merry way I go. If the result denotes failure, I might retry, log the error and fail gracefully, maybe try a different execution path. Best of all, the function signatures indicates that the function may succeed or fail.

Now, consider the following function declaration...

int GetChar();

Most will understand that char is short for character. As such, one might assume that the function returns a character. However, this is clearly not the case as the function returns an int (signed integer). The reason for this is that -1 is a sentinel value that indicates end of file. Of course you will only know this if you read the documentation and said documentation is both accurate and up to date. Personally, I think a better way to to handle this situation is to use some kind of iterator. For example...

class ReadCharacters{
  public bool IsEof {get; private set;}
  public char Current { get; private set; }
  
  public bool Next()
  {
    int ch = GetChar();
    if (ch == -1)
    {
      IsEof = true;
      return false;
    }
    Current = (char)ch;
    return true;
  }
}

I am not going to pretend that the above code is foolproof. However, I think it conveys more information than "GetChar" and is harder to misuse. Both are good properties that help produce code with fewer bugs.

Top

Friday, 14 May 2021

Functional Programming

What is functional programming?

Functional programming places functions first. All aspects of the resultant software is built upon functions. In contrast, Object-Oriented development places objects first. In this case, objects encapsulate state and potentially, behaviour. In functional programming, data and behaviour are typically kept separate. In my experience, the two techniques can be summarised as follows.

  • Object-Oriented

    Well-suited for user interface development as widgets (e.g. buttons, text input, etc) can maintain state such as text, event handlers for when say a button is clicked or text is editied.

    Data types such as stacks, queues, dictionaries, etc. Managing state and exposing behaviour in single place makes sense.

  • Functional

    Better suited where partitioning state across different objects is non-sensical or becomes problematic.

    Data types are complex, for example invoice processing, game state and so on. More complex data types are better manipulated using functions rather than "attaching" functions to the data type (Functions attached to a data type are typically known as methods in Object-Oriented development).

Top

What is a function?

A function accepts one or more inputs and generates a single output. It is also possible for a function to accept zero inputs and just return a value. The main take should be that a function can only create an output based upon the input values supplied. This is important! What this means is that a function cannot use outside state (global state) to generate an output. A function that only uses supplied inputs to generate an output is known as a Pure Function

Top

How do I code a good function?

To create a solid, sound function that can be reasoned about ceratin rules must be followed.

  • Use pure functions

    As mentioned, a pure function generates an output based purely upon its inputs. Using global state is forbidden. Pure functions go a step further, executing code that produces any side-effects is forbidden. This includes writing to the console, writing to log files, updating a database. In short, a pure function should always produce the same output given the supplied inputs. No side-effects, no using global variables. In pure functional programming even throwing exceptions is no go. Makes sense as throwing an exception is a side-effect which goes against the notion of a pure function.

    An exception is really just a goto on steriods. Exceptions can make following program flow and debugging difficult. Well-written software shouldn't need debugging. Sure, debugging a new function whilst still in development is likely a must. Once the function has been tested you should be able to trust it and move on.

    There are ways to implement state changes whilst still adhering to the rules. I shall go into details later,

  • Use immutable data

    Mutable data is data than can change in place. I remember my C/C++ days when dealing with strings. It was common to modify existing strings. This is more performant than creating new string based upon old strings.

    Times have changed, as have data structures, memory and so on. Immutable data is now the way forward. This essentially means once you have created some data, an object, record whatever your language permits, it never changes. If you need different values, you create new data, possibly based upon the original data.

    Take some simple data, the data represents X and Y coordinates in 2D space. Using C#, the code to represent the data is as follows.

    
    public struct Point
    {
      // get and set are C# constructs that allow one to set or get a data value.
      // get allows one to read a data value.
      // set allows one to update (write to) a data value.
      public int X { get; set; }
      public int Y { get; set; }
    
      public Point(int x, int y)
      {
        this.X = x;
        this.Y = y;
      }
    
      public void Offset(int xOffset, int yOffset)
      {
        this.X += xOffset;
        this.Y += yOffset;
      }
    }
    

    Now assume the following test class

    
    public static class TestPoint
    {
      public static void Test1()
      {
        Point pt = new Point(20, 40);
        pt.X = 1000;
        pt.Offset(0,20);
        // pt now contains the values x=1000 and y=60.
      }
    }
    
    Top

    In the above code, pt was created to have an X value of 20 and a Y value of 40. The second statement assignes 1000 to X. This directly modifies the pt variable. This is known as in-place modification. The ability to modify data content in-place makes the data structure mutable. The code also contains an Offset method. This method adds x and y offsets to the existing data.

  • How do I refactor mutable code to immutable code?

    In this case, the transition from mutable to immutable code is easy. Make all data fields readonly and supply a constructor to initialise the data fields. The following code example illustrates the idea.

    
    public struct Point
    {
      // A field that contains only a get construct is deemed to be readonly.
      // The field value may only be assigned via the constructor.
      public int X { get; }
      public int Y { get; }
    
      public Point(int x, int y)
      {
        this.X = x;
        this.Y = y;
      }
    
      public Point WithOffset(int xOffset, int yOffset)
      {
        return new Point(X + xOffset, Y + yOffset);
      }
    }
    
    public static class TestPoint
    {
      public static void Test1()
      {
        // Create a pt variable as per previous example.
        Point pt = new Point(20, 40);
        
        // Unable to modify the pt variable as fields are readonly.
        // We need to create a new data, i.e. a new instance of Point.
        // We create a new point by adding 1000 to the current X and zero to the Y value.
        Point ptNew = pt.WithOffset(1000, 0);
      }
    }
    
    

    Notice how the new code doesn't quite behave in the same way as the previous code. In the previous code we directly set the X value to a 1000. To achieve the same result in the new, immutable data we need to add a new function, let's call it WithX.

    public struct Point
    {
      // A field that contains only a get construct is readonly.
      // The field value may only be assigned via the constructor.
      public int X { get; }
      public int Y { get; }
    
      public Point(int x, int y)
      {
        this.X = x;
        this.Y = y;
      }
    
      public Point WithOffset(int xOffset, int yOffset)
      {
        return new Point(X + xOffset, Y + yOffset);
      }
      
      public Point WithX(int x)
      {
        return new Point(x, Y);
      }
    }
    
    
Top