Funktionale Programmierung

Funktionale Programmierung

# Funktion und Aktion

Funktion stellt einen Halter für parametrisierte anonyme Funktionen bereit. Die führenden Typen sind die Eingaben und der letzte Typ ist immer der Rückgabewert.

// square a number.
Func<double, double> square = (x) => { return x * x; };

// get the square root.
// note how the signature matches the built in method.
Func<double, double> squareroot = Math.Sqrt;

// provide your workings.
Func<double, double, string> workings = (x, y) => 
    string.Format("The square of {0} is {1}.", x, square(y))

Aktion Objekte sind wie void-Methoden, haben also nur einen Eingabetyp. Es wird kein Ergebnis auf den Bewertungsstapel gelegt.

// right-angled triangle.
class Triangle
{
    public double a;
    public double b;
    public double h;
}

// Pythagorean theorem.
Action<Triangle> pythagoras = (x) => 
    x.h = squareroot(square(x.a) + square(x.b));

Triangle t = new Triangle { a = 3, b = 4 };
pythagoras(t);
Console.WriteLine(t.h); // 5.

# Nullreferenzen vermeiden

C#-Entwickler müssen sich mit vielen Nullreferenz-Ausnahmen auseinandersetzen. F#-Entwickler tun dies nicht, weil sie den Option-Typ haben. Ein Option<>-Typ (einige bevorzugen Maybe<> als Namen) bietet einen Some- und einen None-Rückgabetyp. Es macht deutlich, dass eine Methode im Begriff ist, einen Null-Datensatz zurückzugeben.

Zum Beispiel können Sie Folgendes nicht lesen und wissen, ob Sie es mit einem Nullwert zu tun haben.

var user = _repository.GetUser(id);

Wenn Sie über die mögliche Null Bescheid wissen, können Sie einen Boilerplate-Code einführen, um damit umzugehen.

var username = user != null ? user.Name : string.Empty;

Was ist, wenn wir stattdessen eine Option<> zurückgegeben haben?

Option<User> maybeUser = _repository.GetUser(id);

Der Code macht jetzt deutlich, dass möglicherweise ein None-Datensatz zurückgegeben wird und der Boilerplate-Code zum Überprüfen auf Some oder None erforderlich ist:

var username = maybeUser.HasValue ? maybeUser.Value.Name : string.Empty;

Die folgende Methode zeigt, wie eine Option<>

zurückgegeben wird
public Option<User> GetUser(int id)
{
    var users = new List<User>
    {
        new User { Id = 1, Name = "Joe Bloggs" },
        new User { Id = 2, Name = "John Smith" }
    };

    var user = users.FirstOrDefault(user => user.Id == id);

    return user != null ? new Option<User>(user) : new Option<User>();
}

Hier ist eine minimale Implementierung von Option<>.

public struct Option<T>
{
    private readonly T _value;

    public T Value
    {
        get
        {
            if (!HasValue)
                throw new InvalidOperationException();

            return _value;
        }
    }

    public bool HasValue
    {
        get { return _value != null; }
    }

    public Option(T value)
    {
        _value = value;
    }

    public static implicit operator Option<T>(T value)
    {
        return new Option<T>(value);
    }
}

Um das obige zu demonstrieren, kann AvoidNull.csx mit dem C# REPL ausgeführt werden.

Wie gesagt, dies ist eine minimale Implementierung. Eine Suche nach „Vielleicht“ NuGet-Paketen ergibt eine Reihe guter Bibliotheken.

# Funktionen höherer Ordnung

Eine Funktion höherer Ordnung ist eine Funktion, die eine andere Funktion als Argument akzeptiert oder eine Funktion zurückgibt (oder beides).

Dies geschieht häufig mit Lambdas, beispielsweise wenn ein Prädikat an eine LINQ-Where-Klausel übergeben wird:

var results = data.Where(p => p.Items == 0);

