Упрощение вызова функций, хранящихся в среде ReaderT
Предположим, у меня есть такая запись среды:
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
монада, то есть в ReaderT
над IO
параметризованный самой средой.
Я хотел бы иметь лаконичный способ вызова этих функций внутри RIO Env
монада.
Я мог бы написать что-то вроде этого 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
Это возможно?
2 ответа
Из двух примеров мы можем догадаться, что call
имел бы тип (Env -> r) -> r
.
example2 :: RIO Env ()
example2 = call foo 0 "fooarg"
example3 :: RIO Env Int
example3 = call bar 3
Поместите это в типовой класс и рассмотрите два случая: r
это стрела a -> r'
, или r
является RIO Env r'
. Реализация вариативности с классами типов обычно не одобряется из-за их хрупкости, но здесь она работает хорошо, потому чтоRIO
type обеспечивает естественный базовый случай, и все определяется типами средств доступа (поэтому вывод типа не мешает).
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
как базовая монада или 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