Codekonventionen für F#

Die folgenden Konventionen sind das Ergebnis der Arbeit mit großen F#-Codebasen. Die fünf Prinzipien guten F#-Codes bilden die Grundlage jeder Empfehlung. Sie beziehen sich auf die Entwurfsrichtlinien für F#-Komponenten, gelten aber für jeden F#-Code, nicht nur für Komponenten wie Bibliotheken.

Organisieren von Code

F# bietet zwei primäre Möglichkeiten zum Organisieren von Code: Module und Namespaces. Diese sind ähnlich, weisen jedoch die folgenden Unterschiede auf:

  • Namespaces werden als .NET-Namespaces kompiliert. Module werden als statische Klassen kompiliert.
  • Namespaces befinden sich stets auf der obersten Ebene. Module können sich auf der obersten Ebene befinden und in anderen Modulen geschachtelt sein.
  • Namespaces können mehrere Dateien umfassen, Module nicht.
  • Module können mit [<RequireQualifiedAccess>] und [<AutoOpen>] ergänzt werden.

Die folgenden Richtlinien helfen Ihnen bei der Verwendung dieser Elemente zur Organisation Ihres Codes.

Bevorzugen von Namespaces auf oberster Ebene

Für jeden öffentlich nutzbaren Code werden Namespaces gegenüber Modulen auf oberster Ebene bevorzugt. Da sie als .NET-Namespaces kompiliert sind, können sie von C# aus konsumiert werden, ohne auf using static zu greifen.

// Recommended.
namespace MyCode

type MyClass() =
    ...

Die Verwendung eines Moduls auf oberster Ebene mag nicht anders aussehen, wenn es nur von F# aus aufgerufen wird, aber für C#-Consumer kann es eine Überraschung sein, dass sie sich MyClass mit dem MyCode-Modul qualifizieren müssen, wenn sie das spezifische using static-C#-Konstrukt nicht kennen.

// Will be seen as a static class outside F#
module MyCode

type MyClass() =
    ...

[<AutoOpen>] überlegt verwenden

Das Konstrukt [<AutoOpen>] kann den Umfang dessen, was dem Aufrufer zur Verfügung steht, beeinträchtigen, und die Antwort auf die Frage, woher etwas stammt, ist „magisch“. Das ist nicht gut. Eine Ausnahme dieser Regel ist die F#-Kernbibliothek selbst (obwohl diese Tatsache auch etwas kontrovers ist).

Es ist jedoch praktisch, wenn Sie Hilfsfunktionen für eine öffentliche API haben, die Sie getrennt von dieser öffentlichen API organisieren möchten.

module MyAPI =
    [<AutoOpen>]
    module private Helpers =
        let helper1 x y z =
            ...

    let myFunction1 x =
        let y = ...
        let z = ...

        helper1 x y z

Auf diese Weise können Sie die Details der Implementierung sauber von der öffentlichen API einer Funktion trennen, ohne dass Sie bei jedem Aufruf eine Hilfsfunktion vollständig qualifizieren müssen.

Darüber hinaus können Erweiterungsmethoden und Ausdrucks-Generatoren auf Namespace-Ebene mithilfe von [<AutoOpen>] ausgedrückt werden.

Verwenden Sie [<RequireQualifiedAccess>] immer dann, wenn Namen in Konflikt geraten könnten oder meinen, dass dies der Lesbarkeit dient

Das Hinzufügen des Attributs [<RequireQualifiedAccess>] zu einem Modul gibt an, dass das Modul möglicherweise nicht geöffnet wird und dass Verweise auf die Elemente des Moduls expliziten qualifizierten Zugriff erfordern. Das Modul Microsoft.FSharp.Collections.List verfügt beispielsweise über dieses Attribut.

Dies ist nützlich, wenn Funktionen und Werte im Modul Namen aufweisen, die wahrscheinlich mit Namen in anderen Modulen in Konflikt stehen. Die Anforderung eines qualifizierten Zugriffs kann die langfristige Wartbarkeit und die Fähigkeit einer Bibliothek, sich weiterzuentwickeln, erheblich verbessern.

[<RequireQualifiedAccess>]
module StringTokenization =
    let parse s = ...

...

let s = getAString()
let parsed = StringTokenization.parse s // Must qualify to use 'parse'

Topologisches Sortieren von open-Anweisungen

In F# ist die Reihenfolge der Deklarationen wichtig, einschließlich der open-Anweisungen (und open type, die weiter unten open genannt wurden). Dies ist anders als bei C#, wo die Wirkung von using und using static unabhängig von der Reihenfolge dieser Anweisungen in einer Datei ist.

In F# können Elemente, die in einem Bereich geöffnet werden, ein Shadowing bereits vorhandener Elemente durchführen. Das bedeutet, dass das Neuanordnen von open-Anweisungen die Bedeutung des Codes verändern kann. Daher ist eine willkürliche Sortierung aller open-Anweisungen (z. B. alphanumerisch) nicht zu empfehlen, damit Sie nicht ein anderes Verhalten als das erwartete generieren.

Wir empfehlen Ihnen stattdessen, sie topologisch zu sortieren, d. h. Ihre open-Anweisungen in der Reihenfolge anzuordnen, in der die Ebenen Ihres Systems definiert sind. Die alphanumerische Sortierung innerhalb verschiedener topologischer Ebenen kann ebenfalls erwägt werden.

Hier sehen Sie beispielsweise die topologische Sortierung für die öffentliche API-Datei des F#-Compilerdiensts:

namespace Microsoft.FSharp.Compiler.SourceCodeServices

open System
open System.Collections.Generic
open System.Collections.Concurrent
open System.Diagnostics
open System.IO
open System.Reflection
open System.Text

open FSharp.Compiler
open FSharp.Compiler.AbstractIL
open FSharp.Compiler.AbstractIL.Diagnostics
open FSharp.Compiler.AbstractIL.IL
open FSharp.Compiler.AbstractIL.ILBinaryReader
open FSharp.Compiler.AbstractIL.Internal
open FSharp.Compiler.AbstractIL.Internal.Library

open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.Ast
open FSharp.Compiler.CompileOps
open FSharp.Compiler.CompileOptions
open FSharp.Compiler.Driver

open Internal.Utilities
open Internal.Utilities.Collections

