2013-04-17 15 views
14

Questa è una domanda relativa alle pratiche di progettazione dell'API per la definizione delle proprie istanze Monad per le librerie Haskell. Definire le istanze di Monad sembra essere un buon modo per isolare DSL, ad es. Par monade in monad-par, hdph; Process in processo distribuito; Eval in parallelo ecc ...Quando (e quando no) per definire una Monad

Prendo due esempi di librerie haskell, il cui scopo è l'IO con i backend del database. Gli esempi che prendo sono riak per Riak IO e hedis per Redis IO.

In hedis, a Redis monade . Da lì, si esegue IO con Redis come:

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, le cose sono diverse:

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

example = do 
    conn <- connect defaultClient 
    ping conn 

La documentazione per runRedis dice: "Ogni chiamata di runRedis prende una connessione di rete dalla connessione pool ed esegue l'azione Redis fornita. Le chiamate a runRedis possono quindi bloccarsi mentre tutte le connessioni dal pool sono in uso. ". Tuttavia, il pacchetto riak implementa anche i pool di connessione. Questo viene fatto senza istanze monade supplementari sulla parte superiore della monade 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 

Così, l'analogia tra i due pacchetti si riduce a queste due funzioni:

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

Per quanto posso dire, il pacchetto hedis introduce una monad Redis per incapsulare azioni IO con redis usando runRedis. Al contrario, il pacchetto riak in withConnection prende semplicemente una funzione che prende uno Connection e lo esegue nella monade IO.

Quindi, quali sono le motivazioni per la definizione delle istanze Monad e degli stack Monad? Perché i pacchetti riak e redis differiscono nel loro approccio a questo?

+6

Come contesto per le risposte - nel caso non sia ovvio, i tipi 'Redis a' e' Connection -> IO a' sono approssimativamente equivalenti. Quindi questa è essenzialmente una differenza estetica, paragonabile a 'env -> IO a' vs.' ReaderT env IO a'. –

+0

Quindi ciò significa che forse nessuno dei due è corretto e 'Codensity IO Connection' era la monade che voleva da sempre. –

risposta

10

Per me si tratta di incapsulamento e protezione degli utenti da future modifiche di implementazione. Come ha fatto notare Casey, questi due sono all'incirca equivalenti in questo momento - fondamentalmente una monade Reader Connection. Ma immagina come si comportano questi soggetti soggetti a cambiamenti incerti lungo la strada. Cosa succede se entrambi i pacchetti decidono che l'utente ha bisogno di un'interfaccia monad di stato invece di un lettore? Se ciò accade, la funzione di Riak withConnection si trasformerà in una firma di tipo come questo:

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

Ciò richiederà cambiamenti radicali al codice utente. Ma il pacchetto Redis potrebbe portare a un simile cambiamento senza rompere i suoi utenti.

Ora, si potrebbe sostenere che questo scenario ipotetico è molto irrealistico e non è qualcosa che è necessario pianificare. E in questi due casi particolari, potrebbe essere vero. Ma tutti i progetti si evolvono nel tempo e spesso in modi imprevisti. La definizione della propria monade consente di nascondere i dettagli di implementazione interni ai propri utenti e di fornire un'interfaccia più stabile attraverso le modifiche future.

Quando indicato in questo modo, alcuni potrebbero concludere che la definizione della propria monade è l'approccio superiore. Ma non penso che sia sempre così. (La libreria lens mi viene in mente come un contro-esempio potenzialmente valido.) Definire una nuova monade ha dei costi. Se stai usando i trasformatori monad, può imporre una penalità sulle prestazioni.In altri casi l'API potrebbe finire per essere più prolissa. Haskell è molto utile per mantenere la sintassi molto minimale e, in questo caso particolare, la differenza non è molto grande, probabilmente alcuni liftIO per i redis e alcuni lambda per riak.

La progettazione del software viene raramente tagliata e asciugata. È raro che tu possa dire con sicurezza quando e quando non definire la tua monade. Ma possiamo prendere coscienza dei compromessi coinvolti per aiutare la nostra valutazione delle singole situazioni nel momento in cui le incontriamo.

1

In questo caso penso che implementare la monade sia stato un errore. Gli sviluppatori di java stanno implementando tutti i tipi di modelli di design solo per il gusto di averli.

hdbc, ad esempio, funziona anche in semplice monade IO.

Monad per la libreria redis non porta nulla di utile. L'unica cosa che ottiene è sbarazzarsi dell'argomento di una funzione (connessione). Ma paghi per aver sollevato ogni operazione di I/O mentre è in redis monad.

anche se mai bisogno di lavorare con 2 basi di dati Redis ora vuoi un momento difficile cercando di capire quali operazioni per alzare dove :)

L'unica ragione per implementare una monade è quello di creare un nuovo DSL . Come vedi hedis non ha creato una nuova DSL. Le sue operazioni sono esattamente come qualsiasi altra libreria di database. Quindi la monade in hedis è superficiale e non è giustificata.