Не удалось вывести MonadReader в обертке ReaderT

Следуя и адаптируя эту запись в блоге, я пытался создайте решение, которое должно позволять тестировать функцию, которая читает env vars (используя System.Environment.lookupEnv).

Таким образом, я должен иметь возможность внедрить искусственную среду для тестов, которые можно будет прочитать вместо выполнения фактического действия ввода-вывода.

Однако проверка типа завершается ошибкой при попытке чтения файла env.

{-# LANGUAGE GeneralisedNewtypeDeriving #-}

...
import           RIO.Map (Map)
import qualified RIO.Map as Map
...
import qualified System.Environment as E (lookupEnv)
...

newtype MockEnv m a = MockEnv
  { mockEnv :: ReaderT (Map String String) m a
  } deriving (Applicative, Functor, Monad, MonadTrans)

runMockEnv :: MockEnv m a -> Map String String -> m a
runMockEnv (MockEnv e) = runReaderT e

class Monad m => MonadEnv m where
  lookupEnv :: String -> m (Maybe String)

instance MonadEnv IO where
  lookupEnv = E.lookupEnv

instance Monad m => MonadEnv (MockEnv m) where
  lookupEnv k = Map.lookup k <$> ask
                              -- ^^^ error occurs here

На сайте аск выше выдает следующую ошибку:

/home/[REDACTED].hs:45:34: error:
    • Could not deduce (MonadReader (Map String String) (MockEnv m))
        arising from a use of ‘ask’
      from the context: Monad m
        bound by the instance declaration
        at [REDACTED].hs:44:10-40
    • In the second argument of ‘(<$>)’, namely ‘ask’
      In the expression: Map.lookup k <$> ask
      In an equation for ‘lookupEnv’: lookupEnv k = Map.lookup k <$> ask
   |
45 |   lookupEnv k = Map.lookup k <$> ask
   |                                  ^^^


--  While building package [REDACTED]

Пожалуйста, не могли бы вы помочь мне понять, почему это не может проверить тип, и что мне нужно сделать, чтобы это исправить? Заранее спасибо.


person notquiteamonad    schedule 30.01.2021    source источник


Ответы (1)


Типы не похожи друг на друга. У нас есть:

lookupEnv :: String -> MockEnv m (Maybe String)
k :: String
ask :: MonadReader r m => m r
Map.lookup :: Map.lookup :: Ord k => k -> Map k a -> Maybe a
Map.lookup k :: Map String a -> Maybe a

Итак, все это означает, что нам нужно, чтобы бит, где у вас сейчас есть ask, имел тип MockEnv m (Map String a). Самое простое решение — обернуть ask вашей оберткой MockEnv newtype. Например, следующие работы:

  lookupEnv k = Map.lookup k <$> MockEnv ask

Более надежное решение (и то, на которое GHC намекает, что вам нужен экземпляр MonadReader) состоит в том, чтобы позволить MockEnv m быть экземпляром MonadReader:

instance Monad m => MonadReader (Map String String) (MockEnv m) where
  ask = MockEnv ask
  local f (MockEnv r) = MockEnv (local f r)

С этим экземпляром ваше определение экземпляра для MonadEnv (MockEnv m) работает нормально.

person DDub    schedule 30.01.2021
comment
Спасибо, это очень полезно, и первое предложение решает проблему. В какой-то момент я попытался написать экземпляр MonadReader, как вы предложили, но обнаружил, что он не выполняет проверку типов - Illegal instance declaration for ‘MonadReader (Map String String) (MockEnv m)’ (All instance types must be of the form (T a1 ... an) where a1 ... an are *distinct type variables*, and each type variable appears at most once in the instance head. Знаете, почему это так? Он рекомендует использовать гибкие экземпляры, но они все равно не будут компилироваться (та же ошибка) - person notquiteamonad; 30.01.2021
comment
На самом деле, это работает и с MultiParamTypeClasses. Я немного почитаю, чтобы убедиться, что понимаю, что я делаю, используя эти расширения. Спасибо за вашу помощь :) - person notquiteamonad; 30.01.2021