gpt4 book ai didi

haskell - Eff 中的多个 IO 效果(或其他可组合效果的方式)

转载 作者:行者123 更新时间:2023-12-02 14:20:42 26 4
gpt4 key购买 nike

我想尽可能地限制程序中函数的影响,以便例如如果我有一个应该查询数据库的函数,我知道它不会打印用于删除我的文件的内容。

作为一个具体示例,假设我有一个带有“用户”表的数据库。

有些函数只能读取该表,有些函数可以读写。

有了 mtl 和 Transformer,我可以尝试这样的事情:

data User = User { username :: String }
deriving (Show)

class Monad m => ReadDb m where
getUsers :: m [User]
getUserByName :: String -> m (Maybe User)

class Monad m => WriteDb m where
addUser :: String -> m ()
removeUser :: String -> m Bool

然而,实现我需要的实例即使不是不可能,也是很棘手的。成为能够访问数据库我需要一个SqlBackend和IO:

data SqlBackend

instance (MonadReader SqlBackend m, MonadIO m, Monad m) => ReadDb m where
getUsers = undefined
getUserByName = undefined

instance (MonadReader SqlBackend m, MonadIO m, Monad m) => WriteDb m where
addUser = undefined
removeUser = undefined

使用UndecidableInstances,效果很好。但是,假设我还需要日志记录,不,我不会在 [String] 或类似的内容中收集日志字符串那。记录器应该有效地记录,并且记录消息应该出现在实时。

所以我可能会做这样的事情:

class Monad m => Log m where
log :: String -> m ()

日志记录需要一个Logger,所以我可以定义一个实例,例如

data Logger

instance (MonadReader Logger m, MonadIO m, Monad m) => Log m where
log = undefined

现在读取数据库和日志的函数将如下所示:

logUsers :: (ReadDb m, Log m) => m ()
logUsers = getUsers >>= log . show

但不幸的是我无法真正运行它,因为我需要提供MonadReader SqlBackend mMonadReader Logger m,这是不可能的由于功能依赖 MonadReader r m | m -> r.

有一些解决方法(例如实现不同的类型类只是为了获得LoggerSqlBackend),但它们涉及太多样板文件。

作为替代方案,我想尝试 Oleg 的可扩展效果库(Effmonad,在这里实现 http://okmij.org/ftp/Haskell/extensible/Eff.hs )。这据我了解,麻烦在于需要处理多种影响IO 无法在 Eff 中以可组合的方式实现。例如,跟踪库中的效果实现如下:

data Trace

runTrace :: Eff (Trace :> Void) w -> IO w

Void 部分是这里的问题。在我的例子中,我想处理读、写和单独记录操作,并且功能应该能够具有允许这些效果的任何子集的细粒度类型。

这里想到的一件事是Free,但我不确定如何定义仿函数对于这些效果,然后组合它们,例如一个函数日志将能够调用另一个不记录但有其他功能的函数效果相同。

所以我的问题是:如何在我的程序中获得细粒度的效果类型,实际组成的效果处理程序。效果处理程序应该能够运行IO。假设性能不是问题(因此免费等就可以了)。

最佳答案

我认为您的实例声明是一个错误。

instance (MonadReader SqlBackend m, MonadIO m, Monad m) => ReadDb m

此实例将匹配所有类型构造函数m::* -> *,然后如果相关的m不匹配,则稍后失败。 t 适合实例上下文。实例搜索中没有回溯。换句话说,您无法更改 ReadDb 的实例(例如,如果您需要在测试期间模拟数据库)。它还会导致父类(super class)重叠的问题。

最好将程序构建为 monad 转换器堆栈,像往常一样使用 newtype。所以我要写一个自定义的 monad 转换器:

data SqlConfig = SqlConfig { connectionString :: String }

newtype DbT m a = DbT (ReaderT SqlConfig m a) deriving (
Functor,
Applicative,
Alternative,
Monad,
MonadTrans,
MonadPlus,
MonadFix,
MonadIO,
MonadWriter w,
MonadState s,
MonadError e,
MonadCont
)
runDbT :: DbT m a -> SqlConfig -> m a
runDbT (DbT m) = runReaderT m

我正在使用 GeneralizedNewtypeDeriving 派生 mtl(MonadReader除外)。 (这些实例还需要 UndecidableInstances,因为它们未满足覆盖条件。)我不想从DbT,我想将它从基础 monad 中取出。 DbT 不是 ReaderT,它只是碰巧使用 ReaderT 实现的。

mapDbT :: (m a -> n b) -> DbT m a -> DbT n b
mapDbT f (DbT m) = DbT $ mapReaderT f m
instance MonadReader r m => MonadReader r (DbT m) where
ask = lift ask
local = mapDbT . local

只要我们能够访问 IO,我就可以使用 DbT 实现您的类:

instance MonadIO m => MonadReadDb (DbT m) where
getUsers = DbT $ ask >>= (liftIO . query "select * from Users")
getUserByName name = DbT $ ask >>= (liftIO . query "select * from Users where Name = @name")

instance MonadIO m => MonadWriteDb (DbT m) where
addUser u = DbT $ ask >>= (liftIO . query "insert Users (Name) values @name")
removeUser u = DbT $ ask >>= (liftIO . query "delete Users where Name = @name")

同样,我可以设置一个日志记录 monad 转换器:

data LoggingConfig = LoggingConfig { filePath :: String }

newtype LoggerT m a = LoggerT (ReaderT LoggingConfig m a) deriving (
Functor,
Applicative,
Alternative,
Monad,
MonadTrans,
MonadPlus,
MonadFix,
MonadIO,
MonadWriter w,
MonadState s,
MonadError e,
MonadCont
)
runLoggerT :: LoggerT m a -> LoggingConfig -> m a
runLoggerT (LoggerT m) = runReaderT m

instance MonadIO m => MonadLogger (LoggerT m) where
log msg = LoggerT $ do
config <- ask
liftIO $ writeFile (filePath config) msg

-- MonadReader instance omitted. It's identical to the DbT instance

令人烦恼的是 - 这是 mtl 方法的主要缺点 - 您必须编写 O(n^2) 实例才能使这些类型很好地组合。

instance MonadLogger m => MonadLogger (DbT m) where
log = lift . log

instance MonadReadDb m => MonadReadDb (LoggerT m) where
getUsers = lift getUsers
getUserByName = lift . getUserByName

instance MonadWriteDb m => MonadWriteDb (LoggerT m) where
addUser = lift . addUser
removeUser = lift . removeUser

-- and a bunch of identical instances for all the types in transformers

您可以像往常一样使用三个类编写一元程序:

myProgram :: (MonadLogger m, MonadReadDb m, MonadWriteDb m) => m ()
myProgram = do
us <- getUsers
log $ "removing " ++ show (length us) ++ " users"
void $ traverse removeUser us

然后在程序的入口点,当您构建并运行 monad 转换器堆栈时,您只需解开 LoggerTDbT 新类型并提供所需的配置。

runProgram :: LoggerT (DbT IO) a -> LoggingConfig -> SqlConfig -> IO a
runProgram m l s = runDbT (runLoggerT m l) s

ghci> :t runProgram myProgram
runProgram myProgram :: LoggingConfig -> SqlConfig -> IO ()

关于haskell - Eff 中的多个 IO 效果(或其他可组合效果的方式),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42174785/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com