Ein Zeilenumbruch trennt topologische Ebenen, wobei jede Ebene anschließend alphanumerisch sortiert wird. Dadurch wird Code sauber organisiert, ohne dass versehentlich ein Shadowing für Werte durchgeführt wird.

Verwenden von Klassen zur Aufnahme von Werten mit Nebenwirkungen

Die Initialisierung eines Werts kann häufig Nebenwirkungen haben, wie z. B. das Instanziieren eines Kontexts für eine Datenbank oder andere Remoteressource. Es ist verlockend, solche Dinge in einem Modul zu initialisieren und in nachfolgenden Funktionen zu verwenden:

// Not recommended, side-effect at static initialization
module MyApi =
    let dep1 = File.ReadAllText "/Users/<name>/connectionstring.txt"
    let dep2 = Environment.GetEnvironmentVariable "DEP_2"

    let private r = Random()
    let dep3() = r.Next() // Problematic if multiple threads use this

    let function1 arg = doStuffWith dep1 dep2 dep3 arg
    let function2 arg = doStuffWith dep1 dep2 dep3 arg

Dies ist aus einigen Gründen häufig problematisch:

Erstens wird die Anwendungskonfiguration mit dep1 und dep2 in die Codebasis gepusht. Dies ist bei größeren Codebasen schwierig zu verwalten.

Zweitens sollten statisch initialisierte Daten keine Werte enthalten, die nicht threadsicher sind, wenn Ihre Komponente selbst mehrere Threads verwendet. Dies wird eindeutig durch dep3 verletzt.

Schließlich wird die Modulinitialisierung in einen statischen Konstruktor für die gesamte Kompilierungseinheit kompiliert. Wenn bei der Initialisierung von Bindungswerten des Typs „let“ in diesem Modul ein Fehler auftritt, manifestiert sich dieser als TypeInitializationException, der dann für die gesamte Lebensdauer der Anwendung zwischengespeichert wird. Dies kann schwierig zu diagnostizieren sein. In der Regel gibt es eine innere Ausnahme, die Sie zu erklären versuchen können, aber wenn es keine gibt, können Sie nicht sagen, was die eigentliche Grundursache ist.

Verwenden Sie stattdessen einfach eine einfache Klasse zum Aufnehmen von Abhängigkeiten:

type MyParametricApi(dep1, dep2, dep3) =
    member _.Function1 arg1 = doStuffWith dep1 dep2 dep3 arg1
    member _.Function2 arg2 = doStuffWith dep1 dep2 dep3 arg2

Dies ermöglicht Folgendes:

  1. Das Pushen eines abhängigen Zustands außerhalb der API selbst.
  2. Die Konfiguration kann jetzt außerhalb der API erfolgen.
  3. Fehler bei der Initialisierung für abhängige Werte manifestieren sich wahrscheinlich nicht als TypeInitializationException.
  4. Die API ist jetzt einfacher zu testen.

Fehlerverwaltung

Die Fehlerverwaltung in großen Systemen ist ein komplexes und nuanciertes Unterfangen, und es gibt keine Patentrezepte, um sicherzustellen, dass Ihre Systeme fehlertolerant sind und sich einwandfrei verhalten. Die folgenden Leitlinien sollen Ihnen dabei helfen, sich in diesem schwierigen Umfeld zurechtzufinden.

Darstellen von Fehlerfällen und unzulässigen Zuständen in Typen, die Ihrer Domäne eigen sind

Mit diskriminierten Unions gibt Ihnen F# die Möglichkeit, fehlerhafte Programmzustände in Ihrem Typsystem darzustellen. Beispiel:

type MoneyWithdrawalResult =
    | Success of amount:decimal
    | InsufficientFunds of balance:decimal
    | CardExpired of DateTime
    | UndisclosedFailure

In diesem Fall gibt es drei bekannte Möglichkeiten, wie das Abheben von Geld von einem Konto fehlschlagen kann. Jeder Fehlerfall wird im Typ dargestellt und kann somit im gesamten Programm sicher behandelt werden.

let handleWithdrawal amount =
    let w = withdrawMoney amount
    match w with
    | Success am -> printfn $"Successfully withdrew %f{am}"
    | InsufficientFunds balance -> printfn $"Failed: balance is %f{balance}"
    | CardExpired expiredDate -> printfn $"Failed: card expired on {expiredDate}"
    | UndisclosedFailure -> printfn "Failed: unknown"

Wenn Sie die verschiedenen Möglichkeiten modellieren können, wie etwas in Ihrer Domäne fehlschlagen kann, wird Code zur Fehlerbehandlung im Allgemeinen nicht mehr als etwas behandelt, mit dem Sie sich zusätzlich zum regulären Programmablauf beschäftigen müssen. Er ist dann einfach ein Teil des normalen Programmablaufs und gilt nicht als Ausnahme. Dafür sprechen zwei Hauptvorteile:

  1. Die Verwaltung ist einfacher, sobald sich Ihre Domäne im Laufe der Zeit ändert.
  2. Für Fehlerfälle sind Komponententests einfacher.

Verwenden von Ausnahmen, wenn Fehler nicht mit Typen dargestellt werden können

Nicht alle Fehler können in einer Problemdomäne dargestellt werden. Diese Arten von Fehlern sind Ausnahmen, weshalb es in F# möglich ist, Ausnahmen zu erzeugen und abzufangen.

Zunächst wird empfohlen, die Leitlinien für den Entwurf von Ausnahmen zu lesen. Diese gelten auch für F#.

Die wichtigsten Konstrukte, die in F# zum Auslösen von Ausnahmen zur Verfügung stehen, sollten in der folgenden Rangfolge berücksichtigt werden:

Funktion Syntax Zweck
nullArg nullArg "argumentName" Löst eine System.ArgumentNullException mit dem angegebenen Argumentnamen aus.
invalidArg invalidArg "argumentName" "message" Löst eine System.ArgumentException mit dem angegebenen Argumentnamen samt Nachricht aus.
invalidOp invalidOp "message" Löst eine System.InvalidOperationException mit der angegebenen Nachricht aus.
raise raise (ExceptionType("message")) Allgemeiner Mechanismus zum Auslösen von Ausnahmen.
failwith failwith "message" Löst eine System.Exception mit der angegebenen Nachricht aus.
failwithf failwithf "format string" argForFormatString Löst eine System.Exception mit einer Nachricht aus, die durch die Formatzeichenfolge und ihre Eingaben bestimmt wird.

