113 votes

Meilleure approche pour la conception de bibliothèques de F # pour une utilisation à la fois F # et c#

Je suis en train de concevoir une bibliothèque en F#. La bibliothèque doit être convivial pour une utilisation de tous les F# et C#.

Et c'est là que je bloque un peu. Je peux le faire F# sympathique, ou je peux le faire en C# sympathique, mais le problème est de savoir comment rendre sympathique pour les deux.

Ici est un exemple. Imaginez, j'ai la fonction suivante F#:

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

C'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#, 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 est - Func et Action au lieu de cela, comme ceci:

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

Pour y parvenir, je peux créer compose2 fonction 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 le problème à l'aide de compose2 de F#! Maintenant, j'ai à placer tous les standard F# funs en 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 parvenir à l'expérience de première classe pour la bibliothèque écrite en F# pour les deux F# et C# les utilisateurs?

Jusqu'à présent, je ne pouvais pas trouver quelque chose de mieux que ces deux approches:

  1. Deux assemblées distinctes: l'un est destiné pour les utilisateurs de F#, et la seconde à la C# les utilisateurs.
  2. Une assemblée, mais des espaces de noms différents: un pour les utilisateurs de F#, et la seconde pour C# les utilisateurs.

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

  1. Créer projet F#, appeler FooBarFs et les compiler dans FooBarFs.dll.

    • Cible de la bibliothèque uniquement pour les utilisateurs de F#.
    • Masquer tout ce qui est inutile à partir de l' .fsi fichiers.
  2. Créer un autre projet F#, appel si FooBarCs et les compiler dans FooFar.dll

    • Réutiliser le premier projet F# au niveau de la source.
    • Créer .fsi fichier qui cache tout de ce projet.
    • Créer .fsi fichier qui expose à la bibliothèque en C# chemin, à l'aide de C# idiomes de nom, les espaces de noms etc.
    • Créer des wrappers de délégué à la bibliothèque de base, de faire la conversion, le cas échéant.

Je pense que la deuxième approche avec les espaces de noms peuvent être source de confusion pour les utilisateurs, mais alors vous avez un montage.

La question: aucune de ces sont idéal, peut-être que je suis absent une sorte de compilateur drapeau/interrupteur/attribte ou une sorte de truc et il y a une meilleure manière de faire ceci?

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

EDIT: pour clarifier, la question n'est pas seulement à propos des fonctions et des délégués, mais l'ensemble de l'expérience en C# d'un utilisateur avec une bibliothèque F#. Cela comprend les espaces de noms, les conventions de nommage, les expressions idiomatiques et d'autres choses semblables qui sont indigènes dans C#. Fondamentalement, C# l'utilisateur ne devrait pas être en mesure de détecter que la bibliothèque a été créé en F#. Et vice versa, un F# l'utilisateur doit se sentir comme traiter avec un C# de la bibliothèque.


EDIT 2:

Je peux le voir dans les réponses et les commentaires jusqu'à présent que ma question ne manque de la profondeur nécessaire, peut-être surtout en raison de l'utilisation d'un seul exemple où les problèmes d'interopérabilité entre fa# et C# surviennent, la question des fonctions a valeurs. Je pense que c'est l'exemple le plus évident et donc ce m'a amené à l'utiliser pour poser la question, mais du même coup, a donné l'impression que c'est la seule question que je me soucie.

Permettez-moi de fournir des exemples plus concrets. J'ai lu attentivement la plus excellente F# Composant Les Lignes Directrices De Conception document (merci @gradbot pour cela!). Les lignes directrices dans le document, si utilisé, ne adresse certains problèmes, mais pas tous.

Le document est-il divisé en deux parties principales: 1) des lignes directrices pour cibler les utilisateurs de F#; et 2) des lignes directrices pour ciblage de C# les utilisateurs. Nulle part il n'est même tenter de prétendre qu'il est possible d'avoir un uniforme l'approche, qui fait écho à ma question: nous pouvons cibler F#, nous pouvons cibler C#, mais qu'est-ce que l' solution pratique pour cibler à la fois?

