使用 Monad Transformers 避免孤儿实例

Posted

技术标签:

【中文标题】使用 Monad Transformers 避免孤儿实例【英文标题】:Avoiding Orphan Instances with Monad Transformers 【发布时间】:2018-03-07 10:36:23 【问题描述】:

我有与我的应用程序的独立功能相对应的 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】:

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

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

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

顺便说一句,按照 mtltransformers 的命名约定,我们将拥有 MonadWeatherMonadCounter 类,以及 WeatherTCounterT 类型(monad Transformers)。

【讨论】:

重叠实例是我最不喜欢的语言扩展。我承认他们的问题可能不会在测试模型的上下文中产生影响。一个问题是,如果您从不编写实例,则特别容易忘记覆盖实例中的默认值。这可能导致人们错误地使用无意义的实例。存在主义和使用 Data.Constraint.Forall 之类的东西也可能会出现并发症。

以上是关于使用 Monad Transformers 避免孤儿实例的主要内容,如果未能解决你的问题,请参考以下文章

论文笔记:Are Transformers Effective for Time Series Forecasting?

学习函数式编程 Monad

用于 IO monad 的复杂 monad 转换器

我应该总是在javascript中使用monad吗?

使用 monad 转换器改变表达式结果

Promise是Monad吗?