Your C # is already “functional,” just let it.

Hello, Habr! I present to you the translation of the original article "Your C # is already functional, but only if you let it" by Igal Tabachnik.

A few days ago, I tweeted a C # code snippet that implements FizzBuzz , using some of the new features in C # 8.0 . The tweet “became viral”, several people admired its conciseness and functionality, while others asked me why I did not write it in F #?

More than 4 years have passed since the last time I wrote in C #, and the fact that I usually use functional programming has clearly influenced the way I write code today. The snippet I wrote seems very neat and natural, but some people have expressed concerns that it does not look like C # code.
“It looks too functional.” They wrote to me.
Depending on who you ask, “functional programming” means different things to different people. But instead of discussing semantics, I would like to offer an explanation of why this FizzBuzz implementation seems functional.

First, let's look at what this code does:

public static void Main(string[] args)
{
      string FizzBuzz(int x) => 
            (x % 3 == 0, x % 5 == 0) switch
            {  
                  (true, true) => "FizzBuzz", 
                  (true, _) => "Fizz", 
                  (_, true) => "Buzz", 
                  _ => x.ToString()
            };
    
      Enumerable.Range(1, 100 ) 
            .Select(FizzBuzz).ToList() 
            .ForEach(Console.WriteLine); 
}

Here we create a local method, represented by a lambda expression, the result of which is calculated using a tuple.

The novelty here is the use of a tuple (pair) to work with the result of calculating two expressions together (x% 3 = = 0 and x% 5 = = 0). This allows you to use pattern matching to determine the final result. If none of the options matches, then by default a string representation of the number will be returned.

However, none of the many “functional” "features" used in this piece of code (including LINQ-style foreach loops) make this code functional in itself. What makes it functional is the fact that, with the exception of outputting the result to the console, all methods used in this program are expressions.

Simply put, expression is a question that always has an answer. In terms of programming, an expression is a combination of constants, variables, operators, and functions calculated by the runtime to compute (“return”) a value. To illustrate the difference with statements, let's write a more familiar version of FizzBuzz for C # programmers:

public static void Main(string[] args) 
{
      foreach( int x in Enumerable.Range(1, 100)) 
      {
             FizzBuzz(x);
      }
} 

public static void FizzBuzz( int x) 
{
      if (x % 3 == 0  && x % 5 == 0) 
             Console.WriteLine("FizzBuzz"); 
      else if (x % 3 == 0 ) 
             Console.WriteLine("Fizz"); 
      else if (x % 5 == 0 ) 
             Console.WriteLine("Buzz"); 
      else
             Console.WriteLine(x);
}

Naturally, this is far from being a "clean" code, and it can be improved, but one cannot but agree that this is ordinary C # code. Upon closer inspection of the FizzBuzz method, even considering its simplicity, it clearly shows several design problems.

First of all, this program violates the first of the principles of SOLID - the principle of single responsibility. It mixes the logic of calculating the output value based on the number and outputting this value to the console. As a result, it violates the principle of dependency inversion (the last of SOLID), tightly connecting with the output of the result to the console. Finally, such an implementation of the program makes it difficult to reuse and sandbox code. Of course, for such a simple program like this, it makes no sense to go into the intricacies of the design, otherwise something like this may turn out.

All of the above problems can be solved by separating the receipt of the value and output to the console. Even without using fancy language constructs, simply returning the resulting value to the calling code relieves us of responsibility for using this value.

public static string FizzBuzz(int x)
{
      if (x % 3 == 0 && x % 5 == 0)
            return "FizzBuzz";
      else if (x % 3 == 0)
            return "Fizz";
      else if (x % 5 == 0)
            return "Buzz";
      else
            return x.ToString();
}

Of course, these are not radical changes, but this is already enough:

  1. The FizzBuzz method is now an expression that takes a numeric value and returns a string.
  2. It has no other responsibilities and hidden effects, which turns it into a pure function.
  3. It can be used and tested independently, without any additional dependencies and settings.
  4. The code that calls this function is free to do anything with the result - now this is not our responsibility.

And this is the essence of functional programming - the program consists of expressions that result in any values, and these values ​​are returned to the calling code. These expressions, as a rule, are completely independent, and the result of their call depends only on the input data. At the very top, at the entry point (or sometimes called the "end of the world"), the values ​​returned by the functions collect and interact with the rest of the world. In object-oriented jargon, this is sometimes called “onion architecture” (or “ports and adapters”) - a clean core consisting of business logic and an imperative outer shell responsible for interacting with the outside world.

Using expressions instead of statements is used to some extent by all programming languages. C # evolved over time to introduce functions that make it easier to work with expressions: LINQ, expression-based methods, pattern matching, and more. These "features" are often called "functional" because they exist - in languages ​​like F # or Haskell, in which the existence of anything other than expressions is almost impossible.

In fact, this programming style is now encouraged by the C # team. In a recent speech at NDC London, Bill Wagner urges developers to change their (imperative) habits and adopt modern methods:


C # (and other imperative languages ​​such as Java) can be used functionally, but it requires a lot of effort. These languages ​​make functional style an exception, not the norm. I encourage you to learn functional programming languages ​​to become a first-class specialist.

All Articles