Verwenden Sie nullArg, invalidArg und invalidOp als Mechanismus zum Auslösen von ArgumentNullException, ArgumentException und InvalidOperationException, sofern angebracht.

Die Funktionen failwith und failwithf sollten im Allgemeinen vermieden werden, da sie den Basistyp Exception auslösen und keine spezifische Ausnahme. Gemäß den Leitlinien für den Entwurf von Ausnahmen möchten Sie nach Möglichkeit spezifischere Ausnahmen auslösen.

Verwenden von Syntax zur Behandlung von Ausnahmen

F# unterstützt Ausnahmemuster über die Syntax try...with:

try
    tryGetFileContents()
with
| :? System.IO.FileNotFoundException as e -> // Do something with it here
| :? System.Security.SecurityException as e -> // Do something with it here

Die Abstimmung von Funktionalität, die im Falle einer Ausnahme benötigt wird, mit einem Musterabgleich kann etwas schwierig sein, wenn Sie den Code sauber halten wollen. Eine dieser Möglichkeiten hierfür ist der Einsatz aktiver Muster als Instrument zur Gruppierung von Funktionen rund um einen Fehlerfall mit einer Ausnahme selbst. Sie können beispielsweise eine API nutzen, die, wenn sie eine Ausnahme auslöst, wertvolle Informationen in den Metadaten der Ausnahme enthält. Das Aufheben des Umbruchs eines nützlichen Werts im Textkörper der abgefangenen Ausnahme innerhalb des aktiven Musters und die Rückgabe dieses Werts kann in einigen Situationen hilfreich sein.

Keine monadische Fehlerbehandlung verwenden, um Ausnahmen zu ersetzen

