Buenos ejemplos de No es Functor / Functor / Aplicativo / Mónada?

210

Al explicarle a alguien qué es un tipo de clase X, lucho por encontrar buenos ejemplos de estructuras de datos que sean exactamente X.

Entonces, solicito ejemplos para:

  • Un constructor de tipos que no es un Functor.
  • Un constructor de tipos que es un Functor, pero no Aplicativo.
  • Un constructor de tipo que es un aplicativo, pero no es una mónada.
  • Un constructor de tipo que es una mónada.

Creo que hay muchos ejemplos de Monad en todas partes, pero un buen ejemplo de Monad con alguna relación con ejemplos anteriores podría completar la imagen.

Busco ejemplos que serían similares entre sí, que difieren solo en aspectos importantes para pertenecer a la clase de tipo particular.

Si uno pudiera obtener un ejemplo de Arrow en algún lugar de esta jerarquía (¿es entre Applicative y Monad?), ¡Eso también sería genial!

Rotsor
fuente
44
¿Es posible hacer un constructor de tipo ( * -> *) para el cual no existe un adecuado fmap?
Owen
1
Owen, creo que a -> Stringno es un functor.
Rotsor
3
@Rotsor @Owen a -> Stringes un functor matemático, pero no un Haskell Functor, para ser claros.
J. Abrahamson
@J. Abrahamson, ¿en qué sentido es un functor matemático entonces? ¿Estás hablando de la categoría con flechas invertidas?
Rotsor
3
Para las personas que no saben, un functor contravariante tiene un fmap de tipo(a -> b) -> f b -> f a
AJFarmar

Respuestas:

100

Un constructor de tipos que no es un Functor:

newtype T a = T (a -> Int)

Puede hacer un functor contravariante, pero no un functor (covariante). Intenta escribir fmapy fracasarás. Tenga en cuenta que la versión del fundente contravariante se invierte:

fmap      :: Functor f       => (a -> b) -> f a -> f b
contramap :: Contravariant f => (a -> b) -> f b -> f a

Un constructor de tipos que es un functor, pero no Aplicativo:

No tengo un buen ejemplo. Hay Const, pero idealmente me gustaría un no monoide concreto y no puedo pensar en ninguno. Todos los tipos son básicamente numéricos, enumeraciones, productos, sumas o funciones cuando se llega al final. Puede ver a continuación el trabajador porcino y no estoy de acuerdo sobre si Data.Voides un Monoid;

instance Monoid Data.Void where
    mempty = undefined
    mappend _ _ = undefined
    mconcat _ = undefined

Dado que _|_es un valor legal en Haskell, y de hecho es el único valor legal de Data.Voideste, cumple con las reglas de Monoid. No estoy seguro de qué unsafeCoercetiene que ver con esto, porque su programa ya no garantiza que no violará la semántica de Haskell tan pronto como utilice cualquier unsafefunción.

Consulte la Wiki de Haskell para obtener un artículo sobre la parte inferior ( enlace ) o las funciones inseguras ( enlace ).

Me pregunto si es posible crear un constructor de este tipo utilizando un sistema de tipos más rico, como Agda o Haskell con varias extensiones.

Un constructor de tipos que es Aplicativo, pero no Mónada:

newtype T a = T {multidimensional array of a}

Puede hacer un aplicativo con algo como:

mkarray [(+10), (+100), id] <*> mkarray [1, 2]
  == mkarray [[11, 101, 1], [12, 102, 2]]

Pero si lo convierte en una mónada, podría obtener un desajuste de dimensión. Sospecho que ejemplos como este son raros en la práctica.

Un constructor de tipos que es una mónada:

[]

Acerca de las flechas:

Preguntar dónde se encuentra una flecha en esta jerarquía es como preguntar qué tipo de forma es "roja". Tenga en cuenta el tipo de desajuste:

Functor :: * -> *
Applicative :: * -> *
Monad :: * -> *

pero,

