J'essaye de concevoir une bibliothèque en F #. La bibliothèque doit être conviviale pour une utilisation à la fois en F # et C # .
Et c'est là que je suis un peu coincé. Je peux le rendre compatible F #, ou je peux le rendre compatible C #, mais le problème est de savoir comment le rendre convivial pour les deux.
Voici un exemple. Imaginez 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 #, la compose
fonction 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
et à la Action
place, comme ceci:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Pour y parvenir, je peux créer une compose2
fonction comme celle-ci:
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Maintenant, c'est parfaitement utilisable en C #:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Mais maintenant, nous avons un problème avec l'utilisation compose2
de F #! Maintenant, je dois envelopper tous les F # standard funs
dans Func
et 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 pouvons-nous obtenir une expérience de première classe pour la bibliothèque écrite en F # pour les utilisateurs F # et C #?
Jusqu'à présent, je ne pouvais rien proposer de mieux que ces deux approches:
- Deux assemblys distincts: l'un destiné aux utilisateurs F # et le second aux utilisateurs C #.
- Un assembly mais des espaces de noms différents: un pour les utilisateurs F # et le second pour les utilisateurs C #.
Pour la première approche, je ferais quelque chose comme ceci:
Créez un projet F #, appelez-le FooBarFs et compilez-le dans FooBarFs.dll.
- Ciblez la bibliothèque uniquement aux utilisateurs F #.
- Cachez tout ce qui n'est pas nécessaire dans les fichiers .fsi.
Créez un autre projet F #, appelez if FooBarCs et compilez-le dans FooFar.dll
- Réutilisez le premier projet F # au niveau source.
- Créez un fichier .fsi qui cache tout de ce projet.
- Créez un fichier .fsi qui expose la bibliothèque de manière C #, en utilisant des idiomes C # pour le nom, les espaces de noms, etc.
- Créez des wrappers qui délèguent à la bibliothèque principale, 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 alors vous avez un assemblage.
La question: aucun de ceux-ci n'est idéal, il me manque peut-être une sorte d'indicateur / commutateur / attribut de compilateur ou une sorte d'astuce et il y a une meilleure façon de faire cela?
La question: quelqu'un d'autre a-t-il 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 dénomination, les expressions idiomatiques et autres qui sont natifs de C #. Fondamentalement, un utilisateur C # ne devrait pas être en mesure de détecter que la bibliothèque a été créée en F #. Et vice versa, un utilisateur F # devrait avoir envie de travailler avec une bibliothèque C #.
MODIFIER 2:
Je peux voir d'après les réponses et les commentaires jusqu'à présent que ma question n'a pas la profondeur nécessaire, peut-être principalement en raison de l'utilisation d'un seul exemple où des problèmes d'interopérabilité entre F # et C # se posent, la question des valeurs de fonction. Je pense que c'est l'exemple le plus évident et cela m'a donc amené à l'utiliser pour poser la question, mais du même coup a donné l'impression que c'est la seule question qui me préoccupe.
Permettez-moi de donner des exemples plus concrets. J'ai lu le plus excellent document F # Component Design Guidelines (merci beaucoup @gradbot pour cela!). Les lignes directrices du document, si elles sont utilisées, abordent certains des problèmes, mais pas tous.
Le document est divisé en deux parties principales: 1) directives pour cibler les utilisateurs F #; et 2) des directives pour cibler les utilisateurs C #. Nulle part il n'essaye même de prétendre qu'il est possible d'avoir une approche uniforme, qui fait exactement écho à ma question: on peut cibler F #, on peut cibler C #, mais quelle est la solution pratique pour cibler les deux?
Pour rappel, l'objectif est d'avoir une bibliothèque créée en F #, et qui peut être utilisée de manière idiomatique à partir 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 simplement possible d'utiliser des bibliothèques dans différentes langues.
Passons maintenant aux exemples, que je tire directement des Directives de conception de composants F # .
Modules + fonctions (F #) vs espaces de noms + types + fonctions
F #: utilisez des espaces de noms ou des modules pour contenir vos types et modules. L'utilisation idiomatique est de placer des fonctions dans des modules, par exemple:
// 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), pour les API .NET vanilla.
Ceci est incompatible avec la directive F #, et l'exemple devrait être réécrit pour s'adapter aux utilisateurs C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
En faisant cela, nous cassons l'utilisation idiomatique de F # car nous ne pouvons plus l'utiliser
bar
etzoo
sans le préfixer avecFoo
.
Utilisation de tuples
F #: utilisez des tuples lorsque cela est approprié pour les valeurs de retour.
C #: évitez d'utiliser des tuples comme valeurs de retour dans les API .NET vanilla.
Async
F #: utilisez Async pour la programmation asynchrone aux limites de l'API F #.
C #: exposez les opérations asynchrones à l'aide du modèle de programmation asynchrone .NET (BeginFoo, EndFoo), ou en tant que méthodes renvoyant des tâches .NET (Task), plutôt qu'en tant qu'objets F # Async.
Utilisation de
Option
F #: envisagez d'utiliser des valeurs d'option pour les types de retour au lieu de déclencher des exceptions (pour le code F #).
Envisagez d'utiliser le modèle TryGetValue au lieu de renvoyer les valeurs d'option F # (option) dans les API .NET vanilla, et préférez la surcharge de méthode à la prise des valeurs d'option F # comme arguments.
Syndicats discriminés
F #: Utilisez des unions discriminées comme alternative aux hiérarchies de classes pour créer des données arborescentes
C #: pas de directives spécifiques pour cela, mais le concept d'unions discriminées est étranger à C #
Fonctions au curry
F #: les fonctions curry sont idiomatiques pour F #
C #: n'utilisez pas le currying des paramètres dans les API .NET vanilla.
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 .NET vanilla.
L' utilisation de F types de #
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 .NET vanilla. ( Ie ne pas utiliser F #
list
,map
,set
)
Types de fonction (le plus évident)
F #: l' utilisation des 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 .NET vanilla.
Je pense que cela devrait suffire à démontrer la nature de ma question.
Incidemment, les lignes directrices ont également une réponse partielle:
... une stratégie d'implémentation courante lors du développement de méthodes d'ordre supérieur pour les bibliothèques .NET vanilla est de créer toute l'implémentation à l'aide de types de fonction F #, 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 compilation que j'ai manquées .
Selon le document de directives, il semble que la création pour F # d'abord, puis la création d'un wrapper de façade pour .NET soit une stratégie raisonnable.
Reste alors la question de la mise en œuvre pratique de ceci:
Assemblages séparés? ou
Différents espaces de noms?
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 ne surprend ni ne confond les utilisateurs .NET / C #, ce qui signifie que l'espace de noms pour eux devrait probablement ressembler à l'espace de noms principal pour eux. Les utilisateurs F # devront assumer le fardeau de choisir un espace de noms spécifique à F #. Par exemple:
FSharp.Foo.Bar -> espace de noms pour F # face à la bibliothèque
Foo.Bar -> namespace pour le wrapper .NET, idiomatique pour C #
Réponses:
Daniel a déjà expliqué comment définir une version compatible C # de la fonction F # que vous avez écrite, je vais donc ajouter quelques commentaires de plus haut niveau. Tout d'abord, vous devez lire les directives de conception de composants F # (déjà référencées par gradbot). Il s'agit d'un document qui explique comment concevoir des bibliothèques F # et .NET à l'aide de F # et il devrait répondre à beaucoup de vos questions.
Lorsque vous utilisez F #, il existe essentiellement deux types de bibliothèques que vous pouvez écrire:
La bibliothèque F # est conçue pour être utilisée uniquement à partir de F #, donc son interface publique est écrite dans un style fonctionnel (en utilisant des types de fonctions F #, des tuples, des unions discriminées, etc.)
La bibliothèque .NET est conçue pour être utilisée à partir de n'importe quel langage .NET (y compris C # et F #) et elle suit généralement le style orienté objet .NET. Cela signifie que vous exposerez la plupart des fonctionnalités sous forme de classes avec méthode (et parfois des méthodes d'extension ou des méthodes statiques, mais la plupart du temps, le code doit être écrit dans la conception OO).
Dans votre question, vous demandez comment exposer la composition de fonctions en tant que bibliothèque .NET, mais je pense que des fonctions comme la vôtre
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 fonctionnant avecFunc
etAction
, mais ce n'est probablement pas ainsi que vous concevriez une bibliothèque .NET normale en premier lieu (vous utiliseriez peut-être le modèle Builder à la place ou quelque chose du genre).Dans certains cas (c'est-à-dire lors de la conception de bibliothèques numériques qui ne correspondent pas vraiment au style de bibliothèque .NET), il est judicieux de concevoir une bibliothèque qui mélange les styles F # et .NET dans une seule bibliothèque. La meilleure façon de le faire est d'avoir une API F # normale (ou .NET normale), puis de fournir des wrappers pour une utilisation naturelle dans l'autre style. Les wrappers peuvent être dans un espace de noms séparé (comme
MyLibrary.FSharp
etMyLibrary
).Dans votre exemple, vous pouvez laisser l'implémentation F # dans
MyLibrary.FSharp
, puis ajouter des wrappers .NET (C #-friendly) (similaires au code que Daniel a publié) dans l'MyLibrary
espace de noms en tant que 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.la source
Vous n'avez qu'à encapsuler les valeurs de fonction ( fonctions partiellement appliquées, etc.) avec
Func
ouAction
, le reste est converti automatiquement. Par exemple:Il est donc logique d'utiliser
Func
/ leAction
cas échéant. Est-ce que cela élimine 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 douleur à partir de F # et C # (je le fais tout le temps).De plus, F # est plus flexible que C # en termes d'interopérabilité, il est donc généralement préférable de suivre le style .NET traditionnel lorsque cela pose un problème.
ÉDITER
Le travail nécessaire pour créer deux interfaces publiques dans des espaces de noms séparés, je pense, n'est justifié que lorsqu'ils sont complémentaires ou que la fonctionnalité F # n'est pas utilisable à partir de C # (comme les fonctions intégrées, qui dépendent des métadonnées spécifiques à F #).
Reprenez vos points à tour de rôle:
Module +
let
liaisons et type sans constructeur + membres statiques apparaissent exactement les mêmes en C #, alors optez pour des modules si vous le pouvez. Vous pouvez utiliserCompiledNameAttribute
pour donner aux membres des noms compatibles C #.Je me trompe peut-être, mais je suppose que les directives sur les composants ont été rédigées avant d'
System.Tuple
être ajoutées au cadre. (Dans les versions antérieures, F # définissait son propre type de tuple.) Il est depuis devenu plus acceptable de l'utiliserTuple
dans une interface publique pour les types triviaux.C'est là que je pense que vous devez faire les choses à la manière C # parce que F # joue bien avec
Task
mais C # ne joue pas bien avecAsync
. Vous pouvez utiliser async en interne puis appelerAsync.StartAsTask
avant de revenir d'une méthode publique.L'adoption de
null
peut être le plus gros inconvénient lors du développement d'une API à utiliser à partir de C #. Dans le passé, j'ai essayé toutes sortes d'astuces pour éviter de considérer null dans le code F # interne mais, à la fin, il était préférable de marquer les types avec des constructeurs publics avec[<AllowNullLiteral>]
et de vérifier les arguments pour null. Ce n'est pas pire que C # à cet égard.Les unions discriminées sont généralement compilées en hiérarchies de classes mais ont toujours une représentation relativement conviviale en C #. Je dirais, marquez-les
[<AllowNullLiteral>]
et utilisez-les.Les fonctions curry produisent des valeurs de fonction , qui ne doivent pas être utilisées.
J'ai trouvé qu'il valait mieux embrasser null que de dépendre de sa capture à l'interface publique et de l'ignorer en interne. YMMV.
Il est très judicieux d'utiliser
list
/map
/ enset
interne. Ils peuvent tous être exposés via l'interface publique au formatIEnumerable<_>
. En outre,seq
,dict
etSeq.readonly
sont souvent utiles.Voir # 6.
La stratégie que vous adoptez 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 # nécessitait moins de travail - à long terme - que de créer des API distinctes.
la source
module Foo.Bar.Zoo
alors en C #, ce seraclass Foo
dans l'espace de nomsFoo.Bar
. Cependant, si vous avez des modules imbriqués commemodule Foo=... module Bar=... module Zoo==
, cela générera une classeFoo
avec des classes internesBar
etZoo
. ReIEnumerable
, cela peut ne pas être acceptable lorsque nous avons vraiment besoin de travailler avec des cartes et des ensembles. Renull
valeurs etAllowNullLiteral
- une des raisons pour lesquelles j'aime F # est que j'en ai marrenull
en C #, je ne voudrais vraiment pas polluer tout avec à nouveau!Bien que ce soit probablement exagéré, vous pourriez envisager d'écrire une application utilisant Mono.Cecil (il a un support impressionnant sur la liste de diffusion) qui automatiserait la conversion au niveau IL. Par exemple, si vous implémentez votre assembly en F #, à l'aide de l'API publique de style F #, l'outil générerait un wrapper compatible C # dessus.
Par exemple, en F #, vous utiliseriez évidemment
option<'T>
(None
, spécifiquement) au lieu d'utilisernull
comme en C #. L'écriture d'un générateur de wrapper pour ce scénario devrait être assez simple: la méthode wrapper invoquerait la méthode d'origine: si sa valeur de retour étaitSome x
, alors returnx
, sinon returnnull
.Vous auriez besoin de gérer le cas où
T
est un type valeur, c'est-à-dire non nullable; vous devrez envelopper la valeur de retour de la méthode wrapperNullable<T>
, ce qui la rend un peu pénible.Encore une fois, je suis tout à fait certain qu'il serait payant d'écrire un tel outil dans votre scénario, peut-être sauf si vous travaillerez régulièrement sur cette bibliothèque (utilisable de manière transparente à partir de F # et C #) régulièrement. En tout cas, je pense que ce serait une expérience intéressante, que je pourrais même explorer un jour.
la source
Mono.Cecil
signifierait essentiellement écrire un programme pour charger l'assembly F #, trouver des éléments nécessitant un mappage et émettre un autre assembly avec les wrappers C #? Ai-je bien compris?Projet de lignes directrices pour la conception des composants F # (août 2010)
la source