Funktionell programmering med C#

Tidigare i våras höll vi en lunchdragning på Headlight där vi kollade på funktionell programmering och dess grundkoncept och hur man kunde göra för att använda dessa när man skriver C#-kod. Funktionell programmering har dom senaste åren slagit igenom väldigt brett och har kommit att bli extremt populärt.

Det som speciellt är spännande är att man samtidigt börjar använda äldre programmeringsspråk för att skriva moderna applikationer. Dessutom har man skapat nya språk som ska vara speciellt lämpade för att följa den funktionella paradigmen, t.ex. F#.

Vad är funktionell programmering?

Som antyddes ovan så är funktionell programmering inget som är speciellt nytt, det är rent av gammalt. Man kan säga att begreppet formades redan på 30-talet med grunderna i lambda calculus, lambdakalkyl. Man behöver inte gräva ner sig alltför mycket i den teorin för att greppa grunderna i funktionell programmering. Istället kan man beskriva det hyfsat konkret i punktform och låta programkoden:

  • sträva efter att ha samma grund som matematiska funktioner, givna in-parametrar ger ett förutsägbart resultat
  • undvika att förändra tillstånd, inga sidoeffekter
  • använda uttryck, expressions, istället för satser, statements
  • ha funktioner där resultatet endast beror på in-parametrarna
  • implementera oföränderliga typer, immutable types

Det finns otroligt mycket skrivet i ämnet, börja på Wikpedia Functional programming och borra dig neråt så har du lite att göra.

Jag har valt ut en topp-5-lista över egenskaper som beskriver paradigmen och tillsammans med denna lista även ge kodexempel, i C#, så gör det tydligt vad som avses. All kod finns här https://github.com/HeadlightAB/FunctionalProgrammingWithCSharp.

Koncept, topp-5

Pure functions

Funktioner, e.g. dess resultat, är endast beroende av sina in-parametrar. Detta gör att varje given input har ett och endast ett givet output. Det kan däremot finnas flera givna inputs som ger samma resultat, men det omkullkastar inte att given input ger ett förutsägbart output.

I begreppet pure functions ingår också att en funktion ska inte ha några som helst sidoeffekter, dess omgivning ska vara helt opåverkad.
function-f-projection

Nedan följer kod som visar funktionen Add och Increment i klassen Code. Add är pure medan Increment är inte pure.

...
using Xunit;  
... 
public class PureFunctionsTests  
{ 
  private readonly ITestOutputHelper _output;
  ...
  [Fact]
  public void Add_should_produce_the_Sum()
  {
      var sut = new Code();
      const int x = 1;
      const int y = 2;

      var result = sut.Add(x, y);

      Assert.Equal(3, result);
  }

  [Theory]
  [InlineData(1, 1000)]
  public void Increment_is_not_pure(int a, int numberOfInvocations)
  {
      var sut = new Code();

      for (var i = 0; i < numberOfInvocations; i++)
      {
          var result = sut.Increment(a);

          _output.WriteLine($"(invocation {i}) Increment {a} = {result}");

          try
          {
              Assert.Equal(1, result);
          }
          catch (Exception e)
          {
              _output.WriteLine($"Expected this; {e.Message}");
          }
      }
  }

  public class Code
  {
      public int Add(int a, int b) => a + b;

      private int _counter = 0;

      public int Increment(int a) => _counter += a;
  }
}

Rekursion före iteration

Det här innebär att man ska undvika for- och while-loopar och istället försöka använda sig av rekursion. Det finns såklart både för- och nackdelar med detta, det kan var mer resurskrävande men å andra sidan blir det ofta mindre kod att skriva och därmed mindre felbenäget. Den kod som inte innehåller några fel är den oskrivna koden.

Om man tittar på funktionen faktultet, f(x)=x!, så kan man veckla ut den enligt nedan och den är ett praktexempel på funktion att skriva rekursivt:
Factorial

Exempelkoden nedan visar implementationen av fakultet, en rekursiv version skriven på en rad som en expression body och motsvarande iterativa version. Båda ger såklart samma resultat men skillnaderna i implementation är tydliga:

