2017-01-15 4 views
5

Je suis en train d'appliquer le modèle de monade libre comme décrit dans F# for fun and profit pour mettre en œuvre l'accès aux données (pour Microsoft Azure Table Storage)Monad gratuit en F # avec le type de sortie générique

Exemple

Supposons que nous avons trois tables de base de données et trois dao foo, bar, Baz:

Foo   Bar   Baz 

key | col key | col key | col 
--------- --------- --------- 
foo | 1  bar | 2   | 

Je veux sélectionner foo avec key = "toto" et un bar avec key = "bar" pour insérer un Baz avec key = "baz" et col = 3

Select<Foo> ("foo", fun foo -> Done foo) 
    >>= (fun foo -> Select<Bar> ("bar", fun bar -> Done bar) 
    >>= (fun bar -> Insert<Baz> ((Baz ("baz", foo.col + bar.col), fun() -> Done())))) 

Au sein de la fonction interprète

  • Select résultats dans un appel de fonction qui prend une key : string et retourne un obj
  • Insert résultats dans un appel de fonction qui prend un obj et retourne unit

Pro Blem

je définissais deux opérations Select et Insert en plus de Done mettre fin au calcul:

type StoreOp<'T> = 
    | Select of string * ('T -> StoreOp<'T>) 
    | Insert of 'T * (unit -> StoreOp<'T>) 
    | Done of 'T 

Pour chaîne StoreOp de Je suis en train de mettre en œuvre la fonction de liaison correcte:

let rec bindOp (f : 'T1 -> StoreOp<'T2>) (op : StoreOp<'T1>) : StoreOp<'T2> = 
    match op with 
    | Select (k, next) -> 
     Select (k, fun v -> bindOp f (next v)) 
    | Insert (v, next) -> 
     Insert (v, fun() -> bindOp f (next())) 
    | Done t -> 
     f t 

    let (>>=) = bindOp 

Cependant, le compilateur f # me prévient correctement que:

The type variable 'T1 has been constrained to be type 'T2 

Pour cette mise en œuvre de bindOp le type est fixé tout au long du calcul, donc au lieu de:

Foo > Bar > unit 

tout ce que je peux exprimer est:

Foo > Foo > Foo 

Comment dois-je modifier la définition de StoreOp et/ou bindOp pour travailler avec différents types tout au long du calcul?

+2

Je peux vous indiquer la raison exacte de cette erreur dans votre code 'bindOp', mais la raison racine est votre type' StoreOp'. Si vous le regardez de près, vous verrez qu'il ne peut qu'exprimer des chaînes d'opérations sur le même type. –

+1

Ne serait-il pas possible d'éviter tous ces niveaux d'indirection et de faire les choses simples de CRUD dans quelque chose comme un [Script de Transaction] (https://martinfowler.com/eaaCatalog/transactionScript.html)? C'est similaire à ce que Tomas Petricek décrit dans le dernier paragraphe de sa [réponse] (http://stackoverflow.com/a/41668459/467754). Voir aussi [Pourquoi la Monad gratuite n'est pas gratuite] (https://www.youtube.com/watch?v=U0lK0hnbc4U). –

+0

L'implémentation actuelle est un simple ensemble de fonctions CRUD impératives. S'il vous plaît voir le commentaire ci-dessous pour la motivation. – dtornow

Répondre

4

Comme Fyodor mentionné dans les commentaires, le problème est avec la déclaration de type. Si vous vouliez faire compiler au prix de sacrifier la sécurité de type, vous pouvez utiliser obj en deux endroits - ce montre au moins où le problème est le suivant:

type StoreOp<'T> = 
    | Select of string * (obj -> StoreOp<'T>) 
    | Insert of obj * (unit -> StoreOp<'T>) 
    | Done of 'T 

Je ne suis pas tout à fait sûr de ce que les deux opérations sont censé modéliser - mais je suppose que Select signifie que vous lisez quelque chose (avec string clé?) et Insert signifie que vous stockez une certaine valeur (et continuez avec unit). Donc, ici, les données que vous stockez/lisez seraient obj.

Il existe des façons de sécuriser ce type, mais je pense que vous obtiendriez une meilleure réponse si vous expliquiez ce que vous essayez d'obtenir en utilisant la structure monadique. Sans en savoir plus, je pense que l'utilisation de monades gratuites ne fera que rendre votre code très compliqué et difficile à comprendre. F # est un fonctionnel en premier langage, ce qui signifie que vous pouvez écrire des transformations de données dans un style fonctionnel agréable en utilisant des types de données immuables et utiliser la programmation impérative pour charger vos données et stocker vos résultats. Si vous travaillez avec le stockage de table, pourquoi ne pas simplement écrire le code impératif normal pour lire les données de la table de stockage, passer les résultats à une pure transformation fonctionnelle, puis stocker les résultats?

+1

Merci pour votre réponse. Pour répondre à votre question: J'essaie de séparer le code pur et impur comme expliqué dans [Pureté dans un langage impur] (http://blog.leifbattermann.de/2016/12/25/purity-in-an-impure-language -free-monad-tic-tac-toe-cqrs-événement-acidification /). Vous avez mentionné qu'il existe des moyens de sécuriser le type «solution obj». Pourriez-vous partager l'approche que vous adopteriez pour faire cela? – dtornow

+2

Je suis d'accord qu'il est souhaitable de séparer le code pur et impur, mais la monade libre est une façon terrible de le faire. En fin de compte, le code sera impur de toute façon - ce que la monade libre vous permet de faire est de faire abstraction de la façon dont l'impureté est traitée - et je pense qu'il n'y a aucun avantage à cela. (Vous pourriez argumenter que c'est utile pour les tests, mais je pense que cela ne fait qu'obscurcir les tests sur la partie clé que vous devriez tester et sur les opérations sur les données.) S'il y a quelque chose que vous voulez réaliser, alors il y a façon de le faire. –

+0

Comme pour une version de type sécurité, vous obtiendrez un type qui suit statiquement les types de toutes les lectures et écritures telles que M >>>> >> 'Un exemple de projet qui utilise ceci est: http://blumu.github.io/ResumableMonad/ –