Bester Ansatz zum Entwerfen von F#-Bibliotheken zur Verwendung in F# und C#C#

Ein Treffpunkt für C#-Programmierer
Anonymous
 Bester Ansatz zum Entwerfen von F#-Bibliotheken zur Verwendung in F# und C#

Post by Anonymous »

Ich versuche, eine Bibliothek in F# zu entwerfen. Die Bibliothek sollte sowohl für die Verwendung mit F# als auch mit C# geeignet sein.
Und hier stecke ich ein wenig fest. Ich kann es F#-freundlich oder C#-freundlich machen, aber das Problem besteht darin, es für beide freundlich zu machen.
Hier ist ein Beispiel. Stellen Sie sich vor, ich habe die folgende Funktion in F#:

Code: Select all

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Dies ist perfekt aus F# verwendbar:

Code: Select all

let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
In C# hat die Compose-Funktion die folgende Signatur:

Code: Select all

FSharpFunc compose(FSharpFunc f, FSharpFunc a);
Aber natürlich möchte ich nicht FSharpFunc in der Signatur haben, sondern stattdessen Func und Action, etwa so:

Code: Select all

Action compose2(Func f, Action a);
Um dies zu erreichen, kann ich die Funktion „compose2“ wie folgt erstellen:

Code: Select all

