Concepts of functional programming
We can also distinguish functional programming from imperative programming by the concepts. The core ideas of functional programming are encapsulated in the constructs such as first class functions, higher order functions, purity, recursion over loops, and partial functions. We will discuss the concepts in this topic.
First-class and higher-order functions
In imperative programming, the given data is more important and are passed through series of functions (with side effects). Functions are special constructs with their own semantics. In effect, functions do not have the same place as variables and constants. Since a function cannot be passed as a parameter or returned as a result, they are regarded as second class citizens of the programming world. In the functional programming world, we can pass a function as a parameter and return function as a result. They obey the same semantics as variables and their values. Thus, they are first class citizens. We can also create function of functions called second order function through composition. There is no limit imposed on the composability of functions and they are called higher order functions.
Fortunately, the C# language supports these two concepts since it has a feature called function object, which has types and values. To discuss more details about the function object, let's take a look at the following code:
class Program { static void Main(string[] args) { Func<int, int> f = (x) => x + 2; int i = f(1); Console.WriteLine(i); f = (x) => 2 * x + 1; i = f(1); Console.WriteLine(i); } }
We can find the code in FuncObject.csproj
, and if we run it, it will display the following output on the console screen:
Why do we display it? Let's continue the discussion on function types and function values.
Tip
Hit Ctrl + F5 instead of F5 in order to run the code in debug mode but without the debugger. It's useful to stop the console from closing on the exit.
Function types
As with other objects in C#, function objects have a type as well. We can initialize the types in the function declaration. Here is the syntax to declare function objects:
Func<T1, T2, T3, T4, ..., T16, TResult>
Note that we have T1
until T16
, which are the types that correspond to input parameters, and TResult
is a type that corresponds to the return type. If we need to convert our previous mathematical function, f(x) = x + 2
, we can write it as follows:
Func<int, int> f = (x) => x + 2;
We now have a function f
, which has one argument-typed integer and the integer return type as well. Here, we use a lambda expression to define a delegate to be assigned to the object named f
with the Func
type. Don't worry if you are not familiar with delegate and lambda expressions yet. We will discuss them further in our next chapters.
Function values
To assign a value to function variable, there are the following possibilities:
- A function variable can be assigned to an existing method inside a class by its name using a reference. We can use delegate as reference. Let's take a look at the following code snippet:
class Program { delegate int DoubleAction(int inp); static void Main(string[] args) { DoubleAction da = Double; int doubledValue = da(2); } static int Double(int input) { return input * 2; } }
- As we can see in the preceding code, we assign
da
variable to existingDouble()
method usingdelegate
. - A function variable can be assigned to an anonymous function using a lambda expression. Let's look at the following code snippet:
class Program { static void Main(string[] args) { Func<int, int> da = input => input * 2; int doubledValue = da(2); } }
- As we can see, the
da
variable is assigned using lambda expression and we can use theda
variable like we use in previous code snippet.
Now we have a function variable and can assign a variable-integer-typed variable, for instance, to this function variable, as follows:
int i = f(1);
After executing the preceding code, the value of variable i
will be 3
since we pass 1
as the argument, and it will return 1 + 2
. We can also assign the function variable with another function, as follows:
f = (x) => 2 * x + 1; i = f(1);
We assign a new function, 2 * x + 1
, to variable f
, so we will retrieve 3
if we run the preceding code.
Pure functions
In the functional programming, most of the functions do not have side effects. In other words, the function doesn't change any variables outside the function itself. Also, it is consistent, which means that it always returns the same value for the same input data. The following are example actions that will generate side effects in programming:
- Modifying a global variable or static variable since it will make a function interact with the outside world.
- Modifying the argument in a function. This usually happens if we pass a parameter as a reference.
- Raising an exception.
- Taking input and output outside-for instance, get a keystroke from the keyboard or write data to the screen.
Note
Although it does not satisfy the rule of a pure function, we will use many Console.WriteLine()
methods in our program in order to ease our understanding in the code sample.
The following is the sample non-pure function that we can find in NonPureFunction1.csproj
:
class Program { private static string strValue = "First"; public static void AddSpace(string str) { strValue += ' ' + str; } static void Main(string[] args) { AddSpace("Second"); AddSpace("Third"); Console.WriteLine(strValue); } }
If we run the preceding code, as expected, the following result will be displayed on the console:
In this code, we modify the strValue
global variable inside the AddSpace
function. Since it modifies the variable outside, it's not considered a pure function.
Let's take a look at another non-pure function example in NonPureFunction2.csproj
:
class Program { public static void AddSpace(StringBuilder sb, string str) { sb.Append(' ' + str); } static void Main(string[] args) { StringBuilder sb1 = new StringBuilder("First"); AddSpace(sb1, "Second"); AddSpace(sb1, "Third"); Console.WriteLine(sb1); } }
We see the AddSpace
function again but this time with the addition of an argument-typed StringBuilder
argument. In the function, we modify the sb
argument with hyphen
and str
. Since we pass the sb
variable by reference, it also modifies the sb1
variable in the Main
function. Note that it will display the same output as NonPureFunction2.csproj
.
To convert the preceding two examples of non-pure function code into pure function code, we can refactor the code to be the following. This code can be found at PureFunction.csproj
:
class Program { public static string AddSpace(string strSource, string str) { return (strSource + ' ' + str); } static void Main(string[] args) { string str1 = "First"; string str2 = AddSpace(str1, "Second"); string str3 = AddSpace(str2, "Third"); Console.WriteLine(str3); } }
Running PureFunction.csproj
, we will get the same output compared to the two previous non-pure function code. However, in this pure function code, we have three variables in the Main
function. This is because in functional programming, we cannot modify the variable we have initialized earlier. In the AddSpace
function, instead of modifying the global variable or argument, it now returns a string value to satisfy the the functional rule.
The following are the advantages we will have if we implement the pure function in our code:
- Our code will be easy to be read and maintain because the function does not depend on external state and variables. It is also designed to perform specific tasks that increase maintainability.
- The design will be easier to be changed since it is easier to refactor.
- Testing and debugging will be easier since it's quite easy to isolate the pure function.
Recursive functions
In an imperative programming world, we have got destructive assignments to mutate the state if a variable. By using loops, one can change multiple variables to achieve the computational objective. In the functional programming world, since variables cannot be destructively assigned, we need a recursive function call to achieve the objective of looping.
Let's create a factorial function. In mathematical terms, the factorial of the nonnegative integer N
is the multiplication of all positive integers less than or equal to N
. This is usually denoted by N!
. We can denote the factorial of 7
as follows:
7! = 7 x 6 x 5 x 4 x 3 x 2 x 1 = 5040
If we look deeper at the preceding formula, we will discover that the pattern of the formula is as follows:
N! = N * (N-1) * (N-2) * (N-3) * (N-4) * (N-5) ...
Now, let's take a look at the following factorial function in C#. It's an imperative approach and can be found in the RecursiveImperative.csproj
file:
public partial class Program { private static int GetFactorial(int intNumber) { if (intNumber == 0) { return 1; } return intNumber * GetFactorial(intNumber - 1); } }
As we can see, we invoke the GetFactorial()
function from the GetFactorial()
function itself. This is what we call a recursive function. We can use this function by creating a Main()
method containing the following code:
public partial class Program { static void Main(string[] args) { Console.WriteLine( "Enter an integer number (Imperative approach)"); int inputNumber = Convert.ToInt32(Console.ReadLine()); int factorialNumber = GetFactorial(inputNumber); Console.WriteLine( "{0}! is {1}", inputNumber, factorialNumber); } }
We invoke the GetFactorial()
method and pass our desired number to the argument. The method will then multiply our number with what's returned by the GetFactorial()
method, in which the argument has been subtracted by 1. The iteration will last until intNumber - 1
is equal to 0, which will return 1.
Now, let's compare the preceding recursive function in the imperative approach with one in the functional approach. We will use the power of the Aggregate
operator in the LINQ feature to achieve this goal. We can find the code in the RecursiveFunctional.csproj
file. The code will look like what is shown in the following:
class Program { static void Main(string[] args) { Console.WriteLine( "Enter an integer number (Functional approach)"); int inputNumber = Convert.ToInt32(Console.ReadLine()); IEnumerable<int> ints = Enumerable.Range(1, inputNumber); int factorialNumber = ints.Aggregate((f, s) => f * s); Console.WriteLine( "{0}! is {1}", inputNumber, factorialNumber); } }
We initialize the ints
variable, which contains a value from 1 to our desired integer number in the preceding code, and then we iterate ints
using the Aggregate
operator. The output of RecursiveFunctional.csproj
will be completely the same compared to the output of RecursiveImperative.csproj
. However, we use the functional approach in the code in RecursiveFunctional.csproj
.