Funktionale C#-Programmierung im Detail (13) Reine Funktion

Funktionale C#-Programmierung im Detail (13) Reine Funktion

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-pure-function

Die funktionale Programmierung fördert die Modellierung von Operationen mit reinen Funktionen.

Referentielle Transparenz und frei von Nebenwirkungen

Eine Funktion ist rein wenn:

  • Es gibt dieselbe Ausgabe, wenn dieselbe Eingabe gegeben wird. Mit anderen Worten, die Funktion ist referenziell transparent.
  • Es gibt keine offensichtliche Interaktion mit der aufrufenden Funktion oder der Außenwelt, mit anderen Worten, die Funktion hat keine Nebenwirkung. Hier sind einige Beispiele für Nebenwirkungen:
    • Ändern des Status, wie Datenmutation
    • Argumente, äußere Variable oder globale Variable ändern
    • I/O produzieren

Eine reine Funktion ist also wie eine mathematische Funktion, die eine einfache Beziehung zwischen einer Menge von Eingaben und einer Menge von Ausgaben ist, wobei jede bestimmte Eingabe einer bestimmten Ausgabe zugeordnet ist. Beispielsweise sind die folgenden Funktionen nicht referenziell transparent:

  • Console.Read, Console.ReadLine, Console.ReadKey:gibt bei jedem Aufruf eine unvorhersehbare Ausgabe aus
  • Random.Next, Guid.NewGuid:Gibt bei jedem Aufruf eine zufällige Ausgabe aus
  • DateTime.Now, DateTimeOffset.Now:gibt unterschiedliche Ausgabe, wenn es zu unterschiedlichen Zeiten aufgerufen wird

Und die folgenden Funktionen haben Seiteneffekte:

  • MutableDevice.Name’s Setter, MutableDevice.Price’s Setter im vorherigen Teil:Eigenschaften-Setter ändert normalerweise den Zustand und interagiert mit dem System.
  • Im System.Threading-Namespace, Thread.Start, Thread.Abort:ändert den Zustand
  • int.TryParse, Interlocked.Increase und jede Methode ändert das ref/out-Argument
  • Im System.Windows-Namespace, Application.SetExitCode:Ändert intern die globale Variable Environment.ExitCode
  • Console.Read, Console.ReadLine, Console.ReadKey, Console.Write, Console.Write, Console.WriteLine:erzeugt Konsolen-I/O
  • Im System.IO-Namespace erzeugen Directory.Create, Directory.Move, Directory.Delete, File.Create, File.Move, File.Delete, File.ReadAllBytes, File.WriteAllBytes Dateisystem-I/O
  • Im System.Net-Namespace WebRequest.GetRequestStreamAsync, WebRequest.GetResponseAsync und im System.Net.Http-Namespace HttpClient.GetAsync, HttpClient.PostAsync, HttpClinet.PutAsync, HttpClient.DeleteAsync:erzeugt Netzwerk-E/A
  • IDisposable.Dispose:Ändert den Status, um nicht verwaltete Ressourcen freizugeben

Genau genommen kann jede Funktion mit der Außenwelt interagieren. Normalerweise kann ein Funktionsaufruf zumindest die Hardware zum Laufen bringen, die elektrische Energie verbraucht und die Welt erwärmt. Hier werden bei der Identifizierung der Reinheit der Funktion nur explizite Wechselwirkungen berücksichtigt.