Pour rappeler, l'objectif est d'avoir une bibliothèque de l'auteur en F#, et qui peut être utilisé idiomatiquede les deux F# et C# langues.

Le mot clé ici est idiomatiques. Le problème n'est pas le général d'interopérabilité où il est simplement possible utilisation de bibliothèques dans les différentes langues.

Maintenant, pour les exemples, je prends directement à partir de F# Composant Les Lignes Directrices De Conception.

  1. Modules+fonctions (F#) vs les espaces de noms+Types+fonctions

    • F#: Faire utiliser des espaces de noms ou des modules pour contenir vos types et les modules. L'orchestration est de placer des fonctions dans les modules, par exemple:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
      
    • C#: Faire utiliser des espaces de noms, les types et les membres de la primaire la structure organisationnelle de votre les composants (par opposition à des modules), de la vanille .NET Api.

      Ceci est incompatible avec le F# la directive, et l'exemple aurait besoin pour être ré-écrit pour s'adapter à la C# les utilisateurs:

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

      Ce faisant, nous briser l'orchestration à partir de F# parce que on ne peut plus utiliser bar et zoo sans préfixant avec Foo.

  2. L'utilisation de n-uplets

    • F#: utilisez les tuples le cas échéant, pour les valeurs de retour.

    • C#: Évitez d'utiliser des tuples comme valeurs de retour à la vanille .NET Api.

  3. Async

    • F#: N'utiliser Asynchrone pour la programmation asynchrone à F# API limites.

    • C#: N'exposez les opérations asynchrones en utilisant soit la .NET modèle de programmation asynchrone (BeginFoo, EndFoo), ou que les méthodes de retour .NET tâches (Tâche), plutôt que comme F# Async objets.

  4. L'utilisation d' Option

    • F#: Envisager d'utiliser des valeurs d'option pour les types de retour, au lieu de lever des exceptions (pour F#-face de code).

    • Envisager l'utilisation de la TryGetValue modèle au lieu de retourner F# les valeurs de l'option (en option) dans la vanille .NET Api, et préfèrent la surcharge de méthode pour prendre F# option valeurs en tant qu'arguments.

  5. Victimes de syndicats

    • F#: N'utilisez victimes de syndicats comme une alternative à des hiérarchies de classes pour la création structurée de l'arbre de données

    • C#: pas de lignes directrices spécifiques pour cela, mais le concept de discrimination syndicats est étrangère à la C#

  6. Fonctions curryfiées

    • F#: fonctions curryfiées sont idiomatiques pour F#

    • C#: Ne pas utiliser de nourrissage des paramètres de vanille .NET Api.

  7. Vérifier les valeurs null

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

    • C#: Envisager de contrôle pour les valeurs null sur la vanille .NET API limites.

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

    • F#: c'est idiomatiques de les utiliser dans F#

    • C#: pensez à utiliser l' .Collecte NETTE types d'interface IEnumerable et IDictionary pour les paramètres et valeurs de retour à la vanille .NET Api. (c'est à dire ne pas utiliser de F# list, map, set)

  9. Des types de fonction (le plus évident)

    • F#: utilisation des fonctions F# comme des valeurs est idiomatiques F# obiously

    • C#: Faire de l'utiliser .NET délégué des types, de préférence à F# fonction des types de la vanille .NET Api.

Je pense que ce doit être suffisante pour démontrer la nature de ma question.

Par ailleurs, les lignes directrices ont également réponse partielle:

... une commune de la stratégie de mise en œuvre lors de l'élaboration d'ordre supérieur méthodes pour la vanille .Bibliothèques NET est l'auteur de tous la mise en œuvre à l'aide de F# des types de fonction, et créez ensuite le public de l'API à l'aide de délégués qu'une mince façade au sommet de la réelle F# mise en œuvre.

Pour résumer.

Il n'y est une réponse définitive: il n'y a pas de compilateur trucs que j'ai raté.

Selon les lignes directrices de la doc, il semble que la création de F# d'abord et, ensuite, la création une façade wrapper pour .NET est la stratégie raisonnable.

Alors la question demeure quant à la mise en œuvre pratique de cette:

  • Assemblées distinctes? ou

  • Des espaces de noms différents?

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

Je pense que je suis d'accord avec ce que le choix des espaces de noms est telle qu'il ne pas le surprendre ou de brouiller les cartes .NET/C# les utilisateurs, ce qui signifie que l'espace de noms pour eux doit probablement ressembler à elle est l'espace de noms principal. L' F# les utilisateurs auront à prendre la charge de choisir F#-espace de noms spécifique. Par exemple:

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

  • Foo.Bar -> espace de noms .NET wrapper, idiomatiques pour C#

40voto

Tomas Petricek Points 118959

Daniel déjà expliqué comment définir un C#-version imprimable de la fonction F# que vous avez écrit, je vais donc ajouter un peu plus au niveau des commentaires. Tout d'abord, vous devriez lire le F# Composant les lignes Directrices de Conception (déjà référencé par gradbot). C'est un document qui explique comment concevoir F# et .NET des bibliothèques à l'aide de F# et il doit répondre à plusieurs de vos questions.

Lors de l'utilisation de F#, il existe essentiellement deux types de bibliothèques, vous pouvez écrire:

  • F# bibliothèque est conçu pour être utilisé uniquement à partir de F#, donc c'est l'interface publique est écrit dans un style fonctionnel (en utilisant la fonction F# les types, les tuples, les victimes de syndicats, etc.)

  • .NET-library est conçu pour être utilisé à partir de tout .NET de la langue (notamment en C# et F#) et il suit en général .NET orientée objet style. Cela signifie que vous allez exposer la plupart des fonctionnalités que les classes avec la méthode (et parfois les méthodes d'extension ou de méthodes statiques, mais surtout le code doit être écrit dans le OO design).

Dans votre question, vous me demandez comment exposer fonction de la composition une .NET de la bibliothèque, mais je pense que les fonctions telles que votre compose sont de trop bas niveau des concepts .NET-library point de vue. Vous pouvez exposer comme des méthodes de travail avec Func et Action, mais ce n'est probablement pas la façon dont vous design un la normale .NET-library en premier lieu (peut-être que vous souhaitez utiliser le Générateur de modèle à la place ou quelque chose comme ça).

Dans certains cas (c'est à dire lors de la conception de bibliothèques numériques qui ne sont pas vraiment adaptées à l' .NET-library style), il fait un bon sens de la conception d'une bibliothèque qui mêle à la fois F# et .NET styles dans une seule bibliothèque. La meilleure façon de le faire est d'avoir la normale F# (ou normal .NETTE) de l'API et de fournir ensuite des wrappers pour usage naturel dans l'autre style. L'emballage peut être dans un autre espace de noms (comme MyLibrary.FSharp et MyLibrary).

Dans votre exemple, vous pouvez laisser le F# mise en œuvre en MyLibrary.FSharp , puis ajouter .NET (C#) wrappers (similaire au code que Daniel posté) en MyLibrary de l'espace de noms comme méthode statique de la classe. Mais une fois de plus .NET-library aurait probablement plus spécifiques à l'API de la fonction de composition.

19voto

Daniel Points 29764

Vous n'avez qu'à envelopper la fonction valeurs (partiellement appliquée, fonctions, 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. Est-ce à éliminer vos préoccupations? Je pense que les solutions que vous proposez sont trop compliquées. Vous pouvez écrire la totalité de votre bibliothèque en F#, et de l'utiliser sans douleur de F# et C# (je le fais tout le temps).

Aussi, F# est plus souple que le C# en termes d'interopérabilité, de sorte qu'il est généralement préférable de suivre traditionnel .Style NET quand c'est un sujet de préoccupation.

MODIFIER

Le travail nécessaire pour faire deux interfaces publiques de séparer les espaces de noms, je pense, n'est justifiée lorsqu'ils sont complémentaires ou F# fonctionnalité n'est pas utilisable à partir de C# (tels que les fonctions inline, qui dépend de F#métadonnées spécifiques).

La prise de vos points:

  1. Module + let liaisons et de constructeur de moins de type + de membres statiques apparaissent exactement la même chose en C#, donc aller avec des modules si vous le pouvez. Vous pouvez utiliser CompiledNameAttribute "pour donner C#-les noms conviviaux.

  2. J'ai peut-être tort, mais ma conjecture est que le Composant lignes Directrices ont été rédigées avant l' System.Tuple ajoutés au cadre. (Dans les versions antérieures F# défini son propre tuple de type.) Il est depuis devenu plus acceptable d'utiliser des Tuple dans une interface publique pour trivial types.

  3. C'est là que je pense que vous devez faire les choses en C# chemin car F# joue bien avec Task mais le C# n'a pas bien jouer avec Async. Vous pouvez utiliser asynchrone en interne puis appelez Async.StartAsTask avant de revenir à partir d'une méthode publique.

  4. Embrasser null peut-être le seul gros inconvénient lors du développement d'une API pour l'utilisation de C#. Dans le passé, j'ai essayé toutes sortes de astuces pour éviter de considérer la valeur null en interne code F# mais, en fin de compte, c'était le mieux pour marquer les types avec les constructeurs publics avec [<AllowNullLiteral>] et de vérifier args null. C'est pas pire que C# à cet égard.

  5. Victimes de syndicats sont généralement compilées pour les hiérarchies de classe, mais toujours relativement sympathique représentation en C#. Je dirais, de les marquer avec [<AllowNullLiteral>] et de les utiliser.

  6. Fonctions curryfiées produire de la fonction de valeurs, qui ne devrait pas être utilisé.

  7. J'ai trouvé que c'était mieux pour embrasser null que de dépendre d'être pris à l'interface publique et de l'ignorer en interne. YMMV.

  8. Il fait beaucoup de sens d'utiliser list/map/set en interne. Ils peuvent tous être exposés par l'interface publique en tant que IEnumerable<_>. Aussi, seq, dict, et Seq.readonly sont souvent utiles.

  9. Voir #6.

La stratégie qui vous prenez dépend du type et de la taille de votre bibliothèque, mais, dans mon expérience, trouver le sweet spot entre fa# et C# demandé moins de travail à long terme-que la création de séparer les Api.

2voto

gradbot Points 9219

Projet F# Composant Les Lignes Directrices De Conception (Août 2010)

"
Vue d'ensemble
Ce document examine certaines des questions liées à la F n ° composant de conception et de codage. En particulier, il couvre:

  • Lignes directrices pour la conception de la "vanille" .Bibliothèques NET pour l'utilisation de tout .NET de la langue.
  • Lignes directrices pour F#-F# bibliothèques et F# la mise en œuvre du code.
  • Des Suggestions sur des conventions de codage pour F# la mise en œuvre du code

"

2voto

ShdNx Points 1840

Bien qu'il serait sans doute trop, vous pourriez envisager l'écriture d'une application à l'aide de Mono.Cecil (c'est génial de soutien à la liste de diffusion) qui permettrait d'automatiser la conversion sur le IL niveau. Par exemple, vous mettez en œuvre votre assemblée en F#, à l'aide de la F#public de style API, l'outil permettrait de générer un C#-friendly wrapper sur elle.

Par exemple, en F#, vous serait évidemment option<'T> (None, plus précisément) au lieu d'utiliser null comme en C#. Écrire un wrapper générateur pour ce scénario devrait être assez facile: la méthode wrapper invoquer la méthode originale: si c'est de la valeur de retour a été Some x, puis de retour x, sinon retour null.

Vous devez gérer le cas lors de l' T est un type de valeur, c'est à dire non nullable; vous devez envelopper la valeur de retour de la méthode wrapper en Nullable<T>, ce qui la rend un peu douloureux.

Encore une fois, je suis tout à fait certain qu'il aurait à payer pour écrire un tel outil dans votre scénario, peut-être, sauf si vous allez travailler sur cette bibliothèque (utilisable de façon transparente à partir de F# et C# fois) régulièrement. En tout cas, je pense que ce serait une expérience intéressante, que je pourrait même découvrir de temps en temps.

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