Arrow :: * -> * -> *
Dietrich Epp
fuente
3
Buena lista! Sugeriría usar algo más simple Either acomo un ejemplo para el último caso, ya que es más fácil de entender.
fuz
66
Si todavía está buscando un constructor de tipo Aplicativo pero no una Mónada, un ejemplo muy común sería ZipList.
John L
23
_|_habita cada tipo en *, pero el punto Voides que deberías inclinarte hacia atrás para construir uno o has destruido su valor. Es por eso que no es una instancia de Enum, Monoid, etc. Si ya tiene uno, me complace dejarlos juntarlos (dándole un Semigroup) pero mempty, pero no le doy herramientas para construir explícitamente un valor de tipo Voiden void. Tienes que cargar el arma, apuntarla a tu pie y apretar el gatillo tú mismo.
Edward KMETT
2
Pendientemente, creo que tu noción de Cofunctor es incorrecta. El doble de un functor es un functor, porque voltea tanto la entrada como la salida y terminas con la misma cosa. La noción que está buscando es probablemente "functor contravariante", que es ligeramente diferente.
Ben Millwood
1
@AlexVong: "En desuso" -> las personas simplemente están usando un paquete diferente. Hablando de "functor contravariante" no "dual de functor", perdón por la confusión. En algunos contextos, he visto que "cofunctor" solía referirse a "functores contravariantes" porque los functores son auto-duales, pero parece ser una persona confusa.
Dietrich Epp
87

Mi estilo puede estar limitado por mi teléfono, pero aquí va.

newtype Not x = Kill {kill :: x -> Void}

No puede ser un Functor. Si fuera así, tendríamos

kill (fmap (const ()) (Kill id)) () :: Void

y la luna estaría hecha de queso verde.

mientras tanto

newtype Dead x = Oops {oops :: Void}

es un functor

instance Functor Dead where
  fmap f (Oops corpse) = Oops corpse

pero no puede ser aplicativo, o tendríamos

oops (pure ()) :: Void

y Green estaría hecho de queso Moon (que en realidad puede suceder, pero solo más tarde en la noche).

(Nota adicional: Voidcomo Data.Voides un tipo de datos vacío. Si intenta usarlo undefinedpara demostrar que es un Monoide, lo usaré unsafeCoercepara demostrar que no lo es).

Alegremente

newtype Boo x = Boo {boo :: Bool}

es aplicativo de muchas maneras, por ejemplo, como lo diría Dijkstra,

instance Applicative Boo where
  pure _ = Boo True
  Boo b1 <*> Boo b2 = Boo (b1 == b2)

pero no puede ser una mónada. Para ver por qué no, observe que el retorno debe ser constante Boo Trueo Boo False, y de ahí que

join . return == id

no puede sostenerse.

Oh si, casi me olvido

newtype Thud x = The {only :: ()}

es una mónada Ruede el suyo.

Avión para atrapar ...

trabajador porcino
fuente
8
¡El vacío está vacío! Moralmente, de todos modos.
pigworker
9
Void es un tipo con 0 constructores, supongo. No es un monoide porque no hay mempty.
Rotsor
66
indefinido? ¡Qué grosero! Lamentablemente, unsafeCoerce (unsafeCoerce () <*> undefined) no es (), por lo que en la vida real, hay observaciones que violan las leyes.
pigworker
55
En la semántica habitual, que tolera exactamente un tipo de indefinido, tienes toda la razón. Hay otras semánticas, por supuesto. El vacío no se restringe a un submonoide en el fragmento total. Tampoco es un monoide en una semántica que distingue modos de falla. Cuando tenga un momento con una edición más fácil que la basada en el teléfono, aclararé que mi ejemplo solo funciona en una semántica para la que no hay exactamente un tipo indefinido.
pigworker
22
Mucho ruido sobre_|_
Landei
71

Creo que las otras respuestas omitieron algunos ejemplos simples y comunes:

Un constructor de tipos que es un Functor pero no un Aplicativo. Un ejemplo simple es un par:

instance Functor ((,) r) where
    fmap f (x,y) = (x, f y)

Pero no hay forma de definir su Applicativeinstancia sin imponer restricciones adicionales r. En particular, no hay forma de definir pure :: a -> (r, a)un arbitrario r.

