简化存储在 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',或者 rRIO 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