Por que FunctionalDependency é necessário para definir o MonadReader?

8

Eu apenas consegui entender a definição da classe MonadReader

class Monad m => MonadReader r m | m -> r where
...

Depois de ler o documento de Dependência funcional em Haskell, agora posso entender que | m -> respecifica que a variável de tipo ré decidida exclusivamente por m. Eu acho que esse requisito é razoável com base nas poucas instâncias típicas do MonadReader que eu vi até agora (por exemplo Reader), mas parece-me que ainda podemos definir instâncias como Readermesmo sem essa cláusula de dependência funcional.

Minha pergunta é por que precisamos de dependência funcional na definição do MonadReader? Isso é funcionalmente necessário para definir o MonadReader no sentido de que o MonadReader não pode ser definido adequadamente sem ele, ou é apenas uma restrição limitar as maneiras como o MonadReader pode ser usado para que as instâncias do MonadReader se comportem da maneira esperada?

Lifu Huang
fonte
Um exercício divertido quando apresentado a essas perguntas é tentar escrevê-las usando uma definição sem o dep.
Thomas M. Dubuisson
1
Não é necessário para definir MonadReader; é necessário para o uso conveniente MonadReader.
Daniel Wagner

Respostas:

4

É necessário fazer com que a inferência de tipo funcione de maneira mais conveniente para o usuário.

Por exemplo, sem o fundep, isso não seria compilado:

action :: ReaderT Int IO ()
action = do
  x <- ask
  liftIO $ print x

Para fazer a compilação acima, precisaríamos escrever

action :: ReadertT Int IO ()
action = do
  x <- ask :: ReadertT Int IO Int
  liftIO $ print x

Isso ocorre porque, sem o fundep, o compilador não pode inferir que xé um Int. Afinal, uma mônada ReadertT Int IOpode ter várias instâncias

instance MonadReader Int (ReaderT Int IO) where
   ask = ReaderT (\i -> return i)
instance MonadReader Bool (ReaderT Int IO) where
   ask = ReaderT (\i -> return (i != 0))
instance MonadReader String (ReaderT Int IO) where
   ask = ReaderT (\i -> return (show i))
-- etc.

portanto, o programador deve fornecer uma anotação que força x :: Int, ou o código é ambíguo.

chi
fonte
2

Esta não é realmente uma resposta, mas é muito tempo para um comentário. Você está certo de que é possível definir a MonadReaderclasse sem um fundep. Em particular, a assinatura de tipo de cada método determina todos os parâmetros de classe. Seria perfeitamente possível definir uma hierarquia mais fina.

class MonadReaderA r m where
  askA :: m r
  askA = readerA id

  readerA :: (r -> a) -> m a
  readerA f = f <$> askA

-- This effect is somewhat different in
-- character and requires special lifting.
class MonadReaderA r m => MonadReaderB r m where
  localB :: (r -> r) -> m a -> m a

class MonadReaderB r m
  => MonadReader r m | m -> r

ask :: MonadReader r m => m r
ask = askA

reader
  :: MonadReader r m
  => (r -> a) -> m a
reader = readerA

local
  :: MonadReader r m
  => (r -> r) -> m a -> m a
local = localB

O principal problema dessa abordagem é que os usuários precisam escrever várias instâncias.

dfeuer
fonte
1

Eu acho que a fonte de confusão é que, na definição de

class Monad m => MonadReader r m | m -> r where
  {- ... -}

É implicitamente assumido que se mcontém r(para instâncias comuns). Deixe-me usar um definiton mais claro de Readercomo

newtype Reader r a = Reader {runReader :: r -> a}

Quando o r parâmetro é escolhido, você pode definir facilmente uma instância de mônada para Reader r. Isso significa que na definição de classe de tipo mdeve ser substituída Reader r. Então, veja como a expressão acaba sendo:

instance MonadReader r (Reader r) where -- hey!! r is duplicated now
  {- ... -}                             -- The functional dependecy becomes Reader r -> r which makes sense

Mas por que precisamos disso? Veja a definição de askdentro da MonadReaderclasse.

class Monad m => MonadReader r m | m -> r where
  ask :: m r -- r and m are polymorphic here
  {- ... -}

Sem a diversão-dep nada poderia me parar para definir ask um tipo diferente de estado. Ainda mais, eu poderia definir muitas instâncias do leitor de mônada para o meu tipo. Como exemplo, isso seria definições válidas sem func-dep

instance MonadReader Bool (Reader r) where
--                   ^^^^         ^
--                   |            |- This is state type in the user defined newtype 
--                   |- this is the state type in the type class definition
  ask :: Reader r Bool
  ask = Reader (\_ -> True) -- the function that returns True constantly
  {- ... -}                             
instance MonadReader String (Reader r) where
--                   ^^^^^^         ^
--                   |              |- This is read-state type in the user defined newtype 
--                   |- this is the read-state type in the type class definition
  ask :: Reader r String
  ask = Reader (\_ -> "ThisIsBroken") -- the function that returns "ThisIsBroken" constantly
  {- ... -}                             

Então, se eu tivesse um valor val :: ReaderT Int IO Double qual seria o resultado ask. Precisamos especificar uma assinatura de tipo como abaixo

val :: Reader Int Double
val = do
  r <- ask :: Reader Int String
  liftIO $ putStrLn r   -- Just imagine you can use liftIO
  return 1.0

> val `runReader` 1
"ThisIsBroken"
1.0

val :: Reader Int Double
val = do
  r <- ask :: Reader Int Bool
  liftIO $ print r   -- Just imagine you can use liftIO
  return 1.0

> val `runReader` 1
True
1.0

Além de ser insensato, não é conveniente especificar o tipo repetidamente.

Como conclusão, usando a definição real de ReaderT. Quando você tem algo parecido com val :: ReaderT String IO Inta dependência funcional, diz: Esse tipo pode ter apenas uma instância única da MonadReaderclasse, definida como aquela que usa Stringcomor

Ismor
fonte