Un constructor de tipo que es un aplicativo, pero no es una mónada. Un ejemplo bien conocido es ZipList . (Es una newtypeenvoltura de listas y proporciona una Applicativeinstancia diferente para ellos).

fmapse define de la manera habitual. Pero purey <*>se definen como

pure x                    = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith id fs xs)

por lo que purecrea una lista infinita repitiendo el valor dado y <*>cremalleras una lista de funciones con una lista de valores - se aplica i función -ésimo al i -ésimo elemento. (El estándar <*>en []produce todas las combinaciones posibles de aplicar la función i - ésima al elemento j -ésimo). Pero no hay una manera sensata de cómo definir una mónada (ver esta publicación ).


¿Cómo encajan las flechas en la jerarquía de fundador / aplicativo / mónada? Ver modismos son ajenos, las flechas son meticulosas, las mónadas son promiscuas por Sam Lindley, Philip Wadler, Jeremy Yallop. MSFP 2008. (Ellos llaman modismos de los ficticios aplicativos ). El resumen:

Revisamos la conexión entre tres nociones de cómputo: las mónadas de Moggi, las flechas de Hughes y las expresiones idiomáticas de McBride y Paterson (también llamadas fundores aplicativos). Mostramos que los modismos son equivalentes a flechas que satisfacen el tipo de isomorfismo A ~> B = 1 ~> (A -> B) y que las mónadas son equivalentes a flechas que satisfacen el tipo de isomorfismo A ~> B = A -> (1 ~ > B). Además, los modismos se integran en flechas y las flechas se integran en mónadas.

Petr Pudlák
fuente
1
Entonces, ((,) r)es un functor que no es un aplicativo; pero esto es solo porque generalmente no se puede definir purepara todos ra la vez. Por lo tanto, es un capricho de la concisión del lenguaje, de tratar de definir una colección (infinita) de functores aplicativos con una definición de purey <*>; en este sentido, no parece haber nada matemáticamente profundo en este contraejemplo, ya que, para cualquier concreto r, ((,) r) puede convertirse en un aplicador aplicador. Pregunta: ¿Puedes pensar en un functor de HORMIGÓN que no sea aplicativo?
George
1
Ver stackoverflow.com/questions/44125484/… como publicado con esta pregunta.
George
20

Un buen ejemplo para un constructor de tipos que no es un functor es Set: No puede implementar fmap :: (a -> b) -> f a -> f b, porque sin una restricción adicional Ord bno puede construir f b.

Landei
fuente
16
En realidad, es un buen ejemplo ya que matemáticamente tendríamos realmente como para hacer de este un funtor.
Alexandre C.
21
@AlexandreC. No estoy de acuerdo con eso, no es un buen ejemplo. Matemáticamente, dicha estructura de datos forma un functor. El hecho de que no podamos implementar fmapes solo un problema de idioma / implementación. Además, es posible ajustarse Seta la mónada de continuación, lo que la convierte en una mónada con todas las propiedades que esperaríamos, vea esta pregunta (aunque no estoy seguro de si se puede hacer de manera eficiente).
Petr Pudlák
@PetrPudlak, ¿cómo es esto un problema de idioma? ¡La igualdad de bpuede ser indecidible, en ese caso no se puede definir fmap!
Turion
@Turion Ser decidible y definible son dos cosas diferentes. Por ejemplo, es posible definir correctamente la igualdad en términos lambda (programas), aunque no es posible decidirlo mediante un algoritmo. En cualquier caso, este no fue el caso de este ejemplo. Aquí el problema es que no podemos definir una Functorinstancia con la Ordrestricción, pero podría ser posible con una definición diferente Functoro mejor soporte de idioma. En realidad, con ConstraintKinds es posible definir una clase de tipo que se pueda parametrizar de esta manera.
Petr Pudlák
Incluso si pudiéramos superar la ordrestricción, el hecho de que a Setno pueda contener entradas duplicadas significa que fmappodría alterar el contexto. Esto viola la ley de asociatividad.
John F. Miller
11

Me gustaría proponer un enfoque más sistemático para responder esta pregunta, y también mostrar ejemplos que no usen ningún truco especial como los valores "inferiores" o los tipos de datos infinitos o algo así.

