简化存储在 ReaderT 环境中的函数的调用
Simplifying the invocation of functions stored inside an ReaderT environment
假设我有这样的环境记录:
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
type RIO env a = ReaderT env IO a
data Env = Env
{ foo :: Int -> String -> RIO Env (),
bar :: Int -> RIO Env Int
}
env :: Env
env =
Env
{ foo = \_ _ -> do
liftIO $ putStrLn "foo",
bar = \_ -> do
liftIO $ putStrLn "bar"
return 5
}
存储在环境中的函数可能有不同数量的参数,但它们总是会在 RIO Env
monad 中产生值,也就是说,在 ReaderT
over IO
参数化中受环境本身影响。
我希望有一种在 RIO Env
monad 中调用这些函数的简洁方法。
我可以写这样的东西 call
函数:
import Control.Monad.Reader
call :: MonadReader env m => (env -> f) -> (f -> m r) -> m r
call getter execute = do
f <- asks getter
execute f
并像这样使用它(可能与 -XBlockArguments
结合使用):
example1 :: RIO Env ()
example1 = call foo $ \f -> f 0 "fooarg"
但是,理想情况下,我想要一个 call
版本,它允许使用以下更直接的语法,并且仍然适用于具有不同数量参数的函数:
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
这可能吗?
从这两个例子中,我们可以猜测 call
的类型是 (Env -> r) -> r
.
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
将其放入类型 class 中,并考虑两种情况,r
是箭头 a -> r'
,或者 r
是 RIO Env r'
。使用类型 classes 实现可变参数通常不受欢迎,因为它们非常脆弱,但它在这里工作得很好,因为 RIO
类型提供了一个自然的基本情况,并且一切都由访问器的类型决定(所以类型推断不妨碍)。
class Call r where
call :: (Env -> r) -> r
instance Call r => Call (a -> r) where
call f x = call (\env -> f env x)
instance Call (RIO Env r') where
call f = ask >>= f
这里对丽瑶的回答进行了一些小的改进。此版本不特定于 IO
作为基本 monad,或特定于 Env
作为环境类型。在基本情况实例中使用等式约束应该会稍微改进类型推断,尽管 call
旨在使用它可能只会影响类型化的漏洞。
{-# language MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}
class e ~ TheEnv r => Call e r where
type TheEnv r
call :: (e -> r) -> r
instance Call e r => Call e (a -> r) where
type TheEnv (a -> r) = TheEnv r
call f x = call (\env -> f env x)
instance (Monad m, e ~ e') => Call e (ReaderT e' m r) where
type TheEnv (ReaderT e' m r) = e'
call f = ask >>= f
关联类型可以说是矫枉过正。也可以使用函数依赖:
{-# language FunctionalDependencies, TypeFamilies, FlexibleInstances, UndecidableInstances #-}
class Call e r | r -> e where
call :: (e -> r) -> r
instance Call e r => Call e (a -> r) where
call f x = call (\env -> f env x)
instance (Monad m, e ~ e') => Call e (ReaderT e' m r) where
call f = ask >>= f
假设我有这样的环境记录:
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
type RIO env a = ReaderT env IO a
data Env = Env
{ foo :: Int -> String -> RIO Env (),
bar :: Int -> RIO Env Int
}
env :: Env
env =
Env
{ foo = \_ _ -> do
liftIO $ putStrLn "foo",
bar = \_ -> do
liftIO $ putStrLn "bar"
return 5
}
存储在环境中的函数可能有不同数量的参数,但它们总是会在 RIO Env
monad 中产生值,也就是说,在 ReaderT
over IO
参数化中受环境本身影响。
我希望有一种在 RIO Env
monad 中调用这些函数的简洁方法。
我可以写这样的东西 call
函数:
import Control.Monad.Reader
call :: MonadReader env m => (env -> f) -> (f -> m r) -> m r
call getter execute = do
f <- asks getter
execute f
并像这样使用它(可能与 -XBlockArguments
结合使用):
example1 :: RIO Env ()
example1 = call foo $ \f -> f 0 "fooarg"
但是,理想情况下,我想要一个 call
版本,它允许使用以下更直接的语法,并且仍然适用于具有不同数量参数的函数:
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
这可能吗?
从这两个例子中,我们可以猜测 call
的类型是 (Env -> r) -> r
.
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
将其放入类型 class 中,并考虑两种情况,r
是箭头 a -> r'
,或者 r
是 RIO Env r'
。使用类型 classes 实现可变参数通常不受欢迎,因为它们非常脆弱,但它在这里工作得很好,因为 RIO
类型提供了一个自然的基本情况,并且一切都由访问器的类型决定(所以类型推断不妨碍)。
class Call r where
call :: (Env -> r) -> r
instance Call r => Call (a -> r) where
call f x = call (\env -> f env x)
instance Call (RIO Env r') where
call f = ask >>= f
这里对丽瑶的回答进行了一些小的改进。此版本不特定于 IO
作为基本 monad,或特定于 Env
作为环境类型。在基本情况实例中使用等式约束应该会稍微改进类型推断,尽管 call
旨在使用它可能只会影响类型化的漏洞。
{-# language MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}
class e ~ TheEnv r => Call e r where
type TheEnv r
call :: (e -> r) -> r
instance Call e r => Call e (a -> r) where
type TheEnv (a -> r) = TheEnv r
call f x = call (\env -> f env x)
instance (Monad m, e ~ e') => Call e (ReaderT e' m r) where
type TheEnv (ReaderT e' m r) = e'
call f = ask >>= f
关联类型可以说是矫枉过正。也可以使用函数依赖:
{-# language FunctionalDependencies, TypeFamilies, FlexibleInstances, UndecidableInstances #-}
class Call e r | r -> e where
call :: (e -> r) -> r
instance Call e r => Call e (a -> r) where
call f x = call (\env -> f env x)
instance (Monad m, e ~ e') => Call e (ReaderT e' m r) where
call f = ask >>= f