Когда (и когда нет), чтобы определить Монаду

Это вопрос, связанный с практикой проектирования API для определения ваших собственных экземпляров Monad для библиотек Haskell. Определение экземпляров Monad кажется хорошим способом изолировать DSL, например. Par монада в monad-par, hdph; Process в распределенном процессе; Eval параллельно и т.д.

Я беру два примера библиотек haskell, целью которых является IO с базами данных. Примеры, которые я принимаю, riak для Riak IO и hedis для Redis IO.

В hedis определена a Redis monad . Оттуда вы запускаете IO с redis как:

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

В riak все по-другому:

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

example = do
  conn <- connect defaultClient
  ping conn

В документации для runRedis говорится: "Каждый вызов runRedis принимает сетевое соединение из пула соединений и запускает заданное действие Redis. Таким образом, вызовы runRedis могут блокироваться, пока все соединения из пула используются". Однако пакет riak также реализует пулы соединений. Это делается без дополнительных экземпляров монады поверх монады 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

Итак, аналогия между двумя пакетами сводится к двум следующим функциям:

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

Насколько я могу судить, пакет hedis представляет монаду Redis для инкапсуляции операций ввода-вывода с помощью redis с помощью runRedis. Напротив, пакет riak в withConnection просто принимает функцию, которая принимает Connection, и выполняет ее в монаде IO.

Итак, каковы мотивы для определения ваших собственных экземпляров Monad и стеков Monad? Почему пакеты riak и redis отличались своим подходом к этому?

Ответы

Ответ 1

Для меня все это касается инкапсуляции и защиты пользователей от будущих изменений в реализации. Как заметил Кейси, эти два являются примерно эквивалентными прямо сейчас - в основном монада Reader Connection. Но представьте, как они будут вести себя к неопределенным изменениям по дороге. Что делать, если оба пакета в конечном итоге решат, что пользователю нужен интерфейс монады штата вместо чтения? Если это произойдет, функция riak withConnection изменится на подпись типа следующим образом:

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

Это потребует радикальных изменений кода пользователя. Но пакет Redis может снять такое изменение, не нарушая его пользователей.

Теперь можно утверждать, что этот гипотетический сценарий очень нереалистичен, а не то, что вам нужно планировать. И в этих двух частных случаях это может быть правдой. Но все проекты развиваются со временем, а часто и непредвиденными способами. Определение собственной монады позволяет вам скрывать внутренние детали реализации от ваших пользователей и обеспечивать более стабильный интерфейс в будущих изменениях.

При этом говорится, что некоторые могут заключить, что определение вашей собственной монады - превосходный подход. Но я не думаю, что это всегда так. (Библиотека lens приходит в голову как потенциально хороший контрпример.) Определение новой монады имеет издержки. Если вы используете монадные трансформаторы, это может налагать штраф за производительность. В других случаях API может оказаться более подробным. Haskell очень хорош, позволяя сохранить синтаксис очень минимальным, и в этом конкретном случае разница не очень велика - возможно, несколько liftIO для redis и несколько лямбда для riak.

Разработка программного обеспечения редко разрезается и высушивается. Редко, что вы сможете с уверенностью сказать, когда и когда не определить свою собственную монаду. Но мы можем осознать компромиссы, которые помогут нам оценить отдельные ситуации, когда мы их встретим.

Ответ 2

В этом случае я считаю, что реализация монады была ошибкой. Это aki java-разработчики, реализующие все виды шаблонов проектирования только ради их использования.

hdbc, например, также работает в простой IO-монаде.

Монада для библиотеки redis не приносит ничего полезного. Единственное, чего он добивается, это избавиться от одного аргумента функции (соединения). Но вы платите за то, что он поднимает каждую операцию ввода-вывода, в то время как внутри redis monad.

Кроме того, если вам когда-либо понадобится работать с 2 базами данных redis, вам будет сложно найти, какие операции нужно снять, если:)

Единственная причина для реализации монады - создать новый DSL. Как вы видите, hedis не создал новую DSL. Его действия точно так же, как и любая другая библиотека базы данных. Поэтому монада в hedis является поверхностной и не оправдана.