¿Cuándo los constructores de tipos no tienen instancias de clase de tipo?

En general, hay dos razones por las que un constructor de tipos podría no tener una instancia de una clase de tipo determinada:

  1. No se pueden implementar las firmas de tipo de los métodos necesarios de la clase de tipo.
  2. Puede implementar las firmas de tipo pero no puede cumplir con las leyes requeridas.

Los ejemplos del primer tipo son más fáciles que los del segundo tipo porque para el primer tipo, solo necesitamos verificar si uno puede implementar una función con una firma de tipo dada, mientras que para el segundo tipo, debemos demostrar que no hay implementación posiblemente podría satisfacer las leyes.

Ejemplos específicos

  • Un constructor de tipos que no puede tener una instancia de functor porque el tipo no se puede implementar:

    data F z a = F (a -> z)

Este es un contrafunctor, no un functor, con respecto al parámetro tipo a, porque está aen una posición contravariante. Es imposible implementar una función con firma de tipo (a -> b) -> F z a -> F z b.

  • Un constructor de tipos que no es un functor legal aunque fmapse pueda implementar la firma de tipo de :

    data Q a = Q(a -> Int, a)
    fmap :: (a -> b) -> Q a -> Q b
    fmap f (Q(g, x)) = Q(\_ -> g x, f x)  -- this fails the functor laws!

El aspecto curioso de este ejemplo es que podemos poner en práctica fmapdel tipo correcto a pesar de que Fno es posible que sea un funtor, ya que utiliza aen una posición contravariante. Por lo tanto, esta implementación que se fmapmuestra arriba es engañosa, a pesar de que tiene la firma de tipo correcta (creo que esta es la única implementación posible de esa firma de tipo), las leyes de functor no están satisfechas. Por ejemplo, fmap idid, porque let (Q(f,_)) = fmap id (Q(read,"123")) in f "456"es 123, pero let (Q(f,_)) = id (Q(read,"123")) in f "456"es 456.

De hecho, Fes solo un profunctor, no es ni un functor ni un contrafunctor.

  • Un functor legal que no es aplicativo porque la firma de tipo de pureno se puede implementar: tome la mónada Writer (a, w)y elimine la restricción que wdebería ser un monoide. Entonces es imposible construir un valor de tipo a (a, w)partir de a.

  • Un funtor aplicativo que no es porque el tipo de firma <*>no pueden ser implementadas: data F a = Either (Int -> a) (String -> a).

  • Un functor que no es legalmente aplicativo a pesar de que se pueden implementar los métodos de clase de tipo:

    data P a = P ((a -> Int) -> Maybe a)

El constructor de tipos Pes un functor porque asolo se usa en posiciones covariantes.

instance Functor P where
   fmap :: (a -> b) -> P a -> P b
   fmap fab (P pa) = P (\q -> fmap fab $ pa (q . fab))

La única implementación posible de la firma de tipo de <*>es una función que siempre devuelve Nothing:

 (<*>) :: P (a -> b) -> P a -> P b
 (P pfab) <*> (P pa) = \_ -> Nothing  -- fails the laws!

Pero esta implementación no satisface la ley de identidad para los actores aplicativos.

  • Un functor que es Applicativepero no unMonad porque la firma de tipo de bindno se puede implementar.

¡No conozco ninguno de esos ejemplos!

  • Un functor que es Applicativepero noMonad porque las leyes no se pueden cumplir a pesar de que bindse puede implementar la firma de tipo .

Este ejemplo ha generado bastante discusión, por lo que es seguro decir que demostrar que este ejemplo es correcto no es fácil. Pero varias personas han verificado esto independientemente por diferentes métodos. Ver Es `datos PoE a = Vacío | Par aa` una mónada? para discusión adicional.

 data B a = Maybe (a, a)
   deriving Functor

 instance Applicative B where
   pure x = Just (x, x)
   b1 <*> b2 = case (b1, b2) of
     (Just (x1, y1), Just (x2, y2)) -> Just((x1, x2), (y1, y2))
     _ -> Nothing

