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

No comments:

Post a Comment