haskell - applicative functor
¿Hay un Functor que no pueda ser aplicable a la ley? (3)
Esto no es exactamente lo que estás buscando, pero está cerca, así que pensé en compartirlo. La mónada estándar de Writer
tiene una restricción adicional en su instancia de Apply
(es decir, que el tipo w
es una instancia de Semigroup
o Semigroup
) que la instancia de Functor
no tiene, por lo que Writer Foo
es un Functor
pero no se Apply
si Foo
no es un Semigroup
/ Monoid
:
data Writer w a = Writer w a
instance Monoid w => Apply (Writer w) where
Writer w1 f <.> Writer w2 x = Writer (mappend w1 w2) (f x)
Sin embargo, este no es realmente un ejemplo de lo que estás pidiendo, porque en realidad es posible crear una instancia de Apply
Monoid
ley sin una restricción Monoid
:
instance Apply (Writer w) where
Writer w1 f <.> Writer w2 x = Writer w1 (f x)
El problema con esta instancia es que no permite una instancia Applicative
coincidente, ya que no habría manera de implementar pure
tal manera que obtenga una identidad izquierda, es decir:
pure id <.> x /= x
Esta es solo una manera larga de darte la misma respuesta que ya tenías de Conor: una demostración que se basa en pure
para romper la instancia.
Una pregunta reciente hecha en general sobre los límites entre varias clases de Haskell. Se me ocurrió Handler
como ejemplo de un Functor
válido sin una instancia sensible de Apply
**, donde
class Functor f => Apply f where
(<.>) :: f (a -> b) -> f a -> f b
-- optional bits omitted.
Sin embargo, aún no he podido encontrar un ejemplo de un Functor
válido que no pueda convertirse en una instancia válida (si no tiene sentido) de Apply
. El hecho de que Apply
haya tenido (ver actualización) pero una sola ley,
(.) <$> u <.> v <.> w = u <.> (v <.> w)
Parece que esto es bastante complicado.
pigworker (Conor McBride) previamente dio un ejemplo de un Functor
que no es Applicative
, pero se basó en pure
para hacerlo, y eso no está disponible en Apply
.
** Luego, más tarde, me di cuenta de que en realidad podría haber una instancia de Apply
sensible (aunque algo extraña) para Handler
, que recopila conceptualmente excepciones simultáneas.
Actualizar
Edward Kmett ahora ha accepted dos leyes adicionales que propuse para Apply
(para validar las optimizaciones que hice en la instancia de Apply (Coyoneda f)
):
x <.> (f <$> y) = (. f) <$> x <.> y
f <$> (x <.> y) = (f .) <$> x <.> y
Sería interesante ver si estas adiciones cambian la respuesta a esta pregunta.
La "suma" de dos functors ( Data.Functor.Sum
from transformers
) parece ser un ejemplo.
Uno puede mapear fácilmente sobre una rama u otra, pero ¿cómo implementar <.>
Cuando la función en el funtor y el argumento en el funtor se encuentran en diferentes ramas?
ghci> import Data.Functor.Sum
ghci> import Data.Functor.Identity
ghci> let f = InL (Const ()) :: Sum (Const ()) Identity (Int -> Int)
ghci> let x = InR (Identity 5) :: Sum (Const ()) Identity Int
ghci$ f <.> x = ..... ?
Sí, hay Functor
s sin instancia de Apply
. Considere la suma de dos funciones (que son exponentes en los tipos de datos algebraicos ):
data EitherExp i j a
= ToTheI (i -> a)
| ToTheJ (j -> a)
Hay una instancia de Functor
para todos los i
s y j
s:
instance Functor (EitherExp i j) where
fmap f (ToTheI g) = ToTheI (f . g)
fmap f (ToTheJ g) = ToTheJ (f . g)
pero no hay una instancia de Apply
para todos i
s y j
s
instance Apply (EitherExp i j) where
...
ToTheI f <.> ToTheJ x = ____
No hay manera de llenar el espacio en blanco ____
. Para hacerlo, tendríamos que saber algo acerca de i
y j
, pero no hay manera de mirar dentro de cada tipo i
o j
en Haskell. La intuición rechaza esta respuesta; Si sabe algo acerca de i
o j
, al igual que están habitados por un solo valor, puede escribir una instancia de Apply
para EitherExp
class Inhabited a where
something :: a
instance (Inhabited i, Inhabited j) => Apply (EitherExp i j) where
...
ToTheI f <.> ToTheJ x = ToTheI (const ((f something) (x something)))
Pero no sabemos que cada i
y cada j
están Inhabited
. El tipo Void
no está habitado por nada. Ni siquiera tenemos una manera de saber que cada tipo es Inhabited
o Void
.
Nuestra intuición es realmente muy buena; cuando podemos inspeccionar cómo se construyen los tipos, para los tipos de datos algebraicos no hay ningún Functor
que no tenga instancias de Apply
. Lo que sigue son dos respuestas que pueden ser más agradables a nuestra intuición.
No ...
... para tipos de datos algebraicos. Hay 3 posibilidades. La estructura es nula, la estructura puede estar vacía o la estructura no puede estar vacía. Si la estructura es nula, entonces es una Apply
absurd
. Si puede estar vacío, elija cualquier instancia vacía y devuélvala constantemente para cualquier solicitud. Si no puede estar vacío, entonces es una suma de estructuras que cada una no puede estar vacía, se puede hacer una aplicación respetuosa de la ley aplicando uno de los valores † del primero a uno de los valores del segundo y devolviéndolo En alguna estructura constante.
La ley aplicable es muy laxa. Aplicar no tiene por qué tener ningún sentido. No necesita ser "zip-y". No necesita ser fmap
cuando se combina con cosas sospechosas como pure
de Applicative
; no hay noción de pure
con la cual escribir una ley que requiera que tenga sentido.
Cuando la estructura puede estar vacía
Elija cualquier instancia vacía y devuélvala constantemente para cualquier solicitud
u <.> v = empty
Prueba
(.) <$> u <.> v <.> w = u <.> (v <.> w)
(((.) <$> u) <.> v) <.> w = u <.> (v <.> w) -- by infixl4 <$>, infixl4 <.>
(_ ) <.> w = u <.> (_ ) -- by substitution
empty = empty -- by definition of <.>
Cuando la estructura no puede estar vacía.
Si la estructura f
no puede estar vacía, existe una función extract :: forall a. fa -> a
extract :: forall a. fa -> a
. Elija otra función c :: forall a. a -> fa
c :: forall a. a -> fa
que siempre construye la misma estructura no vacía rellena con el argumento en todas partes y define:
u <.> v = c (extract u $ extract v)
Con los teoremas libres.
extract (f <$> u) = f (extract u)
extract . c = id
Prueba
(.) <$> u <.> v <.> w = u <.> (v <.> w)
(((.) <$> u) <.> v) <.> w = u <.> (v <.> w) -- by infixl4 <$>, infixl4 <.>
(c (extract ((.) <$> u) $ extract v)) <.> w = u <.> (v <.> w) -- by definition
(c ((.) (extract u) $ extract v)) <.> w = u <.> (v <.> w) -- by free theorem
c (extract (c ((.) (extract u) $ extract v)) $ extract w) = u <.> (v <.> w) -- by definition
c ( ((.) (extract u) $ extract v) $ extract w) = u <.> (v <.> w) -- by extract . c = id
c (((.) (extract u) $ extract v) $ extract w) = u <.> c (extract v $ extract w) -- by definition
c (((.) (extract u) $ extract v) $ extract w) = c (extract u $ extract (c (extract v $ extract w))) -- by definition
c (((.) (extract u) $ extract v) $ extract w) = c (extract u $ (extract v $ extract w) ) -- by extract . c = id
let u'' = extract u
v'' = extract v
w'' = extract w
c (((.) u'' $ v'') $ w'') = c (u'' $ (v'' $ w''))
c ((u'' . v'') $ w'') = c (u'' $ (v'' $ w'')) -- by definition of partial application of operators
c (u'' $ (v'' $ w'')) = c (u'' $ (v'' $ w'')) -- by definition of (.)
Un poco más merece ser dicho acerca de la definición de extract
para los tipos exponenciales, funciones. Para una función i -> a
hay dos posibilidades. O estoy habitado o no lo está. Si está habitada, elija algún habitante i
† y defina
extract f = f i
Si i
está deshabitado (es nulo), entonces i -> a
es el tipo de unidad con el único valor absurd
. Void -> a
es simplemente otro tipo de vacío que no contiene ninguna a
s; Trátela como una estructura que puede estar vacía.
Cuando la estructura es nula.
Cuando la estructura es nula no hay formas de construirla. Podemos escribir una función única de cada construcción posible (no hay ninguna para pasarla) a cualquier otro tipo.
absurd :: Void -> a
absurd x = case x of {}
Las estructuras fmap f = absurd
pueden ser fmap f = absurd
con fmap f = absurd
. De la misma manera pueden tener una instancia de Apply
con
(<.>) = absurd
Podemos probar trivialmente que para todos u
, v
y w
(.) <$> u <.> v <.> w = u <.> (v <.> w)
No hay u
, v
o w
y la afirmación es vacuamente verdadera .
† Con algunas advertencias sobre la aceptación del axioma de elección para elegir un índice a
para el tipo exponencial a -> b
Si ...
... por Haskell. Imagina que hay otra base de Monad
diferente a IO
, llamémosla OI
. Entonces Sum IO OI
es un Functor
pero nunca puede ser un Apply
.
... para el mundo real. Si tiene una máquina a la que puede enviar funciones (o flechas en una categoría que no sea Hask ), pero no puede combinar dos de las máquinas ni extraer su estado de ejecución, entonces son un Functor sin Aplicación.