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

113

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 composeFunktion die folgende Signatur:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

Aber natürlich will ich nicht FSharpFuncin der Signatur, was ich will Funcund Actionstattdessen so:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

Um dies zu erreichen, kann ich folgende compose2Funktion 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 compose2F #! Jetzt muss ich alle Standard-F # funsin Funcund Actionwie 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:

  1. Zwei separate Assemblys: eine für F # -Benutzer und die zweite für C # -Benutzer.
  2. 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:

  1. 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.
  2. 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 .

  1. 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 barund zooohne es mit prefixing Foo.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Curry-Funktionen

    • F #: Curry-Funktionen sind für F # idiomatisch

    • C #: Verwenden Sie in Vanilla .NET-APIs kein Currying von Parametern.

  7. Auf Nullwerte prüfen

    • F #: Dies ist nicht idiomatisch für F #

    • C #: Überprüfen Sie die Grenzen der Vanilla .NET-API auf Nullwerte.

  8. Die Verwendung von F # -Typen list, map, setusw.

    • 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 )

  9. 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 #

Philip P.
quelle
Was sind Ihre Anliegen, außer erstklassige Funktionen weiterzugeben? F # trägt bereits wesentlich dazu bei, ein nahtloses Erlebnis aus anderen Sprachen zu ermöglichen.
Daniel
Betreff: Ihre Bearbeitung, ich bin immer noch unklar, warum Sie denken, dass eine in F # verfasste Bibliothek von C # nicht standardisiert erscheint. Zum größten Teil bietet F # eine Obermenge der C # -Funktionalität. Es ist meistens eine Frage der Konvention, dass sich eine Bibliothek natürlich anfühlt. Dies gilt unabhängig davon, welche Sprache Sie verwenden. Alles in allem bietet F # alle Tools, die Sie dazu benötigen.
Daniel
Ehrlich gesagt, obwohl Sie ein Codebeispiel hinzufügen, stellen Sie eine ziemlich offene Frage. Sie fragen nach "der allgemeinen Erfahrung eines C # -Benutzers mit einer F # -Bibliothek", aber das einzige Beispiel, das Sie geben, ist etwas, das in keiner zwingenden Sprache wirklich gut unterstützt wird. Die Behandlung von Funktionen als Bürger erster Klasse ist ein ziemlich zentraler Grundsatz der funktionalen Programmierung - der den meisten zwingenden Sprachen nur schlecht zugeordnet ist.
Onorio Catenacci
2
@KomradeP.: In Bezug auf EDIT 2 bietet FSharpx einige Mittel, um die Interoperabilität scheinbarer zu machen. In diesem ausgezeichneten Blog-Beitrag finden Sie einige Hinweise .
Pad

Antworten:

40

Daniel hat bereits erklärt, wie man eine C # -freundliche Version der von Ihnen geschriebenen F # -Funktion definiert, daher werde ich einige übergeordnete Kommentare hinzufügen. Lesen Sie zunächst die Richtlinien für das Design von F # -Komponenten (auf die bereits von gradbot verwiesen wird). In diesem Dokument wird erläutert, wie Sie F # - und .NET-Bibliotheken mit F # entwerfen. Es sollte viele Ihrer Fragen beantworten.