Ausnahmen werden oft als Tabu im reinen funktionalen Paradigma betrachtet. In der Tat verletzen Ausnahmen die Reinheitsvorschriften, sodass Sie diese als nicht unbedingt funktional betrachten können. Dies ignoriert jedoch die Realität, nämlich dass Code ausgeführt werden muss und Laufzeitfehler auftreten können. Generell sollten Sie beim Schreiben von Code davon ausgehen, dass die meisten Dinge nicht rein oder vollständig sind, um unangenehme Überraschungen auf ein Minimum zu reduzieren (wie z. B. leere catch in C# oder die falsche Verwaltung der Stapelüberwachung, das Verwerfen von Informationen).

Es ist wichtig, die folgenden zentralen Stärken und Aspekte von Ausnahmen im Hinblick auf ihre Relevanz und Angemessenheit in der .NET-Runtime und dem sprachübergreifenden Ökosystem insgesamt zu berücksichtigen:

  • Sie enthalten detaillierte Diagnoseinformationen, die beim Debuggen eines Problems hilfreich sind.
  • Sie werden von der Runtime und anderen .NET-Sprachen bestens verstanden.
  • Sie können im Vergleich zu Code, der sich die Mühe macht, Ausnahmen zu vermeiden, indem eine Teilmenge ihrer Semantik ad hoc implementiert wird, eine erhebliche Menge an Bausteinen reduzieren.

Dieser dritte Punkt ist kritisch. Bei nicht trivialen, komplexen Vorgängen kann der Verzicht auf Ausnahmen dazu führen, dass Sie es mit Strukturen wie dieser zu tun haben:

Result<Result<MyType, string>, string list>

Das kann leicht zu instabilem Code führen, wie Musterabgleich bei Fehlern des Typs „als Zeichenfolgen typisiert“:

let result = doStuff()
match result with
| Ok r -> ...
| Error e ->
    if e.Contains "Error string 1" then ...
    elif e.Contains "Error string 2" then ...
    else ... // Who knows?

Außerdem kann es verlockend sein, jede Ausnahme mit dem Wunsch nach einer „einfachen“ Funktion, die einen „angenehmeren“ Typ zurückgibt, zu schlucken:

// Can be problematic due to discarding the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with _ -> None

Leider kann tryReadAllText zahlreiche Ausnahmen auslösen, die auf den unterschiedlichsten Dingen beruhen, die in einem Dateisystem passieren können. Dieser Code verwirft jegliche Informationen darüber, was in Ihrer Umgebung tatsächlich schief laufen könnte. Wenn Sie diesen Code durch einen Ergebnistyp ersetzen, kehren Sie zur Analyse der Fehlermeldung „als Zeichenfolgen typisiert“ zurück:

// Problematic, callers only have a string to figure the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Ok
    with e -> Error e.Message

let r = tryReadAllText "path-to-file"
match r with
| Ok text -> ...
| Error e ->
    if e.Contains "uh oh, here we go again..." then ...
    else ...

Und wenn Sie das Objekt der Ausnahme selbst in den Konstruktor Error aufnehmen, sind Sie gezwungen, den Typ der Ausnahme am Ort des Aufrufs statt in der Funktion zu behandeln. Auf diese Weise werden kontrollierte Ausnahmen erstellt, mit denen der Aufrufer einer API bekanntermaßen nichts anfangen kann.

Eine bessere Alternative zu den obigen Beispielen ist es, spezifische Ausnahmen abzufangen und einen sinnvollen Wert im Kontext der jeweiligen Ausnahme zurückzugeben. Wenn Sie die Funktion tryReadAllText wie folgt modifizieren, hat None mehr Bedeutung:

let tryReadAllTextIfPresent (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with :? FileNotFoundException -> None

Anstatt als Catch-All-Klausel zu fungieren, behandelt diese Funktion nun den Fall, dass eine Datei nicht gefunden wurde, richtig und weist diese Bedeutung einer Rückgabe zu. Dieser Rückgabewert kann diesem Fehlerfall zugeordnet werden, ohne dabei Kontextinformationen zu verwerfen oder den Aufrufer zu zwingen, sich mit einem Fall zu befassen, der an dieser Stelle im Code möglicherweise nicht relevant ist.

Typen wie Result<'Success, 'Error> eignen sich für einfache Vorgänge, bei denen sie nicht geschachtelt sind. Optionale F#-Typen sind perfekt, um darzustellen, wenn etwas entweder irgendetwas oder nichts zurückgeben könnte. Sie sind jedoch kein Ersatz für Ausnahmen und sollten nicht als Ersatz für Ausnahmen dienen. Vielmehr sollten sie mit Bedacht eingesetzt werden, um bestimmte Aspekte der Richtlinie für die Ausnahme- und Fehlerverwaltung gezielt anzugehen.

Teilweise Anwendung und punktfreie Programmierung

F# unterstützt die teilweise Anwendung und somit verschiedene Möglichkeiten, in einem punktfreien Stil zu programmieren. Dies kann für die Wiederverwendung von Code innerhalb eines Moduls oder der Implementierung von bestimmten Elementen von Vorteil sein, ist aber nichts, was Sie öffentlich verfügbar machen sollten. Im Allgemeinen ist die punktfreie Programmierung an sich keine Tugend und kann für Personen, die nicht mit diesem Stil vertraut sind, eine erhebliche kognitive Hürde darstellen.

Keine teilweise Anwendung und kein Currying in öffentlichen APIs verwenden

Mit wenigen Ausnahmen kann die Verwendung der teilweise Anwendung in öffentlichen APIs für Consumer verwirrend sein. Normalerweise sind mit let gebundene Werte in F#-Code Werte und nicht etwa Funktionswerte. Das Kombinieren von Werten und Funktionswerten kann dazu führen, dass Sie ein paar Codezeilen sparen und dafür einen ziemlichen kognitiven Zusatzaufwand in Kauf nehmen müssen, insbesondere in Kombination mit Operatoren wie >>, um Funktionen zusammenzusetzen.

Berücksichtigen der Auswirkungen von Tools für die punktfreie Programmierung

Funktionen mit Currying bezeichnen ihre Argumente nicht. Dies hat Auswirkungen auf Tools. Betrachten Sie die folgenden beiden Funktionen:

let func name age =
    printfn $"My name is {name} and I am %d{age} years old!"

let funcWithApplication =
    printfn "My name is %s and I am %d years old!"

Beide sind gültige Funktionen, aber funcWithApplication ist eine Funktionen mit Currying. Wenn Sie in einem Editor auf deren Typen zeigen, sehen Sie Folgendes:

val func : name:string -> age:int -> unit

val funcWithApplication : (string -> int -> unit)

Am Aufrufort zeigen Ihnen QuickInfos in Tools wie Visual Studio die Typsignatur an, aber da keine Namen definiert sind, werden keine Namen angezeigt. Namen sind für ein gelungenes API-Design von entscheidender Bedeutung, da sie dem Aufrufer helfen, die Bedeutung der API besser zu verstehen. Die Verwendung von punktfreiem Code in der öffentlichen API kann das Verständnis für Aufrufer erschweren.

Wenn Sie auf punktfreien Code wie funcWithApplication stoßen, der öffentlich nutzbar ist, sollten Sie eine vollständige η-Erweiterung durchführen, damit das Tool sinnvolle Namen für Argumente erkennen kann.

Außerdem kann das Debuggen von punktfreiem Code schwierig, wenn nicht gar unmöglich sein. Tools zum Debuggen sind auf an Namen gebundene Werte angewiesen (z. B. let-Bindungen), damit Sie Zwischenwerte auf halbem Wege der Ausführung überprüfen können. Wenn Ihr Code keine zu überprüfenden Werte enthält, gibt es auch nichts zu debuggen. In der Zukunft werden möglicherweise Tools zum Debuggen entwickelt, die diese Werte auf Grundlage zuvor ausgeführter Pfade synthetisieren, aber es ist keine gute Idee, sich auf potenzielle Debugfunktionalität zu verlassen.

Erwägen der teilweisen Anwendung als Technik zum Verringern interner Bausteine

Im Gegensatz zum vorherigen Punkt ist die teilweise Anwendung ein gut geeignetes Tool, um Bausteine innerhalb einer Anwendung oder die tieferen internen Elemente einer API zu reduzieren. Sie kann für Komponententests bei der Implementierung komplizierterer APIs hilfreich sein, bei denen Bausteine oft mühsam zu handhaben sind. Der folgende Code zeigt beispielsweise, wie Sie das erreichen können, was die meisten Simulationsframeworks Ihnen bieten, ohne eine externe Abhängigkeit von einem solchen Framework in Kauf nehmen und eine damit verbundene spezifische API erlernen zu müssen.

Betrachten Sie zum Beispiel die folgende Lösungstopografie:

MySolution.sln
|_/ImplementationLogic.fsproj
|_/ImplementationLogic.Tests.fsproj
|_/API.fsproj

ImplementationLogic.fsproj kann Code verfügbar machen, wie z. B.:

module Transactions =
    let doTransaction txnContext txnType balance =
        ...

type Transactor(ctx, currentBalance) =
    member _.ExecuteTransaction(txnType) =
        Transactions.doTransaction ctx txnType currentBalance
        ...

Komponententests von Transactions.doTransaction in ImplementationLogic.Tests.fsproj sind einfach:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

Durch die teilweise Anwendung von doTransaction mit einem simulierten Kontextobjekt können Sie die Funktion in allen Ihren Komponententests aufrufen, ohne jedes Mal einen simulierten Kontext konstruieren zu müssen:

module TransactionTests

open Xunit
open TransactionTypes
open TransactionsTestingUtil
open TransactionsTestingUtil.TransactionsTestable

let testableContext =
    { new ITransactionContext with
        member _.TheFirstMember() = ...
        member _.TheSecondMember() = ... }

let transactionRoutine = getTestableTransactionRoutine testableContext

[<Fact>]
let ``Test withdrawal transaction with 0.0 for balance``() =
    let expected = ...
    let actual = transactionRoutine TransactionType.Withdraw 0.0
    Assert.Equal(expected, actual)

Wenden Sie diese Technik nicht universell auf Ihre gesamte Codebasis an. Sie ist jedoch eine gute Möglichkeit, Codebausteine für komplizierte interne Elemente und Komponententests dieser internen Elemente zu reduzieren.

Zugriffssteuerung

F# bietet mehrere Optionen für die Zugriffssteuerung, die von den in der .NET-Runtime verfügbaren Optionen geerbt wurden. Diese sind nicht nur für Typen geeignet, sondern auch für Funktionen.

Bewährte Methoden im Kontext von Bibliotheken, die häufig genutzt werden:

  • Bevorzugen Sie Nicht-public-Typen und -Member, solange Sie diese nicht öffentlich nutzbar machen müssen. Dies minimiert auch, woran Consumer koppeln müssen.
  • Bemühen Sie sich, alle Hilfsfunktionen „private“ zu halten.
  • Erwägen Sie die Verwendung von [<AutoOpen>] in einem privaten Modul von Hilfsfunktionen, sollten sie zahlreich werden.

Typrückschluss und Generics

Typrückschluss kann Ihnen das Eingeben einer Menge von Bausteinen ersparen. Und die automatische Generalisierung im F#-Compiler kann Ihnen helfen, generischeren Code fast ohne zusätzlichen Aufwand zu schreiben. Diese Features sind jedoch nicht universell gut geeignet.

  • Erwägen Sie die Bezeichnung von Argumentnamen mit expliziten Typen in öffentlichen APIs, und setzen Sie dabei nicht auf Typrückschluss.

    Der Grund dafür ist, dass Sie die Kontrolle über die Form Ihrer API haben sollten und nicht der Compiler. Obwohl der Compiler beim Rückschluss von Typen gute Arbeit leisten kann, ist es möglich, dass sich die Form Ihrer API ändert, wenn die internen Elemente, auf die sie sich stützt, andere Typen haben. Dies ist möglicherweise das, was Sie möchten, aber es wird mit ziemlicher Sicherheit zu einer Änderung der API führen, mit der nachgeschaltete Consumer dann umgehen müssen. Wenn Sie stattdessen explizit die Form Ihrer öffentlichen API steuern, können Sie diese Breaking Changes unter Kontrolle halten. In DDD-Begriffen kann dies als Antikorruptionsebene betrachtet werden.

  • Erwägen Sie, Ihren generischen Argumenten einen aussagekräftigen Namen zu geben.

    Sofern Sie nicht tatsächlich generischen Code schreiben, der nicht spezifisch für eine bestimmte Domäne gilt, kann ein aussagekräftiger Name anderen Programmierern helfen, die Domäne zu verstehen, in der sie arbeiten. Ein Typparameter namens 'Document im Kontext der Interaktion mit einer Dokumentendatenbank verdeutlicht beispielsweise, dass die Funktion oder das Member, mit der/dem Sie arbeiten, generische Dokumententypen akzeptieren kann.

  • Erwägen Sie, generische Typparameter mit PascalCase zu benennen.

    Dies ist die allgemeine Vorgehensweise in .NET. Daher wird empfohlen, PascalCase anstelle von snake_case oder camelCase zu verwenden.

Schließlich ist die automatische Generalisierung nicht immer ein Segen für Personen, die noch nicht mit F# oder einer großen Codebasis vertraut sind. Bei Verwendung generischer Komponenten gibt es kognitiven Mehraufwand. Wenn darüber hinaus automatisch generalisierte Funktionen nicht mit verschiedenen Eingabetypen verwendet werden (geschweige denn, wenn sie als solche verwendet werden sollen), gibt es keinen wirklichen Vorteil, generisch zu sein. Überlegen Sie stets, ob der Code, den Sie schreiben, tatsächlich davon profitiert, generisch zu sein.

Leistung

Berücksichtigen von Strukturen für kleine Typen mit hohen Zuteilungsraten

Die Verwendung von Strukturen (auch als Werttypen bezeichnet) kann häufig zu einer höheren Leistung für Code führen, da die Zuteilung von Objekten in der Regel vermieden wird. Strukturen sind jedoch nicht stets ein Garant für mehr Schnelligkeit: Wenn die Größe der Daten in einer Struktur 16 Byte überschreitet, kann das Kopieren der Daten oft zu einem höheren CPU-Aufwand führen als die Verwendung eines Verweistyps.

Um festzustellen, ob Sie eine Struktur verwenden sollten, prüfen Sie die folgenden Bedingungen:

  • Wenn die Größe Ihrer Daten maximal 16 Bytes ist.
  • Wenn es wahrscheinlich ist, dass in einem ausgeführten Programm viele Instanzen dieser Typen im Arbeitsspeicher vorhanden sind.

Wenn die erste Bedingung zutrifft, sollten Sie generell eine Struktur verwenden. Wenn beides zutrifft, sollten Sie fast immer eine Struktur verwenden. Es kann zwar einige Fälle geben, in denen die oben genannten Bedingungen zutreffen, aber die Verwendung einer Struktur nicht besser oder schlechter als die Verwendung eines Verweistyps ist, aber diese Fälle sind eher selten. Es ist jedoch wichtig, dass Sie bei solchen Änderungen immer Messungen vornehmen und sich nicht auf Annahmen oder Ihre Intuition verlassen.

Erwägen von Strukturtupeln, wenn kleine Werttypen mit hohen Zuteilungsraten gruppiert werden

Betrachten Sie die folgenden beiden Funktionen:

let rec runWithTuple t offset times =
    let offsetValues x y z offset =
        (x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let (x, y, z) = t
        let r = offsetValues x y z offset
        runWithTuple r offset (times - 1)

let rec runWithStructTuple t offset times =
    let offsetValues x y z offset =
        struct(x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let struct(x, y, z) = t
        let r = offsetValues x y z offset
        runWithStructTuple r offset (times - 1)

Wenn Sie diese Funktionen mit einem statistischen Benchmarktool wie BenchmarkDotNet vergleichen, werden Sie feststellen, dass die runWithStructTuple-Funktion, die Strukturtupel verwendet, 40 % schneller ausgeführt wird und keinen Arbeitsspeicher zuteilt.

Allerdings werden diese Ergebnisse in Ihrem eigenen Code nicht stets der Fall sein. Wenn Sie eine Funktion als inline markieren, kann Code, der Verweistupel verwendet, einige zusätzliche Optimierungen erhalten, oder Code, der Zuteilungen vornehmen würde, kann einfach wegoptimiert werden. Sie sollten stets die Ergebnisse messen, wenn es um Leistung geht, und keinesfalls auf der Grundlage von Annahmen oder Ihrer Intuition handeln.

Erwägen von Strukturdatensätzen, wenn der Typ klein ist und hohe Zuteilungsraten aufweist

Die weiter oben beschriebene Faustregel gilt auch für F#-Datensatztypen. Betrachten Sie die folgenden Datentypen und Funktionen, die sie verarbeiten:

type Point = { X: float; Y: float; Z: float }

[<Struct>]
type SPoint = { X: float; Y: float; Z: float }

let rec processPoint (p: Point) offset times =
    let inline offsetValues (p: Point) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processPoint r offset (times - 1)

let rec processStructPoint (p: SPoint) offset times =
    let inline offsetValues (p: SPoint) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processStructPoint r offset (times - 1)

Dies ähnelt dem vorherigen Tupelcode, aber dieses Mal werden im Beispiel Datensätze und eine innere Inlinefunktion verwendet.

Wenn Sie diese Funktionen mit einem statistischen Benchmarktool wie BenchmarkDotNet vergleichen, werden Sie feststellen, dass processStructPoint fast 60 % schneller ausgeführt wird und nichts dem verwalteten Heap zuteilt.

Erwägen von Strukturen des Typs „Diskriminierte Unions“, wenn der Datentyp klein ist und hohe Zuteilungsraten hat

Die vorherigen Beobachtungen zur Leistung mit Strukturtupeln und -datensätzen gelten auch für diskriminierte Unions in F#. Betrachten Sie folgenden Code:

    type Name = Name of string

    [<Struct>]
    type SName = SName of string

    let reverseName (Name s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> Name

    let structReverseName (SName s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> SName

Es ist üblich, für die Domänenmodellierung diskriminierte Unions für Einzelfälle wie diese zu definieren. Wenn Sie diese Funktionen mit einem statistischen Benchmarktool wie BenchmarkDotNet vergleichen, werden Sie feststellen, dass structReverseName für kleine Zeichenfolgen etwa 25 % schneller ausgeführt wird als reverseName. Bei großen Zeichenfolgen schneiden beide etwa gleich gut ab. In diesem Fall ist es also eine Struktur stets vorzuziehen. Wie bereits erwähnt, sollten Sie stets Messungen durchführen und sich nicht auf Annahmen oder Ihre Intuition stützen.

Obwohl das vorherige Beispiel gezeigt hat, dass eine Struktur des Typs „Diskriminierte Unions“ eine bessere Leistung erzielt, sind bei der Modellierung einer Domäne größere diskriminierte Unions üblich. Größere Datentypen wie diese funktionieren möglicherweise nicht so gut, wenn es sich um Strukturen handelt, die von den auf sie angewendeten Vorgängen abhängen, da mehr Kopiervorgänge erforderlich sein könnten.

Unveränderlichkeit und Mutation

F#-Werte sind standardmäßig unveränderlich, wodurch Sie bestimmte Fehlerklassen vermeiden können (insbesondere solche, die mit Gleichzeitigkeit bzw. Parallelität zu tun haben). In bestimmten Fällen kann eine Arbeitseinheit jedoch am besten durch direkte Mutation des Zustands implementiert werden, um eine optimale (oder sogar vernünftige) Effizienz der Ausführungszeit oder Zuteilung von Arbeitsspeicher zu erreichen. Dies ist in F# mit dem Schlüsselwort mutable auf Optionsbasis möglich.

Die Verwendung von mutable in F# steht womöglich im Widerspruch zur funktionalen Reinheit. Das ist verständlich, aber funktionale Reinheit kann überall im Widerspruch zu Leistungszielen stehen. Ein Kompromiss besteht darin, die Mutation so zu kapseln, dass der Aufrufer sich nicht darum kümmern muss, was passiert, wenn er eine Funktion aufruft. Dies ermöglicht Ihnen, eine Funktionsschnittstelle über eine mutationsbasierte Implementierung für leistungskritischen Code zu schreiben.

Außerdem können Sie mit den F# let-Bindungskonstrukten Bindungen in andere verschachteln. Dies kann genutzt werden, um den Gültigkeitsbereich von mutable-Variablen eng oder auf dem theoretisch kleinsten Wert zu halten.

let data =
    [
        let mutable completed = false
        while not completed do
            logic ()
            // ...
            if someCondition then
                completed <- true   
    ]

Kein Code kann auf die Stummschaltung completed zugreifen, die nur zum Initialisieren data des gebundenen Werts verwendet wurde.

Umschließen von veränderlichem Code in unveränderlichen Schnittstellen

Mit dem Ziel referenzieller Transparenz ist es von entscheidender Bedeutung, Code zu schreiben, der die veränderliche Schattenseite leistungsrelevanter Funktionen nicht verfügbar macht. Beispielsweise implementiert der folgende Code die Funktion Array.contains in der F#-Kernbibliothek:

[<CompiledName("Contains")>]
let inline contains value (array:'T[]) =
    checkNonNull "array" array
    let mutable state = false
    let mutable i = 0
    while not state && i < array.Length do
        state <- value = array[i]
        i <- i + 1
    state

Ein mehrfacher Aufruf dieser Funktion ändert weder das zugrunde liegende Array, noch müssen Sie einen veränderlichen Zustand beibehalten, wenn Sie es nutzen. Es ist referenziell transparent, auch wenn in nahezu jeder Codezeile Mutationen verwendet werden.

Erwägen des Kapselns veränderlicher Daten in Klassen

Im vorherigen Beispiel wurde eine einzelne Funktion verwendet, um Vorgänge mit veränderlichen Daten zu kapseln. Dies ist für komplexere Datensets nicht immer ausreichend. Betrachten Sie die folgenden Funktionsgruppen:

open System.Collections.Generic

let addToClosureTable (key, value) (t: Dictionary<_,_>) =
    if t.ContainsKey(key) then
        t[key] <- value
    else
        t.Add(key, value)

let closureTableCount (t: Dictionary<_,_>) = t.Count

let closureTableContains (key, value) (t: Dictionary<_, HashSet<_>>) =
    match t.TryGetValue(key) with
    | (true, v) -> v.Equals(value)
    | (false, _) -> false

Dieser Code ist leistungsfähig, macht aber die mutationsbasierte Datenstruktur verfügbar, für deren Wartung Aufrufer verantwortlich sind. Diese kann innerhalb einer Klasse ohne zugrunde liegende Member umschlossen werden, die sich ändern können:

open System.Collections.Generic

/// The results of computing the LALR(1) closure of an LR(0) kernel
type Closure1Table() =
    let t = Dictionary<Item0, HashSet<TerminalIndex>>()

    member _.Add(key, value) =
        if t.ContainsKey(key) then
            t[key] <- value
        else
            t.Add(key, value)

    member _.Count = t.Count

    member _.Contains(key, value) =
        match t.TryGetValue(key) with
        | (true, v) -> v.Equals(value)
        | (false, _) -> false

Closure1Table kapselt die zugrunde liegende mutationsbasierte Datenstruktur, sodass der Aufrufer nicht gezwungen ist, die zugrunde liegende Datenstruktur zu warten. Klassen sind eine leistungsstarke Möglichkeit, mutationsbasierte Daten und Routinen zu kapseln, ohne die Details für den Aufrufer verfügbar zu machen.

Bevorzugen von let mutable gegenüber ref

Verweiszellen sind eine Möglichkeit, den Verweis auf einen Wert und nicht den Wert selbst darzustellen. Obwohl sie für leistungskritischen Code verwendet werden können, werden sie nicht empfohlen. Betrachten Sie das folgenden Beispiel:

let kernels =
    let acc = ref Set.empty

    processWorkList startKernels (fun kernel ->
        if not ((!acc).Contains(kernel)) then
            acc := (!acc).Add(kernel)
        ...)

    !acc |> Seq.toList

Die Verwendung einer Verweiszelle „verunreinigt“ den gesamten nachfolgenden Code, wobei die zugrunde liegenden Daten dereferenziert und erneut referenziert werden müssen. Erwägen Sie stattdessen let mutable:

let kernels =
    let mutable acc = Set.empty

    processWorkList startKernels (fun kernel ->
        if not (acc.Contains(kernel)) then
            acc <- acc.Add(kernel)
        ...)

    acc |> Seq.toList

Abgesehen von dem einzelnen Mutationspunkt in der Mitte des Lambdaausdrucks kann jeder andere Code, der acc berührt, dies auf eine Weise tun, die sich nicht von der Verwendung eines normalen mit let gebundenen unveränderlichen Werts unterscheidet. Dadurch werden Änderungen mit der Zeit einfacher.

NULL- und Standardwerte

NULL-Werte sollten in F# generell vermieden werden. Standardmäßig unterstützen in F# deklarierte Typen die Verwendung des Literals null nicht, und alle Werte und Objekte werden initialisiert. Einige gängige .NET-APIs geben jedoch NULL-Werte zurück oder akzeptieren diese, und einige gängige in .NET deklarierte Typen wie Arrays und Zeichenfolgen lassen NULL-Werte zu. Das Vorkommen von null-Werten ist in der F#-Programmierung jedoch sehr selten, und einer der Vorteile von F# ist die Vermeidung von Fehlern durch NULL-Verweise in den meisten Fällen.

Vermeiden der Verwendung des Attributs AllowNullLiteral

Standardmäßig unterstützen in F# deklarierte Typen die Verwendung des Literals null nicht. Sie können F#-Typen manuell mit AllowNullLiteral kommentieren, um dies zuzulassen. Allerdings ist es fast immer besser, dies zu vermeiden.

Vermeiden der Verwendung des Attributs Unchecked.defaultof<_>

Es ist möglich, einen null- oder mit 0 initialisierten Wert für einen F#-Typ zu generieren, indem Sie Unchecked.defaultof<_> verwenden. Dies kann bei der Initialisierung von Speicherplatz für bestimmte Datenstrukturen, bei einem leistungsstarken Programmiermuster oder im Hinblick auf Interoperabilität nützlich sein. Die Verwendung dieses Konstrukts sollte jedoch vermieden werden.

Vermeiden der Verwendung des Attributs DefaultValue

Standardmäßig müssen F#-Datensätze und -Objekte beim Erstellen ordnungsgemäß initialisiert werden. Das Attribut DefaultValue kann verwendet werden, um einige Felder von Objekten mit einem null- oder mit 0 initialisierten Wert aufzufüllen. Dieses Konstrukt wird selten benötigt, und seine Verwendung sollte vermieden werden.

Bei der Prüfung auf NULL-Eingaben sollten Sie bei der ersten Gelegenheit eine Ausnahme auslösen

Wenn Sie neuen F#-Code schreiben, müssen Sie in der Praxis nicht auf NULL-Eingaben prüfen, es sei denn, Sie erwarten, dass dieser Code von C# oder anderen .NET-Sprachen verwendet wird.

Wenn Sie sich entschließen, Prüfungen auf NULL-Eingaben hinzuzufügen, führen Sie die Prüfungen bei der ersten Gelegenheit durch, und lösen Sie eine Ausnahme aus. Beispiel:

let inline checkNonNull argName arg =
    if isNull arg then
        nullArg argName

module Array =
    let contains value (array:'T[]) =
        checkNonNull "array" array
        let mutable result = false
        let mutable i = 0
        while not state && i < array.Length do
            result <- value = array[i]
            i <- i + 1
        result

Aus Legacygründen behandeln einige Zeichenfolgenfunktionen in FSharp.Core NULL-Werte nach wie vor als leere Zeichenfolgen und schlagen bei NULL-Argumenten nicht fehl. Nehmen Sie dies jedoch nicht als Richtschnur, und verwenden Sie keine Programmiermuster, die NULL eine beliebige semantische Bedeutung zuschreiben.

Objektprogrammierung

F# bietet vollständige Unterstützung für Objekte und objektorientierte Konzepte. Obwohl viele objektorientierte Konzepte leistungsstark und nützlich sind, lassen sich nicht alle von ihnen ideal verwenden. Die folgenden Listen bieten allgemeine Anleitungen zu Kategorien objektorientierter Features.

Erwägen Sie die Verwendung dieser Features in verschiedenen Situationen:

  • Punktnotation (x.Length)
  • Instanzmember
  • Implizite Konstruktoren
  • Statische Member
  • Indexernotation (arr[x]) durch Definieren einer Item-Eigenschaft
  • Slicenotation (arr[x..y], arr[x..], arr[..y]) durch Definieren von GetSlice-Membern
  • Benannte und optionale Argumente
  • Schnittstellen und Schnittstellenimplementierungen

Setzen Sie nicht von vornherein auf diese Features, sondern nutzen Sie sie mit Bedacht, wenn sie zur Lösung eines Problems geeignet sind:

  • Methodenüberladung
  • Gekapselte veränderliche Daten
  • Operatoren für Typen
  • Automatische Eigenschaften
  • Implementieren von IDisposable und IEnumerable
  • Typerweiterungen
  • Ereignisse
  • Strukturen
  • Delegaten
  • Enumerationen

Vermeiden Sie diese Features im Allgemeinen, es sei denn, Sie müssen sie verwenden:

  • Vererbungsbasierte Typhierarchien und Implementierungsvererbung
  • NULL-Werte und Unchecked.defaultof<_>

Bevorzugen von Komposition gegenüber Vererbung

Komposition vor Vererbung ist eine seit langem bestehende Praxis, an die sich guter F#-Code halten kann. Das Grundprinzip ist, dass Sie keine Basisklasse verfügbar machen und die Aufrufer zwingen sollten, von dieser Basisklasse zu erben, um die Funktionalität zu erhalten.

Verwenden von Objektausdrücken zum Implementieren von Schnittstellen, wenn Sie keine Klasse benötigen

Mit Objektausdrücken können Sie Schnittstellen im laufenden Betrieb implementieren und die implementierte Schnittstelle an einen Wert binden, ohne dies innerhalb einer Klasse tun zu müssen. Das ist vor allem dann praktisch, wenn Sie nur die Schnittstelle implementieren müssen und keine vollständige Klasse benötigen.

Hier ist zum Beispiel der Code, der in Ionide ausgeführt wird, um eine Codekorrekturaktion bereitzustellen, wenn Sie ein Symbol hinzugefügt haben, für das es keine open-Anweisung gibt:

    let private createProvider () =
        { new CodeActionProvider with
            member this.provideCodeActions(doc, range, context, ct) =
                let diagnostics = context.diagnostics
                let diagnostic = diagnostics |> Seq.tryFind (fun d -> d.message.Contains "Unused open statement")
                let res =
                    match diagnostic with
                    | None -> [||]
                    | Some d ->
                        let line = doc.lineAt d.range.start.line
                        let cmd = createEmpty<Command>
                        cmd.title <- "Remove unused open"
                        cmd.command <- "fsharp.unusedOpenFix"
                        cmd.arguments <- Some ([| doc |> unbox; line.range |> unbox; |] |> ResizeArray)
                        [|cmd |]
                res
                |> ResizeArray
                |> U2.Case1
        }

Da bei der Interaktion mit der Visual Studio Code-API keine Klasse erforderlich ist, sind Objektausdrücke ein ideales Tool dafür. Sie sind auch für Komponententests nützlich, wenn Sie eine Schnittstelle auf improvisierte Weise mit Testroutinen ausstatten möchten.

Erwägen von Typabkürzungen zum Verkürzen von Signaturen

Typabkürzungen sind eine praktische Möglichkeit, eine Bezeichnung einem anderen Typ zuzuweisen, z. B. einer Funktionssignatur oder einem komplexeren Typ. Der folgende Alias weist beispielsweise eine Bezeichnung zu, die für die Definition einer Berechnung mit CNTK, einer Deep Learning-Bibliothek, benötigt wird:

open CNTK

// DeviceDescriptor, Variable, and Function all come from CNTK
type Computation = DeviceDescriptor -> Variable -> Function

Der Name Computation ist eine praktische Möglichkeit, jede Funktion zu bezeichnen, die mit der Signatur übereinstimmt, die sie mit einem Alias versieht. Die Verwendung von Typabkürzungen wie dieser ist praktisch und ermöglicht einen prägnanteren Code.

Vermeiden der Verwendung von Typabkürzungen zur Darstellung Ihrer Domäne

Typabkürzungen sind zwar praktisch, um Funktionssignaturen einen Namen zu geben, können jedoch beim Abkürzen anderer Typen verwirrend sein. Betrachten Sie diese Abkürzung:

// Does not actually abstract integers.
type BufferSize = int

Diese kann auf mehrere Arten verwirrend sein:

  • BufferSize ist keine Abstraktion, sondern nur ein anderer Name für eine ganze Zahl.
  • Wenn BufferSize in einer öffentlichen API verfügbar gemacht wird, kann dies leicht dahingehend fehlinterpretiert werden, dass es mehr als nur int bedeutet. Im Allgemeinen verfügen Domänentypen über mehrere Attribute und sind keine primitiven Typen wie int. Diese Abkürzung verstößt gegen diese Annahme.
  • Die Schreibung von BufferSize (PascalCase) impliziert, dass dieser Typ mehr Daten enthält.
  • Dieser Alias bietet keine größere Klarheit im Vergleich zum Bereitstellen eines benannten Arguments für eine Funktion.
  • Die Abkürzung manifestiert sich nicht in kompilierter Zwischensprache. Sie ist bloß eine ganze Zahl, und dieser Alias ist ein Kompilierzeitkonstrukt.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Zusammenfassend lässt sich sagen, dass der Fallstrick bei Typabkürzungen darin besteht, dass sie nicht Abstraktionen der Typen sind, die sie abkürzen. Im vorigen Beispiel ist BufferSize eigentlich einfach nur ein int, ohne zusätzliche Daten und ohne weitere Vorteile des Typsystems als dem, was int bereits hat.

Ein alternativer Ansatz zur Verwendung von Typabkürzungen zur Darstellung einer Domäne ist die Verwendung von diskriminierten Unions für Einzelfälle. Das vorherige Beispiel kann wie folgt modelliert werden:

type BufferSize = BufferSize of int

Wenn Sie Code schreiben, der mit BufferSize und dem zugrunde liegenden Wert arbeitet, müssen Sie einen solchen konstruieren, anstatt eine beliebige ganze Zahl einzugeben:

module Networking =
    ...
    let send data (BufferSize size) =
    ...

Dadurch verringert sich die Wahrscheinlichkeit, dass versehentlich eine beliebige ganze Zahl an die Funktion send übergeben wird, da der Aufrufer erst den Typ BufferSize konstruieren muss, um einen Wert zu umschließen, ehe er die Funktion aufrufen kann.