使用 monad 堆栈进行依赖注入
Doing dependency injection using monad stacks
我 trying different approaches 要做有时称为依赖注入的事情。为此,我详细阐述了一个天气应用程序的简单示例,我们希望在其中获取天气数据(从网络服务或硬件设备),存储天气数据(可以是数据库或只是一个文件),并报告它(将其打印到屏幕上,或说出天气)。这个想法是编写一个使用一些 fetch
、store
和 report
函数的程序,这些函数的实现可能会有所不同。
我已经使用 functions and free-monads 设法将关注点和抽象从检索、存储和报告的实现中分离出来,但是我使用 monad 堆栈达成的解决方案看起来很糟糕:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module WeatherReporterMTL where
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
type WeatherData = String
class Monad m => WeatherService m where
fetch :: m WeatherData
class Monad m => Storage m where
store :: WeatherData -> m ()
class Monad m => Reporter m where
report :: WeatherData -> m ()
-- | A dummy implementation of the @WeatherService@
newtype DummyService m a = DummyService { runDummyService :: m a }
deriving (Functor, Applicative, Monad, MonadIO)
instance MonadIO m => WeatherService (DummyService m) where
fetch = return "won't get any warmer in December."
-- | A dummy implementation of the @Storage@
newtype DummyStorage m a = DummyStorage { runDummyStorage :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService)
-- It seems wrong that the storage has to be an instance the weather service
-- (@WeatherService@) ...
instance MonadIO m => Storage (DummyStorage m) where
store d = liftIO $ putStrLn $ "No room left for this report: " ++ d
-- | A dummy implementation of the @Reporter@
newtype DummyReporter m a = DummyReporter { runDummyReporter :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService, Storage)
-- Ok, now this seems even worse: we're putting information about
-- how we're gonna stack our monads :/
instance MonadIO m => Reporter (DummyReporter m) where
report d = liftIO $ putStrLn $ "Here at the MTL side " ++ d
reportWeather :: (WeatherService m, Storage m, Reporter m) => m ()
reportWeather = do
w <- fetch
store w
report w
dummyWeatherReport :: IO ()
dummyWeatherReport = runDummyService $ runDummyStorage $ runDummyReporter reportWeather
在上面的代码中,DummyStorage
和 DummyReporter
都必须有 WeatherService
的平凡实例,这显然是错误的。此外,这些实例取决于最后堆叠 monad 的顺序。有没有办法避免不同堆栈之间的信息泄漏?
而不是将实现绑定到特定的新类型,也许你可以有 "free-floating" 需要访问 IO 和一些必要的簿记状态的实现函数,比如
data WeatherState = WeatherState -- dummy
fetch' :: (MonadState WeatherState m,MonadIO m) => m WeatherData
fetch' = undefined
data StorageState = StorageState -- dummy
store' :: (MonadState StorageState m,MonadIO m) => WeatherData -> m ()
store' = undefined
data ReporterState = ReporterState -- dummy
report' :: (MonadState ReporterState m,MonadIO m) => WeatherData -> m ()
report' = undefined
"Injecting" 意味着在携带所需状态的 StateT
上创建一些新类型,然后声明像
这样的实例
newtype Injected a =
Injected { getInjected :: StateT (WeatherState,StorageState,ReportState) a }
deriving (Functor,Applicative,Monad)
instance WeatherService Injected where
fetch = Injected $ zoom _1 fetch'
instance Storage Injected where
store x = Injected $ zoom _2 $ store' x
instance Reporter Injected where
report x = Injected $ zoom _3 $ report' x
我 trying different approaches 要做有时称为依赖注入的事情。为此,我详细阐述了一个天气应用程序的简单示例,我们希望在其中获取天气数据(从网络服务或硬件设备),存储天气数据(可以是数据库或只是一个文件),并报告它(将其打印到屏幕上,或说出天气)。这个想法是编写一个使用一些 fetch
、store
和 report
函数的程序,这些函数的实现可能会有所不同。
我已经使用 functions and free-monads 设法将关注点和抽象从检索、存储和报告的实现中分离出来,但是我使用 monad 堆栈达成的解决方案看起来很糟糕:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module WeatherReporterMTL where
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
type WeatherData = String
class Monad m => WeatherService m where
fetch :: m WeatherData
class Monad m => Storage m where
store :: WeatherData -> m ()
class Monad m => Reporter m where
report :: WeatherData -> m ()
-- | A dummy implementation of the @WeatherService@
newtype DummyService m a = DummyService { runDummyService :: m a }
deriving (Functor, Applicative, Monad, MonadIO)
instance MonadIO m => WeatherService (DummyService m) where
fetch = return "won't get any warmer in December."
-- | A dummy implementation of the @Storage@
newtype DummyStorage m a = DummyStorage { runDummyStorage :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService)
-- It seems wrong that the storage has to be an instance the weather service
-- (@WeatherService@) ...
instance MonadIO m => Storage (DummyStorage m) where
store d = liftIO $ putStrLn $ "No room left for this report: " ++ d
-- | A dummy implementation of the @Reporter@
newtype DummyReporter m a = DummyReporter { runDummyReporter :: m a }
deriving (Functor, Applicative, Monad, MonadIO, WeatherService, Storage)
-- Ok, now this seems even worse: we're putting information about
-- how we're gonna stack our monads :/
instance MonadIO m => Reporter (DummyReporter m) where
report d = liftIO $ putStrLn $ "Here at the MTL side " ++ d
reportWeather :: (WeatherService m, Storage m, Reporter m) => m ()
reportWeather = do
w <- fetch
store w
report w
dummyWeatherReport :: IO ()
dummyWeatherReport = runDummyService $ runDummyStorage $ runDummyReporter reportWeather
在上面的代码中,DummyStorage
和 DummyReporter
都必须有 WeatherService
的平凡实例,这显然是错误的。此外,这些实例取决于最后堆叠 monad 的顺序。有没有办法避免不同堆栈之间的信息泄漏?
而不是将实现绑定到特定的新类型,也许你可以有 "free-floating" 需要访问 IO 和一些必要的簿记状态的实现函数,比如
data WeatherState = WeatherState -- dummy
fetch' :: (MonadState WeatherState m,MonadIO m) => m WeatherData
fetch' = undefined
data StorageState = StorageState -- dummy
store' :: (MonadState StorageState m,MonadIO m) => WeatherData -> m ()
store' = undefined
data ReporterState = ReporterState -- dummy
report' :: (MonadState ReporterState m,MonadIO m) => WeatherData -> m ()
report' = undefined
"Injecting" 意味着在携带所需状态的 StateT
上创建一些新类型,然后声明像
newtype Injected a =
Injected { getInjected :: StateT (WeatherState,StorageState,ReportState) a }
deriving (Functor,Applicative,Monad)
instance WeatherService Injected where
fetch = Injected $ zoom _1 fetch'
instance Storage Injected where
store x = Injected $ zoom _2 $ store' x
instance Reporter Injected where
report x = Injected $ zoom _3 $ report' x