Упрощение вызова функций, хранящихся в среде 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
Другие вопросы по тегам