116 votes

Meilleure approche pour concevoir des bibliothèques F# utilisables à la fois en F# et en C#

J'essaie de concevoir une bibliothèque en F#. La bibliothèque doit être facile à utiliser à partir de à la fois F# et C# .

Et c'est là que je suis un peu coincé. Je peux le rendre convivial pour F#, ou je peux le rendre convivial pour C#, mais le problème est de savoir comment le rendre convivial pour les deux.

Voici un exemple. Imaginons que j'ai la fonction suivante en F# :

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a

Ceci est parfaitement utilisable à partir de F# :

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"

En C#, le compose a la signature suivante :

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

Mais bien sûr, je ne veux pas FSharpFunc dans la signature, ce que je veux c'est Func y Action à la place, comme ceci :

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

Pour cela, je peux créer compose2 comme ceci :

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)

Maintenant, ceci est parfaitement utilisable en C# :

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}

Mais maintenant nous avons un problème en utilisant compose2 de F# ! Maintenant, je dois envelopper tous les standards de F#. funs en Func y Action comme ceci :

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"

La question : Comment obtenir une expérience de première classe pour la bibliothèque écrite en F# pour les utilisateurs de F# et de C# ?

Jusqu'à présent, je n'ai rien trouvé de mieux que ces deux approches :

  1. Deux assemblages distincts : l'un destiné aux utilisateurs de F#, le second aux utilisateurs de C#.
  2. Un seul assemblage mais des espaces de noms différents : un pour les utilisateurs de F#, et le second pour les utilisateurs de C#.

Pour la première approche, je ferais quelque chose comme ceci :

  1. Créez un projet F#, appelez-le FooBarFs et compilez-le dans FooBarFs.dll.

    • Cibler la bibliothèque uniquement sur les utilisateurs de F#.
    • Cachez tout ce qui n'est pas nécessaire dans les fichiers .fsi.
  2. Créez un autre projet F#, appelez-le FooBarCs et compilez-le dans FooFar.dll.

    • Réutiliser le premier projet F# au niveau de la source.
    • Créer un fichier .fsi qui cache tout de ce projet.
    • Créer un fichier .fsi qui expose la bibliothèque en C#, en utilisant les idiomes C# pour les noms, les espaces de noms, etc.
    • Créer des enveloppes qui délèguent à la bibliothèque centrale, en effectuant la conversion si nécessaire.

Je pense que la deuxième approche avec les espaces de noms peut être déroutante pour les utilisateurs, mais vous avez alors une seule assemblée.

La question : Aucune d'entre elles n'est idéale, peut-être que je manque une sorte de drapeau/interrupteur/attribut du compilateur. ou une sorte d'astuce et qu'il existe une meilleure façon de procéder ?

La question : Est-ce que quelqu'un d'autre a essayé de réaliser quelque chose de similaire et si oui, comment l'avez-vous fait ?

EDIT : pour clarifier, la question ne concerne pas seulement les fonctions et les délégués mais l'expérience globale d'un utilisateur C# avec une bibliothèque F#. Cela inclut les espaces de noms, les conventions de nommage, les idiomes et autres qui sont natifs de C#. En gros, un utilisateur de C# ne devrait pas être capable de détecter que la bibliothèque a été créée en F#. Et vice versa, un utilisateur de F# devrait avoir l'impression de traiter avec une bibliothèque C#.


EDIT 2 :

Je peux voir, à partir des réponses et des commentaires jusqu'à présent, que ma question n'a pas la profondeur nécessaire, peut-être surtout en raison de l'utilisation d'un seul exemple où des problèmes d'interopérabilité entre F# et C# la question des valeurs de fonction. Je pense qu'il s'agit de l'exemple le plus évident, ce qui m'a conduit à l'utiliser pour poser la question. m'a conduit à l'utiliser pour poser la question, mais par la même occasion a donné l'impression que c'est le seul problème qui me concerne. l'impression que c'est le seul problème qui me préoccupe.