Bei Verwendung von F # gibt es grundsätzlich zwei Arten von Bibliotheken, die Sie schreiben können:

  • Die F # -Bibliothek kann nur von F # aus verwendet werden, daher ist die öffentliche Schnittstelle in einem funktionalen Stil geschrieben (unter Verwendung von F # -Funktionstypen, Tupeln, diskriminierten Gewerkschaften usw.).

  • Die .NET-Bibliothek kann in jeder .NET-Sprache (einschließlich C # und F #) verwendet werden und folgt normalerweise dem objektorientierten .NET-Stil. Dies bedeutet, dass Sie den größten Teil der Funktionalität als Klassen mit Methode verfügbar machen (und manchmal Erweiterungsmethoden oder statische Methoden, aber meistens sollte der Code im OO-Design geschrieben sein).

In Ihrer Frage fragen Sie, wie Sie die Funktionszusammensetzung als .NET-Bibliothek verfügbar machen können, aber ich denke, dass Funktionen wie Ihre composeaus Sicht der .NET-Bibliothek zu Konzepte auf niedriger Ebene sind. Sie können sie als Methoden verfügbar machen, die mit Funcund arbeitenAction , aber auf diese Weise würden Sie wahrscheinlich überhaupt keine normale .NET-Bibliothek entwerfen (möglicherweise würden Sie stattdessen das Builder-Muster oder ähnliches verwenden).

In einigen Fällen (dh beim Entwerfen numerischer Bibliotheken, die nicht wirklich gut zum .NET-Bibliotheksstil passen) ist es sinnvoll, eine Bibliothek zu entwerfen, die sowohl F # - als auch .NET- Stile in einer einzigen Bibliothek mischt . Der beste Weg, dies zu tun, besteht darin, eine normale F # (oder normale .NET) API zu haben und dann Wrapper für die natürliche Verwendung im anderen Stil bereitzustellen. Die Wrapper können sich in einem separaten Namespace befinden (wie MyLibrary.FSharpund MyLibrary).

In Ihrem Beispiel können Sie die F # MyLibrary.FSharp-Implementierung in belassen und dann .NET (C # -freundliche) Wrapper (ähnlich dem von Daniel veröffentlichten Code) MyLibraryals statische Methode einer Klasse in den Namespace einfügen . Aber auch hier hätte die .NET-Bibliothek wahrscheinlich eine spezifischere API als die Funktionszusammensetzung.

Tomas Petricek
quelle
1
Die F # Component Design Guidelines werden jetzt hier
Jan Schiefer
19

Sie haben nur die Funktion wrap Werte (teilweise aufgebrachte Funktionen, usw.) mit Funcoder Action, der Rest automatisch umgewandelt. Beispielsweise:

type A(arg) =
  member x.Invoke(f: Func<_,_>) = f.Invoke(arg)

let a = A(1)
a.Invoke(fun i -> i + 1)

Es ist also sinnvoll, Func/ Actionwo zutreffend zu verwenden. Beseitigt dies Ihre Bedenken? Ich denke, Ihre vorgeschlagenen Lösungen sind zu kompliziert. Sie können Ihre gesamte Bibliothek in F # schreiben und sie schmerzfrei von F # und C # aus verwenden (ich mache es die ganze Zeit).

Außerdem ist F # in Bezug auf die Interoperabilität flexibler als C #. Daher ist es im Allgemeinen am besten, dem traditionellen .NET-Stil zu folgen, wenn dies ein Problem darstellt.

BEARBEITEN

Die Arbeit, die erforderlich ist, um zwei öffentliche Schnittstellen in separaten Namespaces zu erstellen, ist meines Erachtens nur dann gerechtfertigt, wenn sie sich ergänzen oder die F # -Funktionalität nicht über C # verwendet werden kann (z. B. Inline-Funktionen, die von F # -spezifischen Metadaten abhängen).

Nehmen Sie Ihre Punkte der Reihe nach:

  1. Modul + letBindungen und konstruktorlose statische + Elemente werden in C # genauso angezeigt. Verwenden Sie daher Module, wenn Sie können. Sie können verwenden CompiledNameAttribute, um Mitgliedern C # -freundliche Namen zu geben.

  2. Ich kann mich irren, aber ich vermute, dass die Komponentenrichtlinien geschrieben wurden, bevor System.Tuplesie dem Framework hinzugefügt wurden. (In früheren Versionen hat F # seinen eigenen Tupeltyp definiert.) Seitdem ist die Verwendung Tuplein einer öffentlichen Schnittstelle für triviale Typen akzeptabler geworden .

  3. Hier haben Sie wohl die Dinge auf C # Weise gemacht, weil F # gut spielt, Taskaber C # nicht gut spielt Async. Sie können async intern verwenden und dann aufrufen, Async.StartAsTaskbevor Sie von einer öffentlichen Methode zurückkehren.

  4. Die Umarmung von ist nullmöglicherweise der größte Nachteil bei der Entwicklung einer API zur Verwendung aus C #. In der Vergangenheit habe ich alle Arten von Tricks ausprobiert, um zu vermeiden, dass Null im internen F # -Code berücksichtigt wird. Am Ende war es jedoch am besten, Typen mit öffentlichen Konstruktoren mit [<AllowNullLiteral>]Null zu markieren und Argumente auf Null zu überprüfen. In dieser Hinsicht ist es nicht schlimmer als C #.

  5. Diskriminierte Gewerkschaften werden im Allgemeinen zu Klassenhierarchien zusammengestellt, haben jedoch immer eine relativ freundliche Darstellung in C #. Ich würde sagen, markiere sie mit [<AllowNullLiteral>]und benutze sie.

  6. Curried Funktionen erzeugen Funktionswerte , die nicht verwendet werden sollten.

  7. Ich fand es besser, null zu akzeptieren, als sich darauf zu verlassen, dass es an der öffentlichen Schnittstelle abgefangen wird, und es intern zu ignorieren. YMMV.

  8. Es macht sehr viel Sinn zu verwenden list/ map/ setintern. Sie können alle über die öffentliche Schnittstelle als verfügbar gemacht werden IEnumerable<_>. Außerdem seq, dictund Seq.readonlysind häufig nützlich.

  9. Siehe # 6.

Welche Strategie Sie verfolgen, hängt von der Art und Größe Ihrer Bibliothek ab. Meiner Erfahrung nach war es jedoch auf lange Sicht weniger aufwendig, den Sweet Spot zwischen F # und C # zu finden, als separate APIs zu erstellen.

Daniel
quelle
Ja, ich kann sehen, dass es einige Möglichkeiten gibt, Dinge zum Laufen zu bringen. Ich bin nicht ganz überzeugt. Zu Modulen. Wenn Sie module Foo.Bar.Zoodann in C # haben, wird dies class Fooim Namespace sein Foo.Bar. Wenn Sie jedoch verschachtelte Module wie haben module Foo=... module Bar=... module Zoo==, wird dadurch eine Klasse Foomit inneren Klassen Barund generiert Zoo. Re IEnumerable, kann dies nicht akzeptabel sein , wenn wir brauchen , um die Arbeit mit Karten und Sets wirklich. Zu den nullWerten und AllowNullLiteral- ein Grund, warum ich F # mag, ist, dass ich nullin C # müde bin und wirklich nicht alles wieder damit verschmutzen möchte!
Philip P.
Bevorzugen Sie im Allgemeinen Namespaces gegenüber verschachtelten Modulen für Interop. Betreff: null, ich stimme Ihnen zu, es ist sicherlich eine Regression, dies berücksichtigen zu müssen, aber etwas, mit dem Sie sich befassen müssen, wenn Ihre API von C # aus verwendet wird.
Daniel
2

Obwohl es wahrscheinlich ein Overkill wäre, könnten Sie erwägen, eine Anwendung mit Mono.Cecil zu schreiben (es hat eine großartige Unterstützung auf der Mailingliste), die die Konvertierung auf IL-Ebene automatisieren würde. Wenn Sie beispielsweise Ihre Assembly in F # mithilfe der öffentlichen API im F # -Stil implementieren, generiert das Tool einen C # -freundlichen Wrapper darüber.

Zum Beispiel würden Sie in F # offensichtlich option<'T>( Nonespeziell) verwenden, anstatt nullwie in C # zu verwenden. Das Schreiben eines Wrapper-Generators für dieses Szenario sollte ziemlich einfach sein: Die Wrapper-Methode würde die ursprüngliche Methode aufrufen: Wenn der Rückgabewert war Some x, dann return x, andernfalls return null.

Sie müssten den Fall behandeln, wenn Tes sich um einen Werttyp handelt, dh nicht nullbar. Sie müssten den Rückgabewert der Wrapper-Methode einschließen Nullable<T>, was sie etwas schmerzhaft macht.

Auch hier bin ich mir ziemlich sicher, dass es sich auszahlen würde, ein solches Tool in Ihrem Szenario zu schreiben, vielleicht außer wenn Sie regelmäßig an dieser Bibliothek arbeiten (die nahtlos von F # und C # aus verwendet werden kann). Auf jeden Fall denke ich, dass es ein interessantes Experiment wäre, das ich vielleicht sogar irgendwann erforschen werde.

ShdNx
quelle
klingt interessant. Würde die Verwendung Mono.Cecilbedeuten, im Grunde genommen ein Programm zu schreiben, um eine F # -Baugruppe zu laden, Elemente zu finden, die zugeordnet werden müssen, und eine andere Baugruppe mit den C # -Wrappern auszugeben? Habe ich das richtig verstanden?
Philip P.
@Komrade P.: Sie können das auch tun, aber das ist viel komplizierter, weil Sie dann alle Referenzen anpassen müssten, um auf die Mitglieder der neuen Assembly zu verweisen. Es ist viel einfacher, wenn Sie nur die neuen Elemente (die Wrapper) zur vorhandenen F # -geschriebenen Assembly hinzufügen.
ShdNx
2

Entwurf von Richtlinien für das Design von F # -Komponenten (August 2010)

Übersicht In diesem Dokument werden einige Probleme im Zusammenhang mit dem Design und der Codierung von F # -Komponenten behandelt. Insbesondere umfasst es:

  • Richtlinien zum Entwerfen von Vanille-.NET-Bibliotheken zur Verwendung in einer beliebigen .NET-Sprache.
  • Richtlinien für F # -to-F # -Bibliotheken und F # -Implementierungscode.
  • Vorschläge zu Codierungskonventionen für F # -Implementierungscode
gradbot
quelle