2017-01-15 1 views
5

Ich versuche, das frei Monade Muster anwenden, wie in F# for fun and profit beschriebenen Datenzugriff zu implementieren (für Microsoft Azure Table Storage)Freie Monad in F # mit generischen Ausgabetyp

Beispiel

Nehmen wir an, wir haben drei Datenbanktabellen und drei DAOs foo, Bar, Baz:

Foo   Bar   Baz 

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

ich mit key = "foo" foo auswählen möchten und Bar mit key = "bar" ein Baz einfügen mit key = "baz" und 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())))) 

In Interpreter-Funktion

  • Select führt zu einem Funktionsaufruf, der ein key : string und gibt ein obj
  • Insert Ergebnisse in einem Funktionsaufruf erfolgt, die ein obj nimmt und unit

Pro blem

I definiert zwei Operationen Select und Insert neben Done die Berechnung zu beenden:

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

Um Kette StoreOp die ich versuche, die richtige bind-Funktion zu implementieren:

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 

Allerdings warnt mich der f # -Compiler korrekt:

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

Für diese Implementierung von bindOp wird die Art in der gesamten Berechnung festgelegt, so statt:

Foo > Bar > unit 

alles, was ich ausdrücken kann, ist:

Foo > Foo > Foo 

Wie soll ich die Definition von StoreOp modifizieren und/oder bindOp, um mit verschiedenen Typen während der Berechnung zu arbeiten?

+2

Ich kann Ihnen den genauen Grund für diesen Fehler in Ihrem 'bindOp' Code zeigen, aber der Grund dafür ist Ihr' StoreOp' Typ. Wenn Sie es genau betrachten, werden Sie sehen, dass es immer nur Operationsketten desselben Typs ausdrücken kann. –

+1

Wäre es nicht möglich, alle diese Ebenen der Umleitung zu vermeiden und die einfachen CRUD-Sachen in etwas wie einem [Transaction Script] (https://martinfowler.com/eaaCatalog/transactionScript.html) zu tun? Das ist ähnlich dem, was Tomas Petricek im letzten Absatz seiner Antwort beschreibt (http://stackoverflow.com/a/41668459/467754). Siehe auch [Warum die kostenlose Monade nicht kostenlos ist] (https://www.youtube.com/watch?v=U0lK0hnbc4U). –

+0

Die aktuelle Implementierung ist ein einfacher Satz von imperativen CRUD-Funktionen. Bitte beachten Sie den Kommentar zur Motivation. – dtornow

Antwort

4

Wie Fyodor in den Kommentaren erwähnt, ist das Problem mit der Typdeklaration. Wenn Sie es kompilieren um den Preis zu opfern Typsicherheit machen wollte, Sie obj an zwei Stellen verwenden könnte - das zumindest zeigt, wo das Problem ist:

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

Ich bin nicht ganz sicher, was die beiden Operationen sind soll modellieren - aber ich denke, Select bedeutet, dass Sie etwas lesen (mit string Schlüssel?) und Insert bedeutet, dass Sie einen Wert speichern (und dann weiter mit unit). Also, hier wären die Daten, die Sie speichern/lesen, obj.

Es gibt Möglichkeiten, diesen Typ sicher zu machen, aber ich denke, du würdest besser antworten, wenn du erklärst, was du mit der monadischen Struktur erreichen willst.

Ohne mehr zu wissen, ich denke, die Verwendung von freien Monaden wird nur Ihren Code sehr chaotisch und schwer zu verstehen. F # ist eine Funktional-first Sprache, was bedeutet, dass Sie Datentransformationen in einem netten funktionalen Stil mit unveränderlichen Datentypen schreiben und imperative Programmierung verwenden können, um Ihre Daten zu laden und Ihre Ergebnisse zu speichern. Wenn Sie mit dem Tabellenspeicher arbeiten, schreiben Sie einfach den normalen Imperativcode, um Daten aus dem Tabellenspeicher zu lesen, leiten Sie die Ergebnisse an eine reine funktionale Transformation weiter und speichern Sie dann die Ergebnisse?

+1

Vielen Dank für Ihre Antwort. Um Ihre Frage zu beantworten: Ich versuche, reinen und unreinen Code zu trennen, wie in [Reinheit in einer unreinen Sprache] erklärt (http://blog.leifbattermann.de/2016/12/25/purity-in-an-imure-language -free-monad-tic-tac-toe-cqrs-Ereignis-Säuerung /). Sie haben erwähnt, dass es Möglichkeiten gibt, den Typ "Obj-Lösung" sicher zu machen. Könnten Sie den Ansatz teilen, den Sie dafür machen würden? – dtornow

+2

Ich stimme zu, dass die Trennung von reinem und unreinem Code wünschenswert ist, aber die freie Monade ist eine schreckliche Art, dies zu tun. Am Ende wird der Code sowieso unrein sein - was die freie Monade Ihnen erlaubt, ist zu abstrahieren, wie genau die Unreinheit gehandhabt wird - und ich denke, darin liegt kein Nutzen. (Sie könnten argumentieren, dass das für Testzwecke nützlich ist, aber ich denke, es verschleiert nur das Testen des Schlüsselteils, den Sie testen sollten, und das sind die Operationen an den Daten.) Wenn es etwas Bestimmtes gibt, dann gibt es ein besseres Möglichkeit, das zu tun. –

+0

Wie für eine typsichere Version, würden Sie mit einem Typ enden, der die Typen aller Lese- und Schreibvorgänge statisch verfolgt. M >>>> >> 'Ein Beispielprojekt, das dies verwendet, ist: http://blumu.github.io/ResumableMonad/ –