Permettez-moi de vous donner des exemples plus concrets. J'ai lu les plus excellents Directives pour la conception de composants F# (merci beaucoup à @gradbot pour cela !). Les directives du document, si elles sont utilisées, traitent de certains des problèmes, mais pas tous.

Le document est divisé en deux parties principales : 1) les directives pour cibler les utilisateurs de F# ; et 2) les directives pour cibler les utilisateurs de C#. ciblant les utilisateurs de C#. Nulle part il n'est question de prétendre qu'il est possible d'avoir une approche uniforme. uniforme, ce qui fait exactement écho à ma question : nous pouvons cibler F#, nous pouvons cibler C#, mais quelle est la solution pratique pour cibler les deux ? solution pratique pour cibler les deux ?

Pour rappel, le but est de disposer d'une bibliothèque rédigée en F#, et qui puisse être utilisée idiomatiquement de des langages F# et C#.

Le mot clé ici est idiomatique . Le problème n'est pas l'interopérabilité générale où il est juste possible d'utiliser des bibliothèques dans différents langages.

Passons maintenant aux exemples, que j'emprunte directement à l'anglais. Directives pour la conception de composants F# .

  1. Modules+fonctions (F#) vs Namespaces+Types+fonctions

    • F# : Utilisez des espaces de noms ou des modules pour contenir vos types et modules. L'utilisation idiomatique est de placer les fonctions dans des modules, par ex :

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      // Use from F#
      open Foo
      bar()
      zoo()
    • C# : Utilisez les espaces de noms, les types et les membres comme structure organisationnelle principale pour vos composants (par opposition aux modules). composants (par opposition aux modules), pour les API vanille .NET.

      Ceci est incompatible avec la directive F#, et l'exemple aurait besoin de réécrire l'exemple pour l'adapter aux utilisateurs de C# :

      [<AbstractClass; Sealed>]
      type Foo =
          static member bar() = ...
          static member zoo() = ...

      En faisant cela, nous brisons l'utilisation idiomatique de F# car nous ne pouvons plus utiliser bar y zoo sans le préfixer avec Foo .

  2. Utilisation des tuples

    • F# : Utilisez les tuples lorsque cela est approprié pour les valeurs de retour.

    • C# : Évitez d'utiliser des tuples comme valeurs de retour dans les API vanille de .NET.

  3. Async

    • F# : Utilisez Async pour la programmation asynchrone aux limites de l'API F#.

    • C# : Exposez les opérations asynchrones en utilisant soit le modèle de programmation asynchrone de .NET (BeginFoo, EndFoo), soit comme des méthodes retournant des tâches .NET (Task), plutôt que comme des objets F# Async F#.

  4. Utilisation de Option

    • F# : envisager d'utiliser des valeurs d'option pour les types de retour au lieu de lever des exceptions (pour le code orienté F#).

    • Envisagez d'utiliser le motif TryGetValue au lieu de renvoyer les valeurs des options F# (option) dans vanilla NET, et préférez la surcharge des méthodes à la prise en compte des valeurs d'option F# comme arguments.

  5. Syndicats discriminés

    • F# : Utiliser les unions discriminées comme alternative aux hiérarchies de classes pour créer des données structurées en arbre.

    • C# : Il n'y a pas de directives spécifiques à ce sujet, mais le concept d'unions discriminées est étranger au C#.

  6. Fonctions au curry

    • F# : les fonctions curry sont idiomatiques pour F#

    • C# : N'utilisez pas le currying des paramètres dans les API vanille .NET.

  7. Vérification des valeurs nulles

    • F# : ce n'est pas idiomatique pour F#

    • C# : Envisagez de vérifier les valeurs nulles sur les limites de l'API vanilla .NET.

  8. Utilisation des types F# list , map , set etc.

    • F# : il est idiomatique de les utiliser en F#

    • C# : Envisagez d'utiliser les types d'interface de collection .NET IEnumerable et IDictionary pour les paramètres et les valeurs de retour dans les API vanille de .NET. ( c'est-à-dire ne pas utiliser F# list , map , set )

  9. Types de fonctions (le plus évident)

    • F# : l'utilisation de fonctions F# comme valeurs est idiomatique pour F#, évidemment

    • C# : Utilisez les types de délégués .NET de préférence aux types de fonctions F# dans les API vanille .NET.

Je pense que cela devrait suffire à démontrer la nature de ma question.

D'ailleurs, les lignes directrices ont également une réponse partielle :

... une stratégie de mise en œuvre commune lors de l'élaboration d'une d'ordre supérieur pour les bibliothèques vanille de .NET est d'écrire toute l'implémentation en utilisant des types de fonctions F#, puis de créer l'API publique en utilisant des délégués comme mince façade. puis de créer l'API publique en utilisant des délégués comme une façade mince au-dessus de l'implémentation F# réelle.

Pour résumer.

Il y a une réponse définitive : il n'y a pas d'astuces de compilateur que j'ai manquées .

D'après la documentation sur les directives, il semble qu'il faille d'abord créer pour F# et ensuite créer un wrapper de façade pour .NET est une stratégie raisonnable.

La question reste alors de savoir comment mettre cela en pratique :

  • Des assemblages séparés ? ou

  • Des espaces de noms différents ?

Si mon interprétation est correcte, Tomas suggère que l'utilisation d'espaces de noms séparés devrait être suffisante, et devrait être une solution acceptable.

Je pense que je serai d'accord avec cela, étant donné que le choix des espaces de noms est tel qu'il qu'il ne surprenne pas ou n'embrouille pas les utilisateurs de .NET/C#. pour eux devrait probablement ressembler à l'espace de noms primaire. Les utilisateurs de F# devront assumer la charge de choisir l'espace de noms spécifique à F#. Par exemple :

  • FSharp.Foo.Bar -> espace de nom pour F# face à la bibliothèque

  • Foo.Bar -> espace de nom pour le wrapper .NET, idiomatique pour C#

8 votes

0 votes

À part distribuer des fonctions de premier ordre, quelles sont vos préoccupations ? F# contribue déjà largement à fournir une expérience transparente à partir d'autres langages.

0 votes

En ce qui concerne votre modification, je ne comprends toujours pas pourquoi vous pensez qu'une bibliothèque créée en F# ne serait pas standard par rapport au C#. Pour la plupart, F# fournit un sur-ensemble de la fonctionnalité C#. Faire en sorte qu'une bibliothèque semble naturelle est principalement une question de convention, ce qui est vrai quel que soit le langage que vous utilisez. Tout cela pour dire que F# fournit tous les outils dont vous avez besoin pour le faire.

41voto

Tomas Petricek Points 118959

Daniel a déjà expliqué comment définir une version C#-friendly de la fonction F# que vous avez écrite, je vais donc ajouter quelques commentaires de plus haut niveau. Tout d'abord, vous devriez lire le Directives pour la conception de composants F# (déjà référencé par gradbot). Il s'agit d'un document qui explique comment concevoir des bibliothèques F# et .NET en utilisant F# et il devrait répondre à bon nombre de vos questions.

Lorsque vous utilisez F#, il existe essentiellement deux types de bibliothèques que vous pouvez écrire :

  • Bibliothèque F# est conçu pour être utilisé uniquement de F#, de sorte que son interface publique est écrite dans un style fonctionnel (en utilisant des types de fonctions F#, des tuples, des unions discriminées, etc.)

  • Bibliothèque .NET est conçu pour être utilisé à partir de cualquier Le langage .NET (y compris C# et F#) et il suit généralement le style orienté objet de .NET. Cela signifie que vous exposerez la plupart des fonctionnalités sous forme de classes avec des méthodes (et parfois des méthodes d'extension ou des méthodes statiques, mais la plupart du temps, le code doit être écrit dans le style OO).

Dans votre question, vous demandez comment exposer la composition des fonctions en tant que bibliothèque .NET, mais je pense que les fonctions comme votre compose sont des concepts de trop bas niveau du point de vue de la bibliothèque .NET. Vous pouvez les exposer en tant que méthodes travaillant avec Func y Action mais ce n'est probablement pas ainsi que vous concevriez une bibliothèque .NET normale (vous utiliseriez peut-être plutôt le modèle Builder ou quelque chose du genre).

Dans certains cas (par exemple, lors de la conception de bibliothèques numériques qui ne correspondent pas vraiment au style des bibliothèques .NET), il est judicieux de concevoir une bibliothèque qui combine à la fois F# y .NET dans une seule bibliothèque. La meilleure façon de procéder est de disposer d'une API F# normale (ou .NET normale) et de fournir des wrappers pour une utilisation naturelle dans l'autre style. Les wrappers peuvent se trouver dans un espace de noms séparé (tel que MyLibrary.FSharp y MyLibrary ).

Dans votre exemple, vous pourriez laisser l'implémentation F# en MyLibrary.FSharp puis ajoutez des enveloppes .NET (adaptées à C#) (similaires au code que Daniel a posté) dans la section MyLibrary comme méthode statique d'une classe. Mais encore une fois, la bibliothèque .NET aurait probablement une API plus spécifique que la composition de fonctions.

2 votes

Les directives de conception des composants F# sont désormais hébergées. aquí

19voto

Daniel Points 29764

Il suffit d'envelopper la fonction valeurs (fonctions partiellement appliquées, etc.) avec Func o Action les autres sont convertis automatiquement. Par exemple :

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

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

Il est donc logique d'utiliser Func / Action le cas échéant. Cela élimine-t-il vos préoccupations ? Je pense que les solutions que vous proposez sont trop compliquées. Vous pouvez écrire toute votre bibliothèque en F# et l'utiliser sans problème à partir de F#. y C# (je le fais tout le temps).

En outre, F# est plus souple que C# en termes d'interopérabilité, de sorte qu'il est généralement préférable de suivre le style .NET traditionnel lorsque cela est une préoccupation.

EDITAR

Le travail nécessaire pour faire deux interfaces publiques dans des espaces de noms séparés, je pense, n'est justifié que lorsqu'elles sont complémentaires ou que la fonctionnalité F# n'est pas utilisable depuis C# (comme les fonctions inlined, qui dépendent de métadonnées spécifiques à F#).

Je prends vos points à tour de rôle :

  1. Module + let Les liaisons et les types sans constructeur + les membres statiques apparaissent exactement de la même manière en C#, alors optez pour les modules si vous le pouvez. Vous pouvez utiliser CompiledNameAttribute pour donner aux membres des noms adaptés à C#.

  2. Je peux me tromper, mais je pense que les directives relatives aux composants ont été rédigées avant System.Tuple qui sont ajoutés au cadre. (Dans les versions antérieures, F# définissait son propre type de tuple.) Il est depuis devenu plus acceptable d'utiliser Tuple dans une interface publique pour les types triviaux.

  3. C'est là que je pense que vous devez faire les choses à la manière de C# parce que F# joue bien avec Task mais le C# ne joue pas bien avec Async . Vous pouvez utiliser l'asynchronisme en interne puis appeler Async.StartAsTask avant de revenir d'une méthode publique.

  4. L'étreinte de null est peut-être le plus gros inconvénient du développement d'une API à utiliser à partir de C#. Dans le passé, j'ai essayé toutes sortes d'astuces pour éviter de prendre en compte les nullités dans le code interne de F# mais, en fin de compte, il était préférable de marquer les types avec des constructeurs publics avec [<AllowNullLiteral>] et vérifie que les arguments ne sont pas nuls. Ce n'est pas pire que le C# à cet égard.

  5. Les unions discriminées sont généralement compilées dans les hiérarchies de classes mais ont toujours une représentation relativement conviviale en C#. Je dirais qu'il faut les marquer avec [<AllowNullLiteral>] et les utiliser.

  6. Les fonctions curvilignes produisent des fonctions valeurs qui ne devrait pas être utilisé.

  7. J'ai trouvé qu'il était préférable d'accepter null plutôt que de dépendre du fait qu'il soit détecté dans l'interface publique et de l'ignorer en interne. YMMV.

  8. Il est très logique d'utiliser list / map / set interne. Ils peuvent tous être exposés via l'interface publique en tant que IEnumerable<_> . Aussi, seq , dict y Seq.readonly sont souvent utiles.

  9. Voir n°6.

La stratégie à adopter dépend du type et de la taille de votre bibliothèque mais, d'après mon expérience, trouver le juste milieu entre F# et C# a demandé moins de travail - à long terme - que la création d'API distinctes.

0 votes

Oui je vois qu'il y a des moyens de faire fonctionner les choses, je ne suis pas entièrement convaincu. Re les modules. Si vous avez module Foo.Bar.Zoo alors en C#, ce sera class Foo dans l'espace de noms Foo.Bar . Cependant, si vous avez des modules imbriqués comme module Foo=... module Bar=... module Zoo== ce qui va générer la classe Foo avec des classes internes Bar y Zoo . Re IEnumerable Cela peut ne pas être acceptable lorsque l'on a vraiment besoin de travailler avec des cartes et des ensembles. Re null et AllowNullLiteral -- une des raisons pour lesquelles j'aime F# est que j'en ai assez de null en C#, je ne voudrais vraiment pas tout polluer avec ça à nouveau !

0 votes

Favorise généralement les espaces de noms plutôt que les modules imbriqués pour l'interopérabilité. Re : null, je suis d'accord avec vous, c'est certainement une régression de devoir l'envisager, mais c'est quelque chose que vous devez gérer si votre API sera utilisée à partir de C#.

2voto

gradbot Points 9219

Projet de directives de conception de composants F# (août 2010)

Vue d'ensemble Ce document examine certaines des questions liées à la conception et au codage des composants F#. En particulier, il couvre :

  • Lignes directrices pour la conception de bibliothèques .NET "classiques" pouvant être utilisées à partir de n'importe quel langage .NET.
  • Directives pour les bibliothèques F# à F# et le code d'implémentation F#.
  • Suggestions sur les conventions de codage pour le code d'implémentation F#

2voto

ShdNx Points 1840

Bien que ce soit probablement un peu exagéré, vous pourriez envisager d'écrire une application utilisant Mono.Cecil (il bénéficie d'un soutien formidable sur la liste de diffusion) qui automatiserait la conversion au niveau de l'IL. Par exemple, vous implémentez votre assemblage en F#, en utilisant l'API publique de style F#, puis l'outil génère un wrapper C# convivial.

Par exemple, en F#, vous utiliserez évidemment option<'T> ( None notamment) au lieu d'utiliser null comme en C#. L'écriture d'un générateur de wrapper pour ce scénario devrait être assez facile : la méthode wrapper invoquerait la méthode originale : si la valeur de retour est Some x puis retourner x sinon, retour null .

Vous devrez gérer le cas où T est un type de valeur, c'est-à-dire qu'il n'est pas annulable ; vous devrez intégrer la valeur de retour de la méthode wrapper dans le code suivant Nullable<T> ce qui le rend un peu douloureux.

Encore une fois, je suis tout à fait certain qu'il serait payant d'écrire un tel outil dans votre scénario, sauf peut-être si vous travaillez régulièrement sur cette bibliothèque (utilisable de manière transparente à partir de F# et de C#). En tout cas, je pense que ce serait une expérience intéressante, que je pourrais même explorer un jour.

0 votes

Cela semble intéressant. Est-ce que l'utilisation de Mono.Cecil En fait, il s'agit d'écrire un programme qui charge l'assemblage F#, trouve les éléments nécessitant un mappage, puis émet un autre assemblage avec les enveloppes C# ? Est-ce que j'ai bien compris ?

0 votes

@Komrade P. : Tu peux faire ça aussi, mais c'est beaucoup plus compliqué, car il faudrait alors ajuster toutes les références pour pointer vers les membres de la nouvelle assemblée. C'est beaucoup plus simple si vous ajoutez simplement les nouveaux membres (les wrappers) à l'assemblage existant écrit en F#.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X