let compose2 (f: Func) (a : Action(f.Invoke >> a.Invoke)
Das ist jetzt perfekt in C# verwendbar:

Code: Select all

void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Aber jetzt haben wir ein Problem bei der Verwendung von compose2 aus F#! Jetzt muss ich alle Standard-F#-Funs in Func und Action einpacken, etwa so:

Code: Select all

let useCompose2InFsharp() =
let f = Func(fun item -> item.ToString())
let a = Action(fun item -> printfn "%A" item)
let composite2 = compose2 f a

composite2.Invoke "foo"
composite2.Invoke "bar"
Die Frage: Wie können wir eine erstklassige Erfahrung mit der in F# geschriebenen Bibliothek sowohl für F#- als auch für C#-Benutzer erzielen?
Bisher konnte ich mir nichts Besseres als diese beiden Ansätze vorstellen:
  • Zwei separate Assemblys: eine für F#-Benutzer und die zweite für C#-Benutzer.
  • Eine Assembly, aber anders Namespaces: einer für F#-Benutzer und der zweite für C#-Benutzer.
Für den ersten Ansatz würde ich etwa so vorgehen:
  • Erstellen Sie ein F#-Projekt, nennen Sie es FooBarFs und kompilieren Sie es in FooBarFs.dll.
    • Zielen Sie die Bibliothek ausschließlich auf F# aus. Benutzer.
    • Alles Unnötige aus den .fsi-Dateien ausblenden.
  • Erstellen Sie ein weiteres F#-Projekt, rufen Sie FooBarCs auf und kompilieren Sie es in FooFar.dll
    • Verwenden Sie das erste F#-Projekt auf Quellebene wieder.
    • Erstellen Sie eine .fsi-Datei, die alles davor verbirgt Projekt.
    • Erstellen Sie eine .fsi-Datei, die die Bibliothek auf C#-Art verfügbar macht, und verwenden Sie C#-Redewendungen für Namen, Namespaces usw.
    • Erstellen Sie Wrapper, die an die Kernbibliothek delegieren, und führen Sie bei Bedarf die Konvertierung durch.
Ich denke, der zweite Ansatz mit den Namespaces kann für die Benutzer verwirrend sein, aber dann Sie haben eine Assembly.
Die Frage: Nichts davon ist ideal, vielleicht fehlt mir eine Art Compiler-Flag/Schalter/Attribut
oder eine Art Trick und es gibt einen besseren Weg, dies zu tun?
Die Frage: Hat jemand anderes versucht, etwas Ähnliches zu erreichen, und wenn ja, wie haben Sie das gemacht?
BEARBEITEN: Zur Verdeutlichung geht es bei der Frage nicht nur um Funktionen und Delegiert, sondern die Gesamterfahrung eines C#-Benutzers mit einer F#-Bibliothek. Dazu gehören Namespaces, Namenskonventionen, Redewendungen und dergleichen, die in C# typisch sind. Grundsätzlich sollte ein C#-Benutzer nicht erkennen können, dass die Bibliothek in F# erstellt wurde. Und umgekehrt sollte ein F#-Benutzer das Gefühl haben, sich mit einer C#-Bibliothek zu befassen.

EDIT 2:
Ich kann den bisherigen Antworten und Kommentaren entnehmen, dass meiner Frage die nötige Tiefe fehlt,
vielleicht hauptsächlich aufgrund der Verwendung nur eines Beispiels, bei dem Interoperabilitätsprobleme zwischen F# und C#
auftauchen, nämlich der Frage der Funktionswerte. Ich denke, dass dies das offensichtlichste Beispiel ist, und das hat mich dazu veranlasst, es zum Stellen der Frage zu verwenden, hat aber gleichzeitig auch den Eindruck erweckt, dass dies das einzige Problem ist, das mich beschäftigt.
Lassen Sie mich konkretere Beispiele nennen. Ich habe das hervorragendste
F# Component Design Guidelines
Dokument gelesen (vielen Dank @gradbot dafür!). Die Richtlinien im Dokument, sofern sie verwendet werden, behandeln einige der Probleme, aber nicht alle.
Das Dokument ist in zwei Hauptteile unterteilt: 1) Richtlinien für die Ausrichtung auf F#-Benutzer; und 2) Richtlinien für die
Ansprache von C#-Benutzern. Nirgendwo wird auch nur versucht, so zu tun, als sei ein einheitlicher
Ansatz möglich, der genau meine Frage widerspiegelt: Wir können auf F# abzielen, wir können auf C# abzielen, aber was ist die
praktische Lösung, um auf beide abzuzielen?
Zur Erinnerung: Das Ziel besteht darin, eine in F# verfasste Bibliothek zu haben, die idiomatisch sowohl in F#- als auch in C#-Sprachen verwendet werden kann.
Das Schlüsselwort hier ist idiomatisch. Das Problem ist nicht die allgemeine Interoperabilität, bei der es einfach
möglich ist, Bibliotheken in verschiedenen Sprachen zu verwenden.
Nun zu den Beispielen, die ich direkt den
F# Component Design Guidelines entnehme.
  • Module+Funktionen (F#) vs. Namespaces+Typen+Funktionen
    • F#: Verwenden Sie Namespaces oder Module, um Ihre zu enthalten Typen und Module.
      Die idiomatische Verwendung besteht darin, Funktionen in Modulen zu platzieren, z. B.:

      Code: Select all

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      // Use from F#
      open Foo
      bar()
      zoo()
      
    • C#: Verwenden Sie Namespaces, Typen und Mitglieder als primäre Organisationsstruktur für Ihre
      Komponenten (im Gegensatz zu Modulen) für Vanilla .NET-APIs.
      Dies ist nicht mit der F#-Richtlinie kompatibel und das Beispiel müsste
      umgeschrieben werden, um es an die C#-Benutzer anzupassen:

      Code: Select all

      []
      type Foo =
      static member bar() = ...
      static member zoo() = ...
      
      Auf diese Weise unterbrechen wir jedoch die idiomatische Verwendung von F#, da
      wir bar und zoo nicht mehr verwenden können, ohne ihnen Foo voranzustellen.
  • Verwendung von Tupeln
    • F#: Verwenden Sie Tupel, wenn dies für die Rückgabe angemessen ist Werte.
    • C#: Vermeiden Sie die Verwendung von Tupeln als Rückgabewerte in Vanilla .NET-APIs.
  • Async
    • F#: Verwenden Sie Async für asynchrone Programmierung an F#-API-Grenzen.
    • C#: Machen Sie asynchrone Vorgänge entweder mit dem asynchronen .NET-Programmiermodell
      (BeginFoo, EndFoo) oder als Methoden, die .NET-Aufgaben zurückgeben (Task), verfügbar, statt als F#-Async
      Objekte.
  • Verwendung von Optionen
    • F#: Erwägen Sie die Verwendung von Optionswerten für die Rückgabe Typen statt Ausnahmen auszulösen (für F#-bezogenen Code).
    • Erwägen Sie die Verwendung des TryGetValue-Musters, anstatt F#-Optionswerte (Option) in Vanilla
      .NET-APIs zurückzugeben, und bevorzugen Sie Methodenüberladung gegenüber der Verwendung von F#-Optionswerten als Argumente.
  • Diskriminierte Unions
    • F#: Verwenden Sie diskriminierte Unions als Alternative zu Klassenhierarchien zum Erstellen baumstrukturierter Daten
    • C#: Keine spezifischen Richtlinien dafür, aber das Konzept der diskriminierten Unions ist C# fremd
  • Curried Funktionen
    • F#: Curry-Funktionen sind idiomatisch für F#
    • C#: Verwenden Sie kein Currying von Parametern in Vanilla .NET-APIs.
  • Prüfung auf Nullwerte
    • F#: Dies ist nicht idiomatisch für F#
    • C#: Erwägen Sie die Überprüfung auf Nullwerte an Vanilla .NET API-Grenzen.
  • Verwendung der F#-Typen list, map, set usw.
    • F#: es ist idiomatisch, diese in F# zu verwenden
    • C#: Erwägen Sie die Verwendung der .NET-Sammlungsschnittstellentypen IEnumerable und IDictionary
      für Parameter und Rückgabewerte in Vanilla .NET-APIs. (d. h. keine F#-Liste, -Map, -Set verwenden)
  • Funktionstypen (der offensichtliche)
    • F#: Die Verwendung von F#-Funktionen als Werte ist für F# offensichtlich idiomatisch
    • C#: Verwenden Sie .NET-Delegattypen anstelle von F#-Funktionstypen in Vanilla-.NET-APIs.
Ich denke, diese sollten ausreichen, um die Art meiner Frage zu veranschaulichen.
Übrigens haben die Richtlinien auch eine teilweise Antwort:

... eine gemeinsame Implementierungsstrategie, wenn Die Entwicklung höherwertiger
Methoden für Vanilla .NET-Bibliotheken besteht darin, die gesamte Implementierung mithilfe von F#-Funktionstypen zu erstellen und
dann die öffentliche API mithilfe von Delegaten als dünne Fassade über der eigentlichen F#-Implementierung zu erstellen.

Um es zusammenzufassen.
Es gibt eine eindeutige Antwort: Es gibt keine Compiler-Tricks, die ich habe verpasst.
Gemäß dem Richtliniendokument scheint es eine vernünftige Strategie zu sein, zuerst für F# zu schreiben und dann
einen Fassaden-Wrapper für .NET zu erstellen.
Die Frage bleibt dann bezüglich der praktischen Umsetzung davon:
  • Separate Assemblys? oder
  • Verschiedene Namensräume?

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post