Es algo engorroso demostrar que no hay una Monadinstancia legal . La razón del comportamiento no monádico es que no hay una forma natural de implementar bindcuándo una función f :: a -> B bpodría regresar Nothingo Justpara diferentes valores de a.

Quizás sea más claro considerarlo Maybe (a, a, a), que tampoco es una mónada, y tratar de implementarlo join. Uno encontrará que no hay una forma intuitiva razonable de implementación join.

 join :: Maybe (Maybe (a, a, a), Maybe (a, a, a), Maybe (a, a, a)) -> Maybe (a, a, a)
 join Nothing = Nothing
 join Just (Nothing, Just (x1,x2,x3), Just (y1,y2,y3)) = ???
 join Just (Just (x1,x2,x3), Nothing, Just (y1,y2,y3)) = ???
 -- etc.

En los casos indicados por ???, parece claro que no podemos producir Just (z1, z2, z3)de manera razonable y simétrica a partir de seis valores diferentes de tipo a. Ciertamente podríamos elegir algún subconjunto arbitrario de estos seis valores, por ejemplo, siempre tomar el primer no vacío Maybe, pero esto no satisfaría las leyes de la mónada. Regresar Nothingtampoco satisfará las leyes.

  • Una estructura de datos en forma de árbol que no es una mónada a pesar de que tiene asociatividad bind, pero no cumple con las leyes de identidad.

La mónada usual en forma de árbol (o "un árbol con ramas en forma de functor") se define como

 data Tr f a = Leaf a | Branch (f (Tr f a))

Esta es una mónada gratis sobre el functor f. La forma de los datos es un árbol donde cada punto de ramificación es un "functor-ful" de subárboles. El árbol binario estándar se obtendría con type f a = (a, a).

Si modificamos esta estructura de datos haciendo también las hojas en forma de functor f, obtenemos lo que yo llamo una "semimonada", bindque cumple con las leyes de naturalidad y asociatividad, pero su puremétodo falla una de las leyes de identidad. "Los semimonads son semigrupos en la categoría de endofunctores, ¿cuál es el problema?" Esta es la clase de tipo Bind.

Por simplicidad, defino el joinmétodo en lugar de bind:

 data Trs f a = Leaf (f a) | Branch (f (Trs f a))
 join :: Trs f (Trs f a) -> Trs f a
 join (Leaf ftrs) = Branch ftrs
 join (Branch ftrstrs) = Branch (fmap @f join ftrstrs)

El injerto de rama es estándar, pero el injerto de hoja no es estándar y produce a Branch. Esto no es un problema para la ley de asociatividad, pero rompe una de las leyes de identidad.

¿Cuándo los tipos polinómicos tienen instancias de mónada?

Ninguno de los funtores Maybe (a, a)y Maybe (a, a, a)se puede dar un legítimo Monadejemplo, a pesar de que son obviamente Applicative.

Estos functores no tienen trucos, Voidni en bottomningún lado, ni pereza / rigidez engañosas, ni estructuras infinitas, ni restricciones de clase de tipo. La Applicativeinstancia es completamente estándar. Las funciones returny bindse pueden implementar para estos functores pero no satisfarán las leyes de la mónada. En otras palabras, estos functores no son mónadas porque falta una estructura específica (pero no es fácil entender qué es exactamente lo que falta). Como ejemplo, un pequeño cambio en el functor puede convertirlo en una mónada: data Maybe a = Nothing | Just aes una mónada. Otro functor similar data P12 a = Either a (a, a)es también una mónada.

Construcciones para mónadas polinómicas

En general, aquí hay algunas construcciones que producen Monads legales a partir de tipos polinomiales. En todas estas construcciones, Mes una mónada:

  1. type M a = Either c (w, a)donde whay un monoide
  2. type M a = m (Either c (w, a))donde mhay alguna mónada y wes cualquier monoide
  3. type M a = (m1 a, m2 a)donde m1y m2son mónadas
  4. type M a = Either a (m a)¿Dónde mestá alguna mónada?

