避免使用Monad变形金刚的孤儿实例

时间:2018-03-07 10:36:24

标签: haskell 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模块相互依赖。

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

问题:

  • 我是否在CounterTWeatherTMyAppM的正确轨道上?我想通过组合分离和可模拟的功能来构建我的应用程序。
  • 如何避免孤儿实例?

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

1 个答案:

答案 0 :(得分:5)

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

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

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

顺便说一下,按照mtltransformers的命名惯例,我们会有MonadWeatherMonadCounter个类,WeatherT和{{1} } types(monad变形金刚)。