...
using Xunit;  
...
public class RecursiveFunctionsTests  
{
    [Theory]
    [InlineData(0, 1)]
    [InlineData(1, 1)]
    public void Recursive_base_should_produce_expected_result(uint x, uint expected)
    {
        var sut = new Code();

        var result = sut.FactorialRecursive(x);

        Assert.Equal(expected, result);
    }

    [Theory]
    [InlineData(2, 2)]
    [InlineData(10, 3628800)]
    public void Recursive_should_produce_expected_result(uint x, uint expected)
    {
        var sut = new Code();

        var result = sut.FactorialRecursive(x);

        Assert.Equal(expected, result);
    }

    [Theory]
    [InlineData(0, 1)]
    [InlineData(1, 1)]
    [InlineData(2, 2)]
    [InlineData(10, 3628800)]
    public void Iterative_should_produce_expected_result(uint x, uint expected)
    {
        var sut = new Code();

        var result = sut.FactorialIteration(x);

        Assert.Equal(expected, result);
    }
}

public class Code  
{
    public uint FactorialRecursive(uint x) => x <= 1 ? 1 : x * FactorialRecursive(x - 1);

    public uint FactorialIteration(uint x)
    {
        uint result = 1;

        for (uint i = 2; i <= x; i++)
        {
            result *= i;
        }

        return result;
    }
}

I exemplet är funktionen FactorialRecursive implementerad som en sk expression-bodied function member vilket ger en extremt kompakt kod. Konstruktionen var en nyhet i C# 6 (https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-6) och har i senare versioner av C# utvecklats vidare och numera stöds syntaxen även i konstruktorer, destruktorer/finalizers mm (https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7#more-expression-bodied-members).

Referential transparency

Det här är ett koncept som inte är speciellt användbart när man skriver kod. Begreppet innebär att ett anrop till en funktion ska kunna ersättas med sitt resultat för givna inparametrar utan att resultatet eller beteendet i programmet ändras. Man kan ha med sig det i bakhuvudet när man skriver sin kod, man undviker då till exempel att skriva kod med sidoeffekter och förhoppningsvis så håller man sin kod renare från osunda beroende mellan kontexter.

Ett urspårat exempel på begreppet skulle i kod kunna se ut något i stil med:

public class Code  
{
    public int SomeFunction()
    {
        int sum = Sum(1,2); // = 3

        return sum * 2;
    }

    public int SomeEquivalentFunction()
    {
        int sum = 3; // replaced Sum(1,2)

        return sum * 2;
    }

    private int Sum(int a, int b) 
    {
        return a + b;
    }
}

First-class functions / higher-order functions

Dom flesta moderna programmeringsspråk har stöd för dessa två egenskaper:

  • First-class function: En funktion behandlas som en entitet som har stöd för samma operationer som andra entiteter. Exempel på entiteter är typer, objekt, värden etc och operationer som dom stödjer är att vara parameter, returneras från funktioner, förändras och tilldelas till en variabel. Det innebär alltså att en funktion ska kunna verka in-parameter till andra funktioner, en funktion ska kunna tilldelas till en variabel etc.
  • Higher-order function: En funktion ska antingen ta en eller flera funktioner som in-parameter OCH/ELLER returnera en funktion för att kallas en higher-order function.

Definitionen av first-class och higher-order functions är en smula abstrakt men i kod skulle det kunna se ut så här:

public class FirstClassFunctionsTests  
{
    [Fact]
    public void Should_support_first_class_function()
    {
        Func<int, int, int> aSumFuncNamed = ASumFuncNamed;
        Func<int, int, int> aSumFuncDelegate = delegate(int x, int y) { return x + y; };
        Func<int, int, int> aSumFuncLambda = (x, y) => x + y;

        Assert.Equal(3, aSumFuncNamed(1, 2));
        Assert.Equal(3, aSumFuncDelegate(1, 2));
        Assert.Equal(3, aSumFuncLambda(1, 2));
    }

    private int ASumFuncNamed(int x, int y) => x + y;
}

