使用 Monad 转换器避免孤儿实例

Avoiding Orphan Instances with Monad Transformers

我有与我的应用程序的独立功能相对应的 monad 转换器。

天气模块中:

class Monad m => WeatherT m where
  byCity :: String -> m WeatherData

newtype MockWeather m a = MockWeather { 
  ... 
} deriving (Functor, Applicative, Monad, MonadTrans)


instance Monad m => WeatherT (MockWeather m) where
  ...

计数器模块中:

class Monad m => CounterT m where
  increment :: m Int
  current :: m Int

newtype MockCounter m a = MockCounter {
  ...
} deriving (Functor, Applicative, Monad, MonadTrans)

instance Monad m => CounterT (MockCounter m) where
  ...

它们都可能有多个具有不同实现的实例,例如它们都有一个我在此处使用的模拟实例:MockCounterMockWeather.

主模块中,我将MyApp monad定义为:

newtype MyAppM m a = MyAppM { unMyAppM :: MockCounter (MockWeather m) a }
  deriving (Functor, Applicative, Monad, CounterT, WeatherT)

此定义要求我将 (MockCounter (MockWeather m) 作为 WeatherT 的一个实例:

instance Monad m => WeatherT (MockCounter (MockWeather m))

我在主模块中定义了这个实例,因为我不希望 Weather 和 Counter 模块相互依赖。

但是在主模块中定义此实例使其成为孤儿实例。

问题:

Full code:

主模块

{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Main where

import          Counter
import          Weather

newtype MyAppM m a = MyAppM { unMyAppM :: MockCounter (MockWeather m) a }
  deriving (Functor, Applicative, Monad, CounterT, WeatherT)

instance Monad m => WeatherT (MockCounter (MockWeather m))

runMyAppM :: Int -> MyAppM m a -> m (a, Int)
runMyAppM i = runMockWeather . (`runMockCounter` i) . unMyAppM

myApp :: (Monad m, CounterT m , WeatherT m) => m String
myApp = do
  _ <- increment
  (WeatherData weather) <- byCity "Amsterdam"
  return weather

-- Testing it:
main :: IO ()
main = runMyAppM 12 myApp >>= print

天气模块:

{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Weather where

import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Identity

newtype WeatherData = WeatherData String deriving (Show)

class Monad m => WeatherT m where
  byCity :: String -> m WeatherData

  default byCity :: (MonadTrans t, WeatherT m', m ~ t m') => String -> m WeatherData
  byCity = lift . byCity


newtype MockWeather m a = MockWeather {
  unMockWeather :: IdentityT m a
} deriving (Functor, Applicative, Monad, MonadTrans)

runMockWeather :: MockWeather f a -> f a
runMockWeather = runIdentityT . unMockWeather

instance Monad m => WeatherT (MockWeather m) where
   byCity city = MockWeather $ return $ WeatherData $ "It is sunny in " ++ city

计数器模块:

{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Counter where

import           Control.Monad.Identity
import           Control.Monad.State
import           Control.Monad.Trans.Class

class Monad m => CounterT m where
  increment :: m Int
  current :: m Int

  default increment :: (MonadTrans t, CounterT m', m ~ t m') => m Int
  increment = lift increment

  default current :: (MonadTrans t, CounterT m', m ~ t m') => m Int
  current = lift current


newtype MockCounter m a = MockCounter {
  unMockCounter :: StateT Int m a
} deriving (Functor, Applicative, Monad, MonadTrans, MonadState Int)

defaultMockCounter :: MockCounter Identity ()
defaultMockCounter = MockCounter $ put 0

runMockCounter :: MockCounter m a -> Int -> m (a, Int)
runMockCounter = runStateT . unMockCounter

instance Monad m => CounterT (MockCounter m) where
  increment = MockCounter $ do
    c <- get
    let n = c + 1
    put n
    return n

  current = MockCounter get

你需要一个实例 WeatherT m => WeatherT (MockCounter m),它只是通过 MockCounter m 提升一个 WeatherT m 实例,这要归功于 MockCounter 是一个 monad 转换器。 (您编写的默认方法的重点是定义此类实例。)

为了避免孤立实例,一种方法是将 WeatherCounter 分别分成 ClassTrans 模块。 Class 不需要相互依赖,而每个 Trans 模块可能依赖所有 Class 模块(反过来也是可能的,事实上 mtl 做到了,但是 IMO Trans 取决于 Class 更好:Class 定义接口,Trans 实现)。

这确实是一个(已知)问题,因为如果您有 n 个转换器和 m 类,您可能需要 n*m 个提升实例。一种解决方案是为所有转换器定义一个多态可重叠实例 (MonadTrans t, WeatherT m) => WeatherT (t m)。重叠实例通常不受欢迎,但我不确定在这种情况下存在什么实际问题。

顺便说一下,按照 mtltransformers 的命名约定,我们将有 MonadWeatherMonadCounter 类,以及 WeatherTCounterT 类型(monad 变形金刚)。