La primera construcción es WriterT w (Either c), la segunda construcción es WriterT w (EitherT c m). La tercera construcción es un producto de mónadas pure @Mpor componentes : se define como el producto por componentes de pure @m1y pure @m2, y join @Mse define omitiendo datos de productos cruzados (por ejemplo, m1 (m1 a, m2 a)se asigna m1 (m1 a)omitiendo la segunda parte de la tupla):

 join :: (m1 (m1 a, m2 a), m2 (m1 a, m2 a)) -> (m1 a, m2 a)
 join (m1x, m2x) = (join @m1 (fmap fst m1x), join @m2 (fmap snd m2x))

La cuarta construcción se define como

 data M m a = Either a (m a)
 instance Monad m => Monad M m where
    pure x = Left x
    join :: Either (M m a) (m (M m a)) -> M m a
    join (Left mma) = mma
    join (Right me) = Right $ join @m $ fmap @m squash me where
      squash :: M m a -> m a
      squash (Left x) = pure @m x
      squash (Right ma) = ma

He comprobado que las cuatro construcciones producen mónadas legales.

Yo conjeturo que no hay otras construcciones para mónadas polinómicas. Por ejemplo, el functor Maybe (Either (a, a) (a, a, a, a))no se obtiene a través de ninguna de estas construcciones y, por lo tanto, no es monádico. Sin embargo, Either (a, a) (a, a, a)es monádica porque es isomorfo al producto de tres mónadas a, a, y Maybe a. Además, Either (a,a) (a,a,a,a)es monádico porque es isomorfo al producto de ay Either a (a, a, a).

Las cuatro construcciones mostradas arriba nos permitirán obtener cualquier suma de cualquier número de productos de cualquier número de a's, por ejemplo, Either (Either (a, a) (a, a, a, a)) (a, a, a, a, a))etc. Todos los constructores de tipo tendrán (al menos una) Monadinstancia.

Queda por ver, por supuesto, qué casos de uso podrían existir para tales mónadas. Otro problema es que las Monadinstancias derivadas a través de las construcciones 1-4 en general no son únicas. Por ejemplo, al constructor de tipos type F a = Either a (a, a)se le puede dar una Monadinstancia de dos maneras: mediante la construcción 4 con la mónada (a, a)y mediante la construcción 3 con el isomorfismo de tipo Either a (a, a) = (a, Maybe a). Nuevamente, encontrar casos de uso para estas implementaciones no es inmediatamente obvio.

Queda una pregunta: dado un tipo de datos polinómico arbitrario, cómo reconocer si tiene una Monadinstancia. No sé cómo demostrar que no hay otras construcciones para mónadas polinómicas. No creo que exista ninguna teoría hasta ahora para responder a esta pregunta.

winitzki
fuente
Creo que B es una mónada. ¿Puedes dar un contraejemplo a este enlace Pair x y >>= f = case (f x, f y) of (Pair x' _,Pair _ y') -> Pair x' y' ; _ -> Empty?
Franky
@Franky Associativity falla con esta definición cuando eliges ftal que f xes Emptypero f yes un Pair, y en el siguiente paso ambos lo son Pair. Verifiqué a mano que las leyes no son válidas para esta implementación o para cualquier otra implementación. Pero es bastante trabajo hacer eso. ¡Ojalá hubiera una manera más fácil de resolver esto!
winitzki
1
@Turion Ese argumento no se aplica Maybeporque Maybeno contiene valores diferentes de los aque preocuparse.
Daniel Wagner
1
@Turion Probé esto con un par de páginas de cálculos; El argumento sobre la "forma natural" es sólo una explicación heurística. Una Monadinstancia consta de funciones returny bindque satisfacen las leyes. Hay dos implementaciones returny 25 implementaciones bindque se ajustan a los tipos requeridos. Puede mostrar mediante cálculo directo que ninguna de las implementaciones cumple con las leyes. Para reducir la cantidad de trabajo requerido, utilicé en joinlugar de bindy usé primero las leyes de identidad. Pero ha sido un buen trabajo.
winitzki
1
@duplode No, no creo que Traversablesea ​​necesario. m (Either a (m a))se transforma usando pure @men m (Either (m a) (m a)). Luego trivialmente Either (m a) (m a) -> m a, y podemos usar join @m. Esa fue la implementación por la cual revisé las leyes.
winitzki