Ich versuche eine Bibliothek in F # zu entwerfen. Die Bibliothek sollte sowohl für F # als auch für C # geeignet sein .
Und hier stecke ich ein bisschen fest. Ich kann es F # freundlich machen, oder ich kann es C # freundlich machen, aber das Problem ist, wie man es für beide freundlich macht.
Hier ist ein Beispiel. Stellen Sie sich vor, ich habe die folgende Funktion in F #:
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Dies ist perfekt verwendbar von F #:
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:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Aber natürlich will ich nicht FSharpFunc
in der Signatur, was ich will Func
und Action
stattdessen so:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Um dies zu erreichen, kann ich folgende compose2
Funktion erstellen :
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Dies ist in C # perfekt verwendbar:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Aber jetzt haben wir ein Problem mit compose2
F #! Jetzt muss ich alle Standard-F # funs
in Func
und Action
wie folgt einwickeln :
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 erstklassige Erfahrungen mit der in F # geschriebenen Bibliothek für F # - und C # -Benutzer erzielen?
Bisher konnte ich mir nichts Besseres einfallen lassen als diese beiden Ansätze:
- Zwei separate Assemblys: eine für F # -Benutzer und die zweite für C # -Benutzer.
- Eine Assembly, aber unterschiedliche Namespaces: einer für F # -Benutzer und der zweite für C # -Benutzer.
Für den ersten Ansatz würde ich so etwas tun:
Erstellen Sie ein F # -Projekt, nennen Sie es FooBarFs und kompilieren Sie es in FooBarFs.dll.
- Richten Sie die Bibliothek ausschließlich an F # -Nutzer.
- Verstecken Sie alles Unnötige aus den .fsi-Dateien.
Erstellen Sie ein weiteres F # -Projekt, rufen Sie if FooBarCs auf und kompilieren Sie es in FooFar.dll
- Verwenden Sie das erste F # -Projekt auf Quellenebene erneut.
- Erstellen Sie eine .fsi-Datei, die alles vor diesem Projekt verbirgt.
- Erstellen Sie eine .fsi-Datei, die die Bibliothek auf C # -Weise verfügbar macht, und verwenden Sie C # -Sprachen für Namen, Namespaces usw.
- Erstellen Sie Wrapper, die an die Kernbibliothek delegieren, und führen Sie die Konvertierung bei Bedarf durch.
Ich denke, der zweite Ansatz mit den Namespaces kann für die Benutzer verwirrend sein, aber dann haben Sie eine Assembly.
Die Frage: Keines 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 es gemacht?
BEARBEITEN: Zur Verdeutlichung geht es nicht nur um Funktionen und Delegaten, sondern auch um die allgemeine Erfahrung eines C # -Benutzers mit einer F # -Bibliothek. Dies umfasst Namespaces, Namenskonventionen, Redewendungen und dergleichen, die in C # vorkommen. Grundsätzlich sollte ein C # -Benutzer nicht erkennen können, dass die Bibliothek in F # erstellt wurde. Und umgekehrt sollte ein F # -Benutzer Lust haben, sich mit einer C # -Bibliothek zu befassen.
EDIT 2:
Aus den bisherigen Antworten und Kommentaren kann ich ersehen, dass meiner Frage die erforderliche Tiefe fehlt, möglicherweise hauptsächlich aufgrund der Verwendung nur eines Beispiels, bei dem Interoperabilitätsprobleme zwischen F # und C # auftreten, nämlich der Frage der Funktionswerte. Ich denke, dies ist das offensichtlichste Beispiel, und deshalb habe ich es verwendet, um die Frage zu stellen, aber aus dem gleichen Grund hatte ich den Eindruck, dass dies das einzige Problem ist, mit dem ich mich befasse.
Lassen Sie mich konkretere Beispiele geben. Ich habe das hervorragendste Dokument mit den Richtlinien für das Design von F # -Komponenten gelesen (vielen Dank an @gradbot dafür!). Die Richtlinien im Dokument behandeln, falls verwendet, einige der Probleme, jedoch nicht alle.
Das Dokument ist in zwei Hauptteile unterteilt: 1) Richtlinien für die Ausrichtung auf F # -Nutzer; und 2) Richtlinien für die Ausrichtung auf C # -Nutzer. Nirgendwo wird überhaupt versucht vorzutäuschen, dass es möglich ist, einen einheitlichen Ansatz zu verfolgen, der genau meine Frage widerspiegelt: Wir können auf F # zielen, wir können auf C # zielen, aber was ist die praktische Lösung, um auf beide zu zielen?
Zur Erinnerung, das Ziel ist es, eine Bibliothek in F # zu erstellen, die idiomatisch verwendet werden kann sowohl in F # als auch in C # .
Das Schlüsselwort hier ist idiomatisch . Das Problem ist nicht die allgemeine Interoperabilität, bei der es nur möglich ist, Bibliotheken in verschiedenen Sprachen zu verwenden.
Nun zu den Beispielen, die ich direkt aus den F # Component Design Guidelines entnehme .
Module + Funktionen (F #) vs Namespaces + Typen + Funktionen
F #: Verwenden Sie Namespaces oder Module, um Ihre Typen und Module zu enthalten. Die idiomatische Verwendung besteht darin, Funktionen in Modulen zu platzieren, z.
// 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 neu geschrieben werden, um den C # -Benutzern zu entsprechen:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
Dadurch aber so, brechen wir die idiomatische Verwendung von F # , da können wir nicht mehr verwenden
bar
undzoo
ohne es mit prefixingFoo
.
Verwendung von Tupeln
F #: Verwenden Sie gegebenenfalls Tupel für Rückgabewerte.
C #: Vermeiden Sie die Verwendung von Tupeln als Rückgabewerte in Vanilla .NET-APIs.
Async
F #: Verwenden Sie Async für die asynchrone Programmierung an den Grenzen der F # -API.
C #: Stellen Sie asynchrone Vorgänge entweder mit dem asynchronen .NET-Programmiermodell (BeginFoo, EndFoo) oder als Methoden zur Rückgabe von .NET-Aufgaben (Task) und nicht als asynchrone F # -Objekte bereit.
Gebrauch von
Option
F #: Erwägen Sie die Verwendung von Optionswerten für Rückgabetypen, anstatt Ausnahmen auszulösen (für F # -Gesichtscode).
Verwenden Sie das TryGetValue-Muster, anstatt F # -Optionswerte (Option) in Vanilla .NET-APIs zurückzugeben, und ziehen Sie das Überladen von Methoden der Verwendung von F # -Optionswerten als Argumente vor.
Diskriminierte Gewerkschaften
F #: Verwenden Sie diskriminierte Gewerkschaften als Alternative zu Klassenhierarchien zum Erstellen von Daten mit Baumstruktur
C #: Keine spezifischen Richtlinien dafür, aber das Konzept diskriminierter Gewerkschaften ist C # fremd.
Curry-Funktionen
F #: Curry-Funktionen sind für F # idiomatisch
C #: Verwenden Sie in Vanilla .NET-APIs kein Currying von Parametern.
Auf Nullwerte prüfen
F #: Dies ist nicht idiomatisch für F #
C #: Überprüfen Sie die Grenzen der Vanilla .NET-API auf Nullwerte.
Die Verwendung von F # -Typen
list
,map
,set
usw.F #: Es ist idiomatisch, diese in F # zu verwenden
C #: Erwägen Sie die Verwendung der .NET-Erfassungsschnittstellentypen IEnumerable und IDictionary für Parameter und Rückgabewerte in Vanilla .NET-APIs. ( Dh verwenden F # nicht
list
,map
,set
)
Funktionstypen (der offensichtliche)
F #: Die Verwendung von F # -Funktionen als Werte ist für F # offensichtlich idiomatisch
C #: Verwenden Sie .NET-Delegatentypen gegenüber F # -Funktionstypen in Vanilla .NET-APIs.
Ich denke, diese sollten ausreichen, um die Art meiner Frage zu demonstrieren.
Übrigens haben die Richtlinien auch eine teilweise Antwort:
... Eine gängige Implementierungsstrategie bei der Entwicklung von Methoden höherer Ordnung für Vanilla .NET-Bibliotheken besteht darin, die gesamte Implementierung mithilfe von F # -Funktionstypen zu erstellen und anschließend die öffentliche API mithilfe von Delegaten als dünne Fassade auf der eigentlichen F # -Implementierung zu erstellen.
Zusammenfassen.
Es gibt eine eindeutige Antwort: Es gibt keine Compiler-Tricks, die ich verpasst habe .
Gemäß dem Richtlinien-Dokument scheint es eine vernünftige Strategie zu sein, zuerst für F # zu erstellen und dann einen Fassaden-Wrapper für .NET zu erstellen.
Es bleibt dann die Frage nach der praktischen Umsetzung:
Getrennte Baugruppen? oder
Unterschiedliche Namespaces?
Wenn meine Interpretation korrekt ist, schlägt Tomas vor, dass die Verwendung separater Namespaces ausreichend und eine akzeptable Lösung sein sollte.
Ich denke, ich werde dem zustimmen, da die Auswahl der Namespaces so ist, dass die .NET / C # -Benutzer nicht überrascht oder verwirrt werden, was bedeutet, dass der Namespace für sie wahrscheinlich so aussehen sollte, als wäre er der primäre Namespace für sie. Die F # -Nutzer müssen die Last auf sich nehmen, einen F # -spezifischen Namespace zu wählen. Beispielsweise:
FSharp.Foo.Bar -> Namespace für F # gegenüber der Bibliothek
Foo.Bar -> Namespace für .NET-Wrapper, idiomatisch für C #