• ЖЖ code Haskell ;; ээ
    Драйвер редиса возвращает `Redis (Either Reply a)`. При операции `HGET` возвращает, соответственно, `Redis (Either Reply (Maybe ByteString))`, типа значения по ключу может и не быть. Программист для упрощения пишет сначала специальную функцию `redis'`, которая заворачивает ответ редиса в EitherT-based монаду, чтоб соединять подобные вычисления и возвращать первую неудачу:
    
    ```
    newtype Redis' a = Redis' (EitherT Redis.Reply Redis.Redis a) deriving (MonadIO)
    instance Monad Redis' where
        return = Redis' . EitherT . return . Right
        (Redis' m) >>= f = Redis' $ m >>= \rv -> unwrapRedis' $ f rv
    redis' = Redis' . EitherT
    ```
    
    Таким образом, теперь, если вы получаете ответ `Redis (Either Reply a)`, вы его преобразуете в
    
    ```
    Redis' (EitherT Reply Redis a)
    ```
    
    и можете соединять подобные вычисления в do-блоке типа:
    
    ```
    do
        res <- redis' $ Redis.hget foo bar
        res2 <- redis' $ Redis.hget baz zab
    ```
    
    и вычисление остановится на первом возврате ошибки.
    
    Далее. Программисту необходимо по кучке значений сделать `HGET` и вернуть это как-то внутри кортежа, поскольку мы внутри новой монады `Redis'` -- завернуть результат в неё. В случае, если значения хоть по одному ключу не существует, хочется вернуть `Nothing` для всех. Потому создаётся новая монада:
    
    ```
    newtype HashFields a = HashFields (MaybeT Redis' a)
        deriving (Functor, Monad)
    instance Applicative HashFields where
        pure = return
        (<*>) = ap
    ```
    
    описывающая вычисления типа `Redis'`, которые могут вернуть неудачу. Пишется новая функция
    
    ```
    hashField = HashFields . MaybeT . redis'
    ```
    
    способная завернуть результат неудачи в новую монаду, которая умеет останавливаться на первой неудаче. Также пишется специальная функция для `HGET`:
    
    ```
    getField :: ByteString -- ^ Key
             -> ByteString -- ^ Hash field name
             -> HashFields String
    getField key field = fmap toString $ hashField $ Redis.hget key field
    ```
    
    Также напишем функцию, которая "запустит" наше вычисление:
    
    ```
    getRedisFields :: forall a. HashFields a -> Redis' (Maybe a)
    getRedisFields (HashFields f) = runMaybeT f
    ```
    
    И теперь лёгким движением руки мы можем сделать что-то вроде:
    
    ```
    getRedisFields ((,,,,,) <$>
        getField k "foo" <*>
        getField k "bar" <*>
        getField k "baz" <*>
        getField k "zab" <*>
        getField k "rab" <*>
        getField k "oof")
    ```
    
    Вопрос: вам не кажется это "слишком"? Я пока еще не настолько просто манипулирую типами в голове, чтоб ощутить всю ситуацию, хорошо хоть в целом могу медленно прости по шагам по коду, но есть ощущение, что что-то здесь не так.

Replies (11)

  • @kb, Одной монады хватит
    data MyMonad a = Result a | Reply Reply | Nothing
  • @rkit, ну, редис и так это делает в виде монады

    data Redis = Redis (Either Reply a)
  • @kb, Redis (Either e a) ~ EitherT e Redis a

    Redis (Either e (Maybe a) ~ EitherT e Redis (Maybe a) ~ MaybeT (EitherT e Redis) a

    Все bind имеют нужный смысл — прерывание на первой же ошибке. Всё нормально изящно и идиоматично.

    ИМХО newtype обёртка над EitherT не нужна
  • @kb, К чему было написана вся эта стена текста, если редис и так делает это?
  • @rkit, Я не понял цель вашего комментария про "одной монады хватит". У редиса как раз и есть одна монада, умеющая выражать всё то, что выражает описанная вами.
  • @kb, Не знаю, где нашелся редис с такой монадой. Ни на на хекедже, ни в посте таких нет.
    Вместо Redis', MaybeT Redis', и всей этой еботы с getRedisFields достаточно сделать одну монаду, примерно описанную мной, прописать для нее соответствующие инстансы, и выполнять в ней все операции.
  • @rkit, Вот текущий редис (в последней версии он немного другой, но суть примерно та же):

    hackage.haskell.org

    Вот операция hget:

    hackage.haskell.org
  • @kb, data Redis = Redis (Either Reply a)
    newtype Redis a = Redis (ReaderT RedisEnv IO a)
    суть примерно та же
    Дальнейший разговор продолжать не имею смысла.
  • @rkit, да, за несоответствие типа извиняюсь. Про "суть примерно та же" — имелось в виду с последней версией библиотеки hedis, а не с типом, который я по ошибке привёл.
  • @rkit, В вашем типа я, например, не очень понимаю, какой смысл в Nothing при операции, которая Nothing вернуть не может. Например, EXISTS.
  • @kb, s/типа/типе/