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

hom*_*mam 1 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
  ...
Run Code Online (Sandbox Code Playgroud)

Counter模块中:

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
  ...
Run Code Online (Sandbox Code Playgroud)

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

Main模块中,我将MyAppmonad 定义为:

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

这个定义要求我做(MockCounter (MockWeather m)一个实例WeatherT:

instance Monad m => WeatherT (MockCounter (MockWeather m))
Run Code Online (Sandbox Code Playgroud)

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

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

问题:

  • 我在这里与谁合作CounterT,WeatherT并且MyAppM?我想通过组合分离和可模拟的功能来构建我的应用程序.
  • 我怎样才能避免孤立实例?

完整代码:

主要模块

{-# 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
Run Code Online (Sandbox Code Playgroud)

天气模块:

{-# 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
Run Code Online (Sandbox Code Playgroud)

柜台模块:

{-# 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
Run Code Online (Sandbox Code Playgroud)

Li-*_*Xia 5

由于monad变换器这一事实,你需要一个WeatherT m => WeatherT (MockCounter m)只提升WeatherT m实例的实例.(您编写的默认方法的要点是定义此类实例.)MockCounter mMockCounter

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

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

顺便说一句,遵循命名约定mtl,transformers我们将有MonadWeatherMonadCounter类,WeatherTCounterT类型(monad变形金刚).

  • 重叠实例是我最不喜欢的语言扩展。我会承认,他们的问题“可能”不会在测试模型的背景下解决。一个问题是,如果您从不编写实例,那么忘记覆盖实例中的默认值特别容易。这可能导致人们错误地使用废话实例。存在性以及使用诸如Data.Constraint.Forall之类的东西也可能会带来麻烦。 (2认同)