Refactoring Into Pure Functions

An important aspect of pure functional transformations is learning how to refactor code using pure functions.

Note

The common nomenclature in functional programming is that you refactor programs using pure functions. In Visual Basic and C++, this aligns with the use of functions in the respective languages. However, in C#, functions are called methods. For the purposes of this discussion, a pure function is implemented as a method in C#.

As noted previously in this section, a pure function has two useful characteristics:

  • It has no side effects. The function does not change any variables or the data of any type outside of the function.

  • It is consistent. Given the same set of input data, it will always return the same output value.

One way of transitioning to functional programming is to refactor existing code to eliminate unnecessary side effects and external dependencies. In this way, you can create pure function versions of existing code.

This topic discusses what a pure function is and what it is not. The Manipulating Information in a WordprocessingML Document tutorial shows how to manipulate a WordprocessingML document, and includes two examples of how to refactor using a pure function.

Eliminating Side Effects and External Dependencies

The following examples contrast two non-pure functions and a pure function.

Non-Pure Function that Changes a Class Member

In the following code, the HypenatedConcat function is not a pure function, because it modifies the aMember data member in the class:

public class Program
{
    private static string aMember = "StringOne";

    public static void HypenatedConcat(string appendStr)
    {
        aMember += '-' + appendStr;
    }

    public static void Main()
    {
        HypenatedConcat("StringTwo");
        Console.WriteLine(aMember);
    }
}
Module Module1
    Dim aMember As String = "StringOne"

    Public Sub HypenatedConcat(ByVal appendStr As String)
        aMember = aMember & "-" & appendStr
    End Sub

    Sub Main()
        HypenatedConcat("StringTwo")
        Console.WriteLine(aMember)
    End Sub
End Module

This code produces the following output:

StringOne-StringTwo

Note that it is irrelevant whether the data being modified has public or private access, or is a static (shared) member or an instance member. A pure function does not change any data outside of the function.

Non-Pure Function that Changes an Argument

Furthermore, the following version of this same function is not pure because it modifies the contents of its parameter, sb.

public class Program
{
    public static void HypenatedConcat(StringBuilder sb, String appendStr)
    {
        sb.Append('-' + appendStr);
    }

    public static void Main()
    {
        StringBuilder sb1 = new StringBuilder("StringOne");
        HypenatedConcat(sb1, "StringTwo");
        Console.WriteLine(sb1);
    }
}
Module Module1
    Public Sub HypenatedConcat(ByVal sb As StringBuilder, ByVal appendStr As String)
        sb.Append("-" & appendStr)
    End Sub

    Sub Main()
        Dim sb1 As StringBuilder = New StringBuilder("StringOne")
        HypenatedConcat(sb1, "StringTwo")
        Console.WriteLine(sb1)
    End Sub
End Module

This version of the program produces the same output as the first version, because the HypenatedConcat function has changed the value (state) of its first parameter by invoking the Append member function. Note that this alteration occurs despite that fact that HypenatedConcat uses call-by-value parameter passing.

Important noteImportant Note:

For reference types, if you pass a parameter by value, it results in a copy of the reference to an object being passed. This copy is still associated with the same instance data as the original reference (until the reference variable is assigned to a new object). Call-by-reference is not necessarily required for a function to modify a parameter.

Pure Function

This next version of the program hows how to implement the HypenatedConcat function as a pure function.

class Program
{
    public static string HyphenatedConcat(string s, string appendStr)
    {
        return (s + '-' + appendStr);
    }

    public static void Main(string[] args)
    {
        string s1 = "StringOne";
        string s2 = HyphenatedConcat(s1, "StringTwo");
        Console.WriteLine(s2);
    }
}
Module Module1
    Public Function HyphenatedConcat(ByVal s As String, ByVal appendStr As String) As String
        Return (s & "-" & appendStr)
    End Function

    Sub Main()
        Dim s1 As String = "StringOne"
        Dim s2 As String = HyphenatedConcat(s1, "StringTwo")
        Console.WriteLine(s2)
    End Sub
End Module

Again, this version produces the same line of output: StringOne-StringTwo. Note that to retain the concatenated value, it is stored in the intermediate variable s2.

One approach that can be very useful is to write functions that are locally impure (that is, they declare and modify local variables) but are globally pure. Such functions have many of the desirable composability characteristics, but avoid some of the more convoluted functional programming idioms, such as having to use recursion when a simple loop would accomplish the same thing.

Standard Query Operators

An important characteristic of the standard query operators is that they are implemented as pure functions.

For more information, see Standard Query Operators Overview.

See Also

Concepts

Introduction to Pure Functional Transformations

Functional Programming vs. Imperative Programming