Die Where()-Klausel könnte viele verschiedene Prädikate erhalten, was ihr eine beträchtliche Flexibilität verleiht.

Das Übergeben einer Methode an eine andere Methode wird auch beim Implementieren des Strategie-Entwurfsmusters beobachtet. Beispielsweise könnten verschiedene Sortiermethoden ausgewählt und an eine Sort-Methode eines Objekts übergeben werden, abhängig von den Anforderungen zur Laufzeit.

# Unveränderlichkeit

Unveränderlichkeit ist in der funktionalen Programmierung üblich und in der objektorientierten Programmierung selten.

Erstellen Sie beispielsweise einen Adresstyp mit veränderlichem Zustand:

public class Address () 
{
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City  { get; set; }
}

Jedes Stück Code könnte jede Eigenschaft im obigen Objekt ändern.

Erstellen Sie nun den unveränderlichen Adresstyp:

public class Address () 
{
    public readonly string Line1;
    public readonly string Line2;
    public readonly string City;

    public Address(string line1, string line2, string city) 
    {
        Line1 = line1;
        Line2 = line2;
        City  = city;
    }
}

Denken Sie daran, dass schreibgeschützte Sammlungen die Unveränderlichkeit nicht respektieren. Zum Beispiel

public class Classroom
{
    public readonly List<Student> Students;
    
    public Classroom(List<Student> students)
    {
        Students = students;
    }
}

ist nicht unveränderlich, da der Benutzer des Objekts die Sammlung ändern (Elemente hinzufügen oder daraus entfernen) kann. Um es unveränderlich zu machen, muss man entweder eine Schnittstelle wie IEnumerable verwenden, die keine hinzuzufügenden Methoden verfügbar macht, oder es zu einer ReadOnlyCollection machen.

public class Classroom
{
    public readonly ReadOnlyCollection<Student> Students;

    public Classroom(ReadOnlyCollection<Student> students)
    {
        Students = students;
    }
}

List<Students> list = new List<Student>();
// add students
Classroom c = new Classroom(list.AsReadOnly());   

Mit dem unveränderlichen Objekt haben wir die folgenden Vorteile:

  • Es befindet sich in einem bekannten Zustand (anderer Code kann es nicht ändern).
  • Es ist threadsicher.
  • Der Konstruktor bietet einen einzigen Ort für die Validierung.
  • Zu wissen, dass das Objekt nicht geändert werden kann, macht den Code leichter verständlich.

# unveränderliche Sammlungen

Der System.Collections.Immutable Das NuGet-Paket stellt unveränderliche Sammlungsklassen bereit.

# Artikel erstellen und hinzufügen

var stack = ImmutableStack.Create<int>();
var stack2 = stack.Push(1); // stack is still empty, stack2 contains 1
var stack3 = stack.Push(2); // stack2 still contains only one, stack3 has 2, 1

# Erstellen mit dem Builder

Bestimmte unveränderliche Sammlungen haben einen Builder innere Klasse, die verwendet werden kann, um kostengünstig große unveränderliche Instanzen zu erstellen:

var builder = ImmutableList.CreateBuilder<int>(); // returns ImmutableList.Builder
builder.Add(1);
builder.Add(2);
var list = builder.ToImmutable();

# Erstellen aus einem bestehenden IEnumerable

var numbers = Enumerable.Range(1, 5);
var list = ImmutableList.CreateRange<int>(numbers);

Liste aller unveränderlichen Sammlungstypen:

  • System.Collections.Immutable.ImmutableArray<T>
  • System.Collections.Immutable.ImmutableDictionary<TKey,TValue>
  • System.Collections.Immutable.ImmutableHashSet<T>
  • System.Collections.Immutable.ImmutableList<T>
  • System.Collections.Immutable.ImmutableQueue<T>
  • System.Collections.Immutable.ImmutableSortedDictionary<TKey,TValue>
  • System.Collections.Immutable.ImmutableSortedSet<T>
  • System.Collections.Immutable.ImmutableStack<T>