¿Por qué se necesita FunctionalDependency para definir MonadReader?

8

Me las arreglé para entender la definición de la clase. MonadReader

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

Después de leer el documento de Dependencia funcional en Haskell, ahora puedo entender que | m -> respecifica que la variable de tipo rse decide de forma exclusiva m. Creo que este requisito es razonable en función de las pocas instancias típicas de MonadReader que he visto hasta ahora (por ejemplo Reader), pero me parece que todavía podemos definir instancias como Readerincluso sin esta cláusula de dependencia funcional.

Mi pregunta es ¿por qué necesitamos dependencia funcional en la definición de MonadReader? ¿Es esto funcionalmente necesario para definir MonadReader en el sentido de que MonadReader no se puede definir adecuadamente sin él, o es simplemente una restricción para limitar las formas en que MonadReader puede usarse para que las instancias de MonadReader se comporten de una manera esperada?

Lifu Huang
fuente
Un ejercicio divertido cuando se le presentan estas preguntas es tratar de escribirlas usando una definición sin el dep.
Thomas M. DuBuisson
1
No es necesario para definir MonadReader; Es necesario para usar convenientemente MonadReader.
Daniel Wagner

Respuestas:

4

Es necesario para que la inferencia de tipos funcione de una manera que sea más conveniente para el usuario.

Por ejemplo, sin el fundep esto no compilaría:

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

Para hacer la compilación anterior necesitaríamos escribir

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

Esto es porque, sin el fundep, el compilador no puede inferir que xes un Int. Después de todo, una mónada ReadertT Int IOpodría tener múltiples instancias

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.

entonces el programador debe proporcionar alguna anotación que fuerce x :: Int , o el código es ambiguo.

chi
fuente
2

Esto no es realmente una respuesta, pero es demasiado largo para un comentario. Tienes razón en que es posible definir la MonadReaderclase sin un fundep. En particular, la firma de tipo de cada método determina cada parámetro de clase. Sería bastante posible definir una jerarquía más 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

El principal problema con este enfoque es que los usuarios tienen que escribir un montón de instancias.

dfeuer
fuente
1

Creo que la fuente de confusión es que en la definición de

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

Se asume implícitamente que se mcontiene a rsí mismo (para instancias comunes). Déjame usar una definición más ligera de Readercomo

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

Cuando rse elige el parámetro, puede definir fácilmente una instancia de mónada para Reader r. Eso significa que en la definición de clase de tipo se mdebe sustituir Reader r. Así que mira cómo la expresión termina siendo:

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

Pero, ¿por qué necesitamos esto? Mira la definición de askdentro de la MonadReaderclase.

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

Sin el fun-dep nada podría detenerme por definir askuna forma de devolver un tipo diferente como el estado. Aún más, podría definir muchas instancias de lector de mónada para mi tipo. Como ejemplo, estas serían definiciones válidas sin 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
  {- ... -}                             

Entonces, si tuviera un valor, val :: ReaderT Int IO Double¿cuál sería el resultado ask? Tendríamos que especificar una firma de tipo como se muestra a continuación

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

Además de no tener sentido, no es conveniente especificar el tipo una y otra vez.

Como conclusión, utilizando la definición real de ReaderT. Cuando tiene algo como que val :: ReaderT String IO Intla dependencia funcional dice: Tal tipo podría tener solo una instancia única de MonadReadertypeclass que se define como la que usa Stringcomor

lsmor
fuente