public class HigherOrderFunctionsTest  
{
    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(4)]
    [InlineData(5)]
    public void FunctionAsAParameter_OneMoreTest(int x)
    {
        static int Double(int a) => 2 * a;

        var result = IsEven(Double, x);

        Assert.True(result);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(10)]
    [InlineData(100)]
    public void FunctionAsAParameter_Test(int p)
    {
        static int Random(int max) => new Random().Next(max);

        var result = IsEven(Random, p) ? "even" : "odd";

        _output.WriteLine($"{result}");
    }

    private static bool IsEven(Func<int, int> func, int x) => func(x) % 2 == 0;
}

Notera i koden ovan användningen av delegat, lambdauttryck, funktionspekare och lokal namngiven funktion.

Immutable types

En instans av en typ som är immutable, oföränderlig, är ej möjlig att förändra när den en gång är skapad. En immutable type som innehåller metoder som förändrar dess egenskaper ska, per definition, returnera en ny instans av typen, dvs INTE modifiera det interna tillståndet hos instansen. Exemplet nedan demonstrerar konceptet:

public class Rectangle  
{
    public int Width { get; }
    public int Height { get; }

    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }

    public Rectangle Grow(int growth)
    {
        return new Rectangle(Width + growth, Height + growth);
    }

    public Rectangle GrowChained(int growth)
    {
        return Widen(growth).Raise(growth);
    }

    public Rectangle GrowUsingWith(int growth)
    {
        return With(Width + growth, Height + growth);
    }

    private Rectangle Widen(int growWidth)
    {
        return With(length: Width + growWidth);
    }

    private Rectangle Raise(int growHeight)
    {
        return With(height: Height + growHeight);
    }

    private Rectangle With(int length = -1, int height = -1)
    {
        return new Rectangle(length == -1 ? Width : length, height == -1 ? Height : height);
    }
}

public class ThisIsImmutableTests  
{
    [Fact]
    public void Prove_the_immutability()
    {
        var rectangleOne = new Rectangle(1, 2);

        var rectangleTwo = rectangleOne.Grow(10);

        Assert.Equal(1, rectangleOne.Width);
        Assert.Equal(2, rectangleOne.Height);

        Assert.Equal(11, rectangleTwo.Width);
        Assert.Equal(12, rectangleTwo.Height);

        Assert.NotSame(rectangleOne, rectangleTwo);
    }
}

Vi ser att Rectangle-typen har två egenskaper, publikt läsbara men INTE möjliga att påverka vid något annat tillfälle än vid själva skapandet av instansen, dvs i konstruktorn.

Om vi tittar på alla metoder i Rectangle-klassen så kommer alla dessa att returnera en ny instans av en rektangel, i och för sig centraliserat till den privata funktionen private Rectangle With(...), men hur som helst så returneras alltid en ny instans. Detta får som följd att immutable types är:

  • Trådsäkra, instanser ändras aldrig och därför kan dessa delas mellan trådar utan att problem uppstår.
  • Alltid i giltigt eller korrekt tillstånd, engelskans valid state, eftersom det finns aldrig någon vits med att returnera ett objekt i ett icke-giltigt tillstånd då detta tillstånd aldrig kan korrigeras och bli giltigt.

Summering

Koncepten ovan är dom, vad jag tycker, 5 viktigaste inom funktionell programmering. Dom här koncepten är enkla att följa även om man använder ett imperativt språk som t.ex C#. Man behöver inte vara totalt funktionellt anal för att dra nytta av koncepten. Ska man välja ett eller ett par koncept så tycker jag man ska titta lite extra på:

  • Immutable types/objects
  • Funktioner ska INTE ha några sidoeffekter

Fokus på dess två kommer att hjälpa dig att skriva robustare kod som är lättare att felsöka och avlusa.

Vill man fördjupa sig i paradigmen funktionell programmering så finns det otroligt mycket information att tillgå. Bland annat finns det flera podcasts helt och hållet "tillägnade" funktionell programmering https://purelyfunctional.tv/functional-programming-podcasts/.

Komplett källkod för exemplen ovan finns här https://github.com/HeadlightAB/FunctionalProgrammingWithCSharp.

Notera att en del exempel här inte stämmer överens till 100% med det som finns på Github.