2013-04-17 14 views
14

To jest pytanie dotyczące praktyk projektowania interfejsów API do definiowania własnych instancji Monad dla bibliotek Haskell. Zdefiniowanie instancji Monada wydaje się być dobrym sposobem na odizolowanie DSL, np. Par monada w monad-par, hdph; Process w procesie rozproszonym; Eval równolegle itp ...Kiedy (i kiedy nie) zdefiniować Monadę

Podejmuję dwa przykłady bibliotek haskell, których celem jest IO z backendami baz danych. Przykłady, które biorę, to riak dla Riak IO i hedis dla Redis IO.

W hedis, Redis monada is defined. Stamtąd uruchomić IO z REDiS jak:

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 

W riak, rzeczy są różne:

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

example = do 
    conn <- connect defaultClient 
    ping conn 

Dokumentacja runRedis mówi: „Każde wywołanie runRedis wykonuje połączenia sieciowego z połączenia puli i uruchamia daną akcję Redis, dlatego wywołania runRedis mogą zostać zablokowane, gdy wszystkie połączenia z puli są w użyciu. ". Jednak pakiet riak implementuje także pule połączeń. Odbywa się to bez dodatkowych przypadkach monady na górze monady IO:

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 

więc analogia pomiędzy tymi dwoma pakietami sprowadza się do tych dwóch funkcji:

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

O ile mogę powiedzieć, pakiet hedis wprowadza monadę Redis do enkapsulacji działań IO z redis za pomocą runRedis. W przeciwieństwie do tego pakiet riak w withConnection po prostu przyjmuje funkcję, która pobiera Connection i wykonuje ją w Monadzie IO.

Jakie są więc motywy do definiowania własnych instancji Monady i stosów Monadów? Dlaczego pakiety riak i redis różniły się podejściem do tego?

+6

Jako kontekst dla odpowiedzi - w przypadku, gdy nie jest to oczywiste, typy "Redis a" i "Connection -> IO a" są w przybliżeniu równoważne. Jest to w gruncie rzeczy różnica kosmetyczna, porównywalna z 'env -> IO a' vs.' ReaderT env IO a'. –

+0

To oznacza, że ​​być może żadna z nich nie jest poprawna, a "Połączenie gęstościowe IO" było monadą, której chciał od samego początku. –

Odpowiedz

10

Dla mnie chodzi o hermetyzację i ochronę użytkowników przed przyszłymi zmianami w implementacji. Jak zauważył Casey, te dwa są w przybliżeniu równoważne teraz - w zasadzie monada Reader Connection. Ale wyobraź sobie, jak będą się zachowywać pod wpływem niepewnych zmian na drodze. Co się stanie, jeśli oba pakiety ostatecznie zdecydują, że użytkownik potrzebuje interfejsu monad stanu zamiast czytnika? Jeśli tak się stanie, withConnection funkcja riak może ulec zmianie do podpisywania tego typu jak:

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

To będzie wymagać rozległe zmiany kodu użytkownika. Pakiet Redis może jednak wprowadzić taką zmianę, nie naruszając przy tym użytkowników.

Teraz można argumentować, że ten hipotetyczny scenariusz jest bardzo nierealistyczny i nie jest czymś, co trzeba zaplanować. I w tych dwóch szczególnych przypadkach może to być prawda. Jednak wszystkie projekty ewoluują w czasie i często w nieprzewidziany sposób. Definiowanie własnej monady pozwala ukryć wewnętrzne szczegóły implementacji od użytkowników i zapewnić interfejs, który jest bardziej stabilny dzięki przyszłym zmianom.

Po stwierdzeniu w ten sposób, niektórzy mogą stwierdzić, że definiowanie własnej monady jest nadrzędnym podejściem. Ale nie sądzę, że tak jest zawsze. (Biblioteka lens przypomina potencjalnie dobry przykład.) Definiowanie nowej monady wiąże się z kosztami. Jeśli używasz transformatorów monad, możesz nałożyć karę za wydajność.W innych przypadkach interfejs API może okazać się bardziej szczegółowy. Haskell jest bardzo dobry, pozwalając zachować składnię bardzo minimalną iw tym konkretnym przypadku różnica nie jest zbyt duża - prawdopodobnie kilka z nich to liftIO dla redis i kilka lambd dla riak.

Projektowanie oprogramowania jest rzadko cięte i suszone. Rzadko zdarza się, że będziesz w stanie śmiało powiedzieć, kiedy i kiedy nie zdefiniować swojej monady. Możemy jednak zdawać sobie sprawę z kompromisów, które pomagają nam ocenić indywidualne sytuacje, gdy je napotykamy.

1

W tym przypadku uważam, że wdrożenie monady było błędem. Występuje w programistów java, którzy wdrażają wszystkie rodzaje wzorców projektowych tylko ze względu na ich posiadanie.

Na przykład hdbc działa również w zwykłej monadzie IO.

Monada dla biblioteki redis nie przynosi niczego użytecznego. Jedyne, co osiąga, to pozbyć się jednego argumentu funkcji (połączenia). Ale płacisz za to, że podnosi każdą operację IO, podczas gdy wewnątrz monady Redis.

Również jeśli kiedykolwiek potrzebne do pracy z 2 baz danych Redis teraz będziesz mieć twardy czas próbuje dowiedzieć się, które operacje podnieść gdzie :)

Jedynym powodem do wdrożenia monady jest stworzenie nowego DSL . Jak widać hedis nie utworzył nowego DSL. Jego operacje są dokładnie takie, jak każda inna biblioteka baz danych. Dlatego monada in hedis jest powierzchowna i nie jest uzasadniona.

Powiązane problemy