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 #