使用 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
...
它们都可能有多个具有不同实现的实例,例如它们都有一个模拟实例,我在这里主要使用:MockCounter
和 MockWeather
。
在主模块中,我将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 模块相互依赖。
但是在主模块中定义这个实例使它成为一个孤立实例。
问题:
我在CounterT
、WeatherT
和MyAppM
的轨道上是否正确?我想通过组合解耦和可模拟的功能来构建我的应用。
如何避免孤立实例?
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 => WeatherT (MockCounter m)
实例,它通过 MockCounter m
提升 WeatherT m
实例,这要归功于 MockCounter
是一个单子转换器。 (您编写的默认方法的重点是定义此类实例。)
为避免孤立实例,一种方法是将Weather
和Counter
分别分离到Class
和Trans
模块中。 Class
不需要相互依赖,而每个 Trans
模块可能依赖于所有 Class
模块(反过来也是可能的,实际上 mtl
是如何做到的,但是 IMO Trans
取决于 Class
更好:Class
定义接口,Trans
实现)。
这确实是一个(已知的)问题,因为如果您有n
转换器和m
类,您可能需要n*m
提升实例。一种解决方案是为所有转换器(MonadTrans t, WeatherT m) => WeatherT (t m)
定义一个多态可重叠实例。重叠的实例通常不受欢迎,但我不确定这种情况下存在哪些实际问题。
顺便说一句,按照 mtl
和 transformers
的命名约定,我们将拥有 MonadWeather
和 MonadCounter
类,以及 WeatherT
和 CounterT
类型(monad Transformers)。
【讨论】:
重叠实例是我最不喜欢的语言扩展。我承认他们的问题可能不会在测试模型的上下文中产生影响。一个问题是,如果您从不编写实例,则特别容易忘记覆盖实例中的默认值。这可能导致人们错误地使用无意义的实例。存在主义和使用 Data.Constraint.Forall 之类的东西也可能会出现并发症。以上是关于使用 Monad Transformers 避免孤儿实例的主要内容,如果未能解决你的问题,请参考以下文章
论文笔记:Are Transformers Effective for Time Series Forecasting?