2013-04-17 8 views
14

Dies ist eine Frage in Bezug auf die API-Design-Praktiken zum Definieren eigener Monad-Instanzen für Haskell-Bibliotheken. Definition von Monad-Instanzen scheint eine gute Möglichkeit zu sein, DSLs z.B. Par monad in monad-par, hdph; Process im verteilten Prozess; Eval parallel usw.Wann (und wenn nicht) eine Monade definieren

Ich nehme zwei Beispiele für Haskell-Bibliotheken, deren Zweck es ist, IO mit Datenbank-Backends zu IO. Die Beispiele, die ich nehme, sind riak für Riak IO und hedis für Redis IO.

In hedis, ein Redis monad . Von dort laufen Sie IO mit redis als:

data Redis a -- instance Monad Redis 
runRedis :: Connection -> Redis a -> IO a 
class Monad m => MonadRedis m 
class MonadRedis m => RedisCtx m f | m -> f 
set :: RedisCtx m f => ByteString -> ByteString -> m (f Status) 

example = do 
    conn <- connect defaultConnectInfo 
    runRedis conn $ do 
    set "hello" "world" 
    world <- get "hello" 
    liftIO $ print world 

In Riak, sind die Dinge anders:

create :: Client -> Int -> NominalDiffTime -> Int -> IO Pool 
ping :: Connection -> IO() 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

example = do 
    conn <- connect defaultClient 
    ping conn 

Die Dokumentation für runRedis sagt: „Jeder Aufruf von runRedis eine Netzwerkverbindung aus der Verbindung erfolgt pool und führt die angegebene Redis-Aktion aus. Aufrufe von runRedis können daher blockieren, während alle Verbindungen aus dem Pool verwendet werden. ". Das Riak-Paket implementiert jedoch auch Verbindungspools. Dies wird ohne zusätzliche Monade Instanzen auf dem IO Monade getan:

create :: Client-> Int -> NominalDiffTime -> Int -> IO Pool 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

exampleWithPool = do 
    pool <- create defaultClient 1 0.5 1 
    withConnection pool $ \conn -> ping conn 

So ist die Analogie zwischen den beiden Paketen läuft darauf hinaus, diese beiden Funktionen:

runRedis  :: Connection -> Redis a -> IO a 
withConnection :: Pool -> (Connection -> IO a) -> IO a 

Soweit ich das beurteilen kann, Das Paket hedis führt eine Monade Redis ein, um IO-Aktionen mit Redis unter Verwendung von runRedis zu kapseln. Im Gegensatz dazu nimmt das Riak-Paket in withConnection einfach eine Funktion an, die eine Connection nimmt, und führt sie in der IO-Monade aus.

Also, was sind die Gründe für die Definition Ihrer eigenen Monad Instanzen und Monad Stacks? Warum unterscheiden sich die Riak- und Redis-Pakete in ihrem Ansatz?

+6

Als Kontext für Antworten - falls es nicht offensichtlich ist, sind die Typen 'Redis a' und' Connection -> IO a' ungefähr gleichwertig. Dies ist also im Wesentlichen ein kosmetischer Unterschied, vergleichbar mit "env -> IO a" gegenüber "ReaderT env IO a". –

+0

Dann heißt das vielleicht, dass keiner von beiden richtig ist und "Codesity IO Connection" die Monade war, die er die ganze Zeit haben wollte. –

Antwort

10

Für mich dreht sich alles um Kapselung und Schutz der Benutzer vor zukünftigen Implementierungsänderungen. Wie Casey betont hat, sind diese beiden in etwa gleich - im Grunde eine Reader Connection Monade. Aber stellen Sie sich vor, wie sich diese unter unsicheren Veränderungen verhalten werden. Was ist, wenn beide Pakete entscheiden, dass der Benutzer eine State-Monad-Schnittstelle anstelle eines Lesegeräts benötigt? Wenn das passiert, ist Riak withConnection Funktion wird so zu einer Art Signatur ändern:

withConnection :: Pool -> (Connection -> IO (a, Connection)) -> IO a 

Dies wird umfassende Änderungen an Benutzercode erforderlich ist. Aber das Redis-Paket könnte eine solche Veränderung vollziehen, ohne seine Benutzer zu brechen.

Nun könnte man argumentieren, dass dieses hypothetische Szenario ist sehr unrealistisch und nicht etwas, was Sie planen müssen. Und in diesen beiden Fällen mag das stimmen. Aber alle Projekte entwickeln sich im Laufe der Zeit und oft in unvorhergesehener Weise. Indem Sie Ihre eigene Monade definieren, können Sie interne Implementierungsdetails von Ihren Benutzern verbergen und eine Schnittstelle bereitstellen, die durch zukünftige Änderungen stabiler ist.

Wenn dies so angegeben wird, könnten einige zu dem Schluss kommen, dass die Definition Ihrer eigenen Monade der überlegene Ansatz ist. Aber ich denke nicht, dass das immer so ist. (Die Bibliothek lens kommt mir als ein potenziell gutes Gegenbeispiel in den Sinn.) Das Definieren einer neuen Monade hat Kosten. Wenn Sie Monad-Transformer verwenden, kann dies zu einer Leistungseinbuße führen.In anderen Fällen ist die API möglicherweise etwas ausführlicher. Haskell ist sehr gut, damit Sie die Syntax sehr klein halten und in diesem speziellen Fall ist der Unterschied nicht sehr groß - wahrscheinlich ein paar liftIO für redis und ein paar lambdas für riak.

Softwaredesign wird selten geschnitten und getrocknet. Es ist selten, dass Sie sicher sagen können, wann und wann Sie Ihre eigene Monade nicht definieren. Wir können uns jedoch der damit verbundenen Kompromisse bewusst werden, die uns bei der Beurteilung individueller Situationen helfen, wenn wir ihnen begegnen.

1

In diesem Fall denke ich, die Implementierung von Monad war ein Fehler. Es ist ein Java-Entwickler, der alle Arten von Design-Mustern implementiert, nur um sie zu haben.

hdbc zum Beispiel funktioniert auch in Plain IO Monad.

Monad für Redis-Bibliothek bringt nichts nützliches. Das einzige, was es erreicht, ist, ein Funktionsargument (Verbindung) loszuwerden. Aber Sie zahlen dafür, dass Sie jede IO-Operation innerhalb von redis monad aufheben.

Auch wenn Sie jemals mit 2 redis Datenbanken arbeiten müssen jetzt wurde du eine harte Zeit versucht, welche Operationen, um herauszufinden, zu heben, wo :)

Der einzige Grund, eine Monade zu implementieren, ist ein neues DSL zu erstellen . Wie Sie sehen, hat hedis kein neues DSL erstellt. Seine Operationen sind genau wie jede andere Datenbankbibliothek. Daher ist Monade in Hedis oberflächlich und nicht gerechtfertigt.

Verwandte Themen