Im Gegensatz dazu sind die folgenden Funktionen rein, weil sie sowohl referenziell transparent als auch frei von Nebenwirkungen sind:

  • Die meisten mathematischen Funktionen, wie die arithmetischen Operatoren von decimal, die meisten statischen Methoden des System.Math-Typs usw. Nehmen Sie Math.Max ​​und Math.Min als Beispiele, ihre berechnete Ausgabe hängt nur von der Eingabe ab, und sie sind wohnhaft transparent, sie erzeugen auch keine Seiteneffekte, wie Zustandsänderung, Argumentänderung, globale Variablenänderung, I/O usw.:
    namespace System
    {
        public static class Math
        {
            public static int Max(int val1, int val2) => (val1 >= val2) ? val1 : val2;
    
            public static int Min(int val1, int val2) => (val1 <= val2) ? val1 : val2;
        }
    }
  • string.Concat, string.Substring, string.Insert, string.Replace, string.Trim, string.ToUpper, string.ToLower:Akzeptiert einen oder mehrere Strings als Eingabe und gibt einen neuen String aus, da String ein unveränderlicher Typ ist .
  • string.Length, Nullable.HasValue, Console.Error oder jeder Eigenschafts-Getter gibt einen Zustand zurück. Der Getter von MutableDevice.Name und der Getter von MutableDevice.Price sind ebenfalls rein. Für ein bestimmtes MutableDevice-Objekt geben sie einen vorhersagbaren Zustand zurück, und während der Ausführung der Getter ändern die Getter nicht den Zustand oder erzeugen andere Nebeneffekte.
  • Methoden des Objekts, wie GetHashCode, GetType, Equals, ReferenceEquals, ToString
  • Konvertierungsmethoden des Typs „System.Convert“, wie ToBoolean, ToInt32 usw.

Reine Funktion hat viele Vorteile, zum Beispiel:

  • Es beinhaltet keine Zustandsänderung, die eine Hauptquelle von Codeproblemen ist.
  • Es ist in sich abgeschlossen, mit stark verbesserter Testbarkeit und Wartbarkeit.
  • Wenn zwei reine Funktionsaufrufe keine Datenabhängigkeit haben, spielt die Reihenfolge der Funktionsaufrufe keine Rolle, was die parallele Datenverarbeitung wie Parallel LINQ erheblich vereinfacht.

Wie bereits erwähnt, gibt es auch ein spezielles Paradigma der funktionalen Programmierung, das als rein funktionale Programmierung bezeichnet wird und bei dem alle Operationen als reine Funktionsaufrufe modelliert werden. Folglich sind auch nur unveränderliche Werte und unveränderliche Datenstrukturen erlaubt. Einige wenige Sprachen, wie Haskell, sind für dieses Paradigma konzipiert. In Haskell verwaltet I/O mit Monad, was im Kapitel zur Kategorietheorie behandelt wird. Die anderen funktionalen Sprachen wie C# und F# werden als unreine funktionale Sprache bezeichnet.

PureAttribute- und Code-Verträge

.NET stellt System.Diagnostics.Contracts.PureAttribute bereit, um anzugeben, dass ein benannter Funktionsmember rein ist:

internal static partial class Purity
{
    [Pure]
    internal static bool IsPositive(int int32) => int32 > 0;

    internal static bool IsNegative(int int32) // Impure.
    {
        Console.WriteLine(int32.WriteLine()); // Side effect: console I/O.
        return int32 < 0;
    }
}

Es kann auch für einen Typ verwendet werden, um anzugeben, dass alle seine Funktionsmitglieder rein sind:

[Pure]
internal static class Pure
{
    internal static int Increase(int int32) => int32 + 1;

    internal static int Decrease(int int32) => int32 - 1;
}

Leider ist dieses Attribut nicht für allgemeine Zwecke und wird nur von .NET-Codeverträgen verwendet. Code Contracts ist ein Microsoft-Tool für .NET Framework. Es besteht aus:

  • Codieren Sie Vertrags-APIs unter System.Diagnostics.Contracts-Namespace, um Vorbedingungen, Nachbedingungen, Invariante, Reinheit usw. anzugeben, einschließlich des obigen PureAttribute.
  • Verträgt Assemblys für einige .NET Framework-Assemblys
  • Umschreiber und Analysator für die Kompilierzeit
  • Laufzeitanalyse

Um zu demonstrieren, wie [Pure] mit Code Contracts funktioniert, installieren Sie das Tool aus der Visual Studio Gallery, gehen Sie dann in Visual Studio zu den Projekteigenschaften und fügen Sie das Symbol für die bedingte Kompilierung CONTRACTS_FULL:

hinzu

Beachten Sie, dass es eine neue Registerkarte Code Contract gibt. Wechseln Sie zur Registerkarte und aktivieren Sie Perform Runtime Contract Checking:

Codeverträge können mit den statischen Methoden des Typs „System.Diagnostics.Contracts.Contract“ angegeben werden. Mit Contract-Methoden dürfen nur reine Funktionsaufrufe verwendet werden:

internal static int PureContracts(int int32)
{
    Contract.Requires<ArgumentOutOfRangeException>(IsPositive(int32)); // Function precondition.
    Contract.Ensures(IsPositive(Contract.Result<int>())); // Function post condition.

    return int32 + 0; // Function logic.
}

Für den Aufrufer der obigen Funktion kann das Code Contract-Tool die angegebene Vorbedingung und Nachbedingung zur Kompilierzeit und zur Laufzeit überprüfen, wenn die Überprüfung aktiviert ist. Und logischerweise sollte die Vor- und Nachbedingungsprüfung referenziell transparent und nebenwirkungsfrei sein. Im Gegensatz dazu ruft das folgende Beispiel eine unreine Funktion in einer Vorbedingung und einer Nachbedingung auf:

internal static int ImpureContracts(int int32)
{
    Contract.Requires<ArgumentOutOfRangeException>(IsNegative(int32)); // Function precondition.
    Contract.Ensures(IsNegative(Contract.Result<int>())); // Function post condition.

    return int32 + 0; // Function logic.
}

Beim Kompilieren gibt Code Contract eine Warnung aus:Detected call to method IsNegative(System.Int32)' without [Pure] in Contracts of method 'ImpureContracts(System.Int32)'.

[Pure] kann nicht für die anonyme Funktion verwendet werden. Und für alle benannten Funktionsmember muss [Pure] mit Vorsicht verwendet werden. Die folgende Methode wird als rein deklariert:

[Pure] // Incorrect.
internal static ProcessStartInfo Initialize(ProcessStartInfo processStart)
{
    processStart.RedirectStandardInput = false;
    processStart.RedirectStandardOutput = false;
    processStart.RedirectStandardError = false;
    return processStart;
}

Aber eigentlich wird es überhaupt nicht rein, indem es seinen Zustand ändert. Es gibt kein Tool, um den internen Code zur Kompilierungs- oder Laufzeit zu überprüfen und Warnungen oder Fehler auszugeben. Die Reinheit kann nur zur Designzeit künstlich sichergestellt werden.

Reinheit in .NET

Wenn Code kompiliert und für die Assembly erstellt wird, können seine Verträge entweder in derselben Assembly oder in einer separaten Vertragsassembly kompiliert werden. Für bereits gelieferte .NET Framework-FCL-Assemblys stellt Microsoft separate Vertragsassemblys für einige der am häufigsten verwendeten Assemblys bereit:

  • Microsoft.VisualBasic.Compatibility.Contracts.dll
  • Microsoft.VisualBasic.Contracts.dll
  • mscorlib.Contracts.dll
  • PresentationCore.Contracts.dll
  • PresentationFramework.Contracts.dll
  • System.ComponentModel.Composition.Contracts.dll
  • System.Configuration.Contracts.dll
  • System.Configuration.Install.Contracts.dll
  • System.Contracts.dll
  • System.Core.Contracts.dll
  • System.Data.Contracts.dll
  • System.Data.Services.Contracts.dll
  • System.DirectoryServices.Contracts.dll
  • System.Drawing.Contracts.dll
  • System.Numerics.Contracts.dll
  • System.Runtime.Caching.Contracts.dll
  • System.Security.Contracts.dll
  • System.ServiceModel.Contracts.dll
  • System.ServiceProcess.Contracts.dll
  • System.Web.ApplicationServices.Contracts.dll
  • System.Web.Contracts.dll
  • System.Windows.Forms.Contracts.dll
  • System.Xml.Contracts.dll
  • System.Xml.Linq.Contracts.dll
  • WindowsBase.Contracts.dll

Eine Vertragsbaugruppe enthält die Verträge (Vorbedingung, Nachbedingung, Invariante usw.) für APIs in einer bestimmten FLC-Baugruppe. Beispielsweise stellt mscorlib.Contracts.dll die Verträge für APIs in mscorlib.dll bereit, System.ComponentModel.Composition.Contracts.dll stellt die Verträge für APIs in System.ComponentModel.Composition.dll bereit usw. Die obige Math.Abs-Funktion wird bereitgestellt in mscorlib.dll, daher wird sein Paritätsvertrag in mscorlib.Contracts.dll bereitgestellt, mit derselben Signatur, enthält aber nur Verträge und keine Logik:

namespace System
{
    public static class Math
    {
        [Pure]
        public static int Abs(int value)
        {
            Contract.Requires(value != int.MinValue);
            Contract.Ensures(Contract.Result<int>() >= 0);
            Contract.Ensures((value - Contract.Result<int>()) <= 0);

            return default;
        }
    }
}

Für den Aufrufer von Math.Abs ​​kann das Code Contract-Tool die obige Vorbedingung und Nachbedingung aus mscorlib.Contracts.dll laden und die Prüfung zur Kompilierzeit und zur Laufzeit ausführen, wenn die Prüfung aktiviert ist. Die C#-Sprache ist nicht rein funktional konzipiert, ebensowenig wie .NET-APIs. Daher ist nur ein kleiner Prozentsatz der eingebauten Funktionen rein. Um dies zu demonstrieren, können diese Montageverträge durch Reflexion überprüft werden. Die in .NET integrierten Reflexions-APIs funktionieren mit diesem Assemblykontrast nicht gut. Beispiel:„mscorlib.Contracts.dll“ enthält den Typ „System.Void“, der von der .NET-Reflektion als besonderer Typ betrachtet wird und Abstürze verursacht. Das Mono.Cecil NuGet-Paket, eine Reflektionsbibliothek eines Drittanbieters, kann hier funktionieren. Das folgende LINQ to Objects-Beispiel ruft die Mono.Cecil-APIs auf, um die Vertragsassemblys für die öffentlichen Funktionsmember mit [Pure] abzufragen, und fragt dann alle öffentlichen Funktionsmember aller .NET Framework-FCL-Assemblys ab:

internal static void PureFunction(string contractsAssemblyDirectory, string gacDirectory = @"C:\Windows\Microsoft.NET\assembly")
{
    string[] contractAssemblyFiles = Directory
        .EnumerateFiles(contractsAssemblyDirectory, "*.dll")
        .ToArray();
    string pureAttribute = typeof(PureAttribute).FullName;
    // Query the count of all public function members with [Pure] in all public class in all contract assemblies.
    int pureFunctionCount = contractAssemblyFiles
        .Select(assemblyContractFile => AssemblyDefinition.ReadAssembly(assemblyContractFile))
        .SelectMany(assemblyContract => assemblyContract.Modules)
        .SelectMany(moduleContract => moduleContract.GetTypes())
        .Where(typeContract => typeContract.IsPublic)
        .SelectMany(typeContract => typeContract.Methods)
        .Count(functionMemberContract => functionMemberContract.IsPublic
            && functionMemberContract.CustomAttributes.Any(attribute =>
                attribute.AttributeType.FullName.Equals(pureAttribute, StringComparison.Ordinal)));
    pureFunctionCount.WriteLine(); // 2473

    string[] assemblyFiles = new string[] { "GAC_64", "GAC_MSIL" }
        .Select(platformDirectory => Path.Combine(gacDirectory, platformDirectory))
        .SelectMany(assemblyDirectory => Directory
            .EnumerateFiles(assemblyDirectory, "*.dll", SearchOption.AllDirectories))
        .ToArray();
    // Query the count of all public function members in all public class in all FCL assemblies.
    int functionCount = contractAssemblyFiles
        .Select(contractAssemblyFile => assemblyFiles.First(assemblyFile => Path.GetFileName(contractAssemblyFile)
            .Replace(".Contracts", string.Empty)
            .Equals(Path.GetFileName(assemblyFile), StringComparison.OrdinalIgnoreCase)))
        .Select(assemblyFile => AssemblyDefinition.ReadAssembly(assemblyFile))
        .SelectMany(assembly => assembly.Modules)
        .SelectMany(module => module.GetTypes())
        .Where(type => type.IsPublic)
        .SelectMany(type => type.Methods)
        .Count(functionMember => functionMember.IsPublic);
    functionCount.WriteLine(); // 83447
}

Infolgedessen sind in den oben genannten Mainstream-FCL-Versammlungen nur 2,96 % der Mitglieder öffentlicher Funktionen rein.