J'essaie de concevoir une bibliothèque en F #. La bibliothèque doit être conviviale pour une utilisation à partir de à la fois F # et C #.
Et c'est là que je suis un peu coincé. Je peux le rendre convivial F #, ou je peux le rendre convivial 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 depuis F #:
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
En C #, la fonction 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
et Action
à la place, comme ceci:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Pour ce faire, je peux créer compose2
fonctionne comme ceci:
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 du mal à utiliser compose2
de F #! Maintenant, je dois envelopper tous les F # funs
standard 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 n'ai rien trouvé de mieux que ces deux approches:
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.
Créez un autre projet F #, appelez if FooBarCs et compilez-le dans FooFar.dll
Je pense que la deuxième approche avec les espaces de noms peut être déroutante pour les utilisateurs, mais vous avez alors un assembly.
La question: aucune de celles-ci n'est idéale, peut-être qu'il me manque une sorte d'indicateur de compilateur/commutateur/attribte ou une sorte d'astuce et il y a une meilleure façon de faire ça?
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 idiomes et similaires 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 traiter avec une bibliothèque C #.
EDIT 2:
Je peux voir dans les réponses et 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 fonctions et des valeurs. 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, j'ai donné 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 le plus excellent document F # Component Design Guidelines (merci beaucoup @gradbot pour cela!). Les lignes directrices du document, si elles sont utilisées, traitent certains 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'essaie même de prétendre qu'il est possible d'avoir une approche 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?
Pour rappel, le but est d'avoir une bibliothèque créée en F #, et qui peut être utilisée idiomatiquement à 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 possible d'utiliser des bibliothèques dans différentes langues.
Passons maintenant aux exemples, que je prends directement de F # Component Design Guidelines .
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 des espaces de noms, des types et des membres comme structure organisationnelle principale pour vos composants (par opposition aux modules), pour les API Vanilla .NET.
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() = ...
Ce faisant, nous rompons l'utilisation idiomatique de F # car nous ne pouvons plus utiliser bar
et Zoo
sans le préfixer avec Foo
.
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 Vanilla .NET.
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 (tâche), 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 des valeurs d'options F # (option) dans les API Vanilla .NET, et préférez la surcharge de méthode à la prise de valeurs d'options 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 curry
F #: les fonctions au curry sont idiomatiques pour F #
C #: n'utilisez pas le currying des paramètres dans les API Vanilla .NET.
Vérification des valeurs nulles
F #: ce n'est pas idiomatique pour F #
C #: pensez à vérifier les valeurs nulles sur les limites de l'API Vanilla .NET.
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 Vanilla .NET. ( c'est-à-dire, n'utilisez pas F # list
, map
, set
)
Types de fonctions (l'évidente)
F #: l'utilisation des fonctions F # comme valeurs est idiomatique pour F #, évidemment
C #: utilisez les types délégués .NET de préférence aux types de fonctions F # dans les API Vanilla .NET.
Je pense que cela devrait suffire à démontrer la nature de ma question.
Soit dit en passant, 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 Vanilla .NET consiste à créer toute l'implémentation à l'aide des types de fonctions F #, puis à créer l'API publique en utilisant les 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 de tours de compilation que j'ai ratés.
Selon le document de directives, il semble que la création de F # d'abord, puis la création d'un wrapper de façade pour .NET est la 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 pas ou ne confond pas 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 prendre la charge de choisir un espace de noms spécifique à F #. Par exemple:
FSharp.Foo.Bar -> espace de noms pour la bibliothèque F #
Foo.Bar -> espace de noms pour le wrapper .NET, idiomatique pour C #
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 niveau supérieur. Tout d'abord, vous devriez lire les F # Component Design Guidelines (déjà référencé par gradbot). Il s'agit d'un document qui explique comment concevoir des bibliothèques F # et .NET à l'aide de F # et qui devrait répondre à bon nombre 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 seulement de F #, donc son interface publique est écrite dans un style fonctionnel (en utilisant Types de fonctions F #, tuples, unions discriminées, etc.)
. La bibliothèque NET est conçue pour être utilisée à partir du langage any .NET (y compris C # et F #) et elle suit généralement 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 surtout 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 votre compose
sont des concepts de niveau trop bas du point de vue de la bibliothèque .NET. Vous pouvez les exposer en tant que méthodes fonctionnant avec Func
et Action
, 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 comme ça).
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 deux F # et . Styles NET dans une seule bibliothèque. La meilleure façon de le faire est d'avoir une API F # (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
et MyLibrary
).
Dans votre exemple, vous pouvez laisser l'implémentation F # dans MyLibrary.FSharp
, puis ajoutez des wrappers .NET (compatibles C #) (similaires au code publié par Daniel) dans l'espace de noms MyLibrary
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 des fonctions.
Vous n'avez qu'à encapsuler les valeurs des fonctions (fonctions partiellement appliquées, etc.) avec Func
ou Action
, le reste 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 l'intégralité de 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 problème.
ÉDITER
Je pense que le travail requis pour créer deux interfaces publiques dans des espaces de noms séparés n'est garanti que lorsqu'elles sont complémentaires ou que la fonctionnalité F # n'est pas utilisable à partir de C # (comme les fonctions intégrées, qui dépendent de métadonnées spécifiques à F #).
Prendre vos points à tour de rôle:
Les liaisons module + let
et les membres statiques + type sans constructeur apparaissent exactement de la même façon en C #, alors utilisez les modules si vous le pouvez. Vous pouvez utiliser CompiledNameAttribute
pour donner aux membres des noms conviviaux en C #.
Je me trompe peut-être, mais je pense que les directives sur les composants ont été écrites avant System.Tuple
en cours d'ajout 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.
C'est là que je pense que vous devez faire les choses en C # parce que F # joue bien avec Task
mais C # ne joue pas bien avec Async
. Vous pouvez utiliser async en interne puis appeler Async.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, au final, il était préférable de marquer les types avec des constructeurs publics avec [<AllowNullLiteral>]
et vérifiez 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 avec [<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 sur l'interface publique et de l'ignorer en interne. YMMV.
Il est très judicieux d'utiliser list
/map
/set
en interne. Ils peuvent tous être exposés via l'interface publique comme IEnumerable<_>
. En outre, seq
, dict
et Seq.readonly
sont souvent utiles.
Voir # 6.
La stratégie que vous choisissez 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 nécessité moins de travail - à long terme - que la création d'API distinctes.
Bien que ce soit probablement une exagération, vous pourriez envisager d'écrire une application en utilisant Mono.Cecil (elle a un support impressionnant sur la liste de diffusion) qui automatiserait la conversion au niveau IL. Par exemple, vous implémentez votre assembly en F #, à l'aide de l'API publique de style F #, puis l'outil génère un wrapper compatible C # par-dessus.
Par exemple, en F #, vous utiliserez évidemment option<'T>
(None
, en particulier) au lieu d'utiliser null
comme en C #. L'écriture d'un générateur d'encapsuleur pour ce scénario devrait être assez simple: la méthode d'encapsuleur invoquerait la méthode d'origine: si sa valeur de retour était Some x
, puis retournez x
, sinon retournez null
.
Vous devrez gérer le cas où T
est un type de valeur, c'est-à-dire non nullable; vous devez encapsuler la valeur de retour de la méthode wrapper dans Nullable<T>
, ce qui le rend un peu douloureux.
Encore une fois, je suis certain que cela serait payant d'écrire un tel outil dans votre scénario, peut-être sauf si vous travaillez régulièrement sur cette bibliothèque (utilisable de manière transparente à partir de F # et C #). En tout cas, je pense que ce serait une expérience intéressante, que je pourrais même explorer un jour.
Projet de directives de conception des composants F # (août 2010)
Présentation Ce document examine certains des problèmes liés à la conception et au codage des composants F #. Il couvre notamment:
- Lignes directrices pour la conception de bibliothèques .NET "Vanilla" à utiliser à partir de n'importe quel langage .NET.
- Lignes directrices pour les bibliothèques F # -to-F # et le code d'implémentation F #.
- Suggestions sur les conventions de codage pour le code d'implémentation F #