Lo siento, realmente no sé mis matemáticas, así que tengo curiosidad por saber cómo pronunciar las funciones en la clase de tipos Aplicativos
Saber sus matemáticas, o no, es en gran parte irrelevante aquí, creo. Como probablemente sepa, Haskell toma prestados algunos fragmentos de terminología de varios campos de las matemáticas abstractas, sobre todo la teoría de categorías , de donde obtenemos functores y mónadas. El uso de estos términos en Haskell difiere un poco de las definiciones matemáticas formales, pero por lo general están lo suficientemente cerca como para ser buenos términos descriptivos de todos modos.
La Applicativeclase de tipo se encuentra en algún lugar entre Functory Monad, por lo que uno esperaría que tuviera una base matemática similar. La documentación del Control.Applicativemódulo comienza con:
Este módulo describe una estructura intermedia entre un funtor y una mónada: proporciona expresiones puras y secuenciación, pero sin vinculación. (Técnicamente, un functor monoidal laxo fuerte).
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
No es tan pegadizo como Monad, creo.
Básicamente, todo esto se reduce a que Applicativeno corresponde a ningún concepto que sea particularmente interesante matemáticamente, por lo que no hay términos prefabricados que capturen la forma en que se usa en Haskell. Por lo tanto, deje las matemáticas a un lado por ahora.
Si queremos saber cómo llamarlo (<*>), puede ser útil saber qué significa básicamente.
Entonces, ¿qué pasa Applicative, de todos modos, y por qué lo llamamos así?
En la Applicativepráctica, lo que equivale a una forma de convertir funciones arbitrarias en a Functor. Considere la combinación de Maybe(posiblemente el tipo de datos no trivial más simple Functor) y Bool(del mismo modo, el tipo de datos no trivial más simple).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
La función fmapnos permite pasar notde trabajar Boola trabajar Maybe Bool. ¿Pero y si queremos levantarnos (&&)?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Bueno, ¡eso no es lo que queremos en absoluto ! De hecho, es bastante inútil. Podemos tratar de ser inteligentes y colarse otra Boolen Maybepor la parte posterior ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... pero eso no es bueno. Por un lado, está mal. Por otra parte, es feo . Podríamos seguir intentándolo, pero resulta que no hay forma de levantar una función de múltiples argumentos para trabajar en un arbitrarioFunctor . ¡Molesto!
Por otro lado, podríamos hacerlo fácilmente si usamos Maybela Monadinstancia de:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Ahora, traducir una función simple es muy complicado, por lo que Control.Monadproporciona una función para hacerlo automáticamente liftM2. El 2 en su nombre se refiere al hecho de que trabaja en funciones de exactamente dos argumentos; existen funciones similares para funciones de 3, 4 y 5 argumentos. Estas funciones son mejores , pero no perfectas, y especificar el número de argumentos es feo y torpe.
Lo que nos lleva al artículo que introdujo la clase de tipo Aplicativo . En él, los autores hacen esencialmente dos observaciones:
- Elevar funciones de múltiples argumentos a un
Functores algo muy natural de hacer
- Hacerlo no requiere todas las capacidades de un
Monad
La aplicación de función normal se escribe mediante una simple yuxtaposición de términos, por lo que para hacer que la "aplicación elevada" sea lo más simple y natural posible, el artículo presenta operadores infijos para sustituir la aplicación, elevados enFunctor y una clase de tipo para proporcionar lo que se necesita para eso. .
Todo lo cual nos lleva al siguiente punto: (<*>)simplemente representa la aplicación de la función, entonces, ¿por qué pronunciarla de manera diferente a como lo hace el "operador de yuxtaposición" de espacios en blanco?
Pero si eso no es muy satisfactorio, podemos observar que el Control.Monadmódulo también proporciona una función que hace lo mismo para las mónadas:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Donde apes, por supuesto, la abreviatura de "aplicar". Dado que cualquiera Monadpuede ser Applicative, y apnecesita solo el subconjunto de características presentes en este último, quizás podamos decir que si (<*>)no fuera un operador, debería llamarse ap.
También podemos abordar las cosas desde la otra dirección. La Functoroperación de elevación se llama fmapporque es una generalización de la mapoperación en listas. ¿Qué tipo de función en las listas funcionaría (<*>)? Hay lo que aphace en las listas, por supuesto, pero eso no es particularmente útil por sí solo.
De hecho, existe una interpretación quizás más natural para las listas. ¿Qué le viene a la mente cuando mira la siguiente firma de tipo?
listApply :: [a -> b] -> [a] -> [b]
Hay algo tan tentador en la idea de alinear las listas en paralelo, aplicando cada función de la primera al elemento correspondiente de la segunda. Desafortunadamente para nuestro viejo amigo Monad, esta simple operación viola las leyes de las mónadas si las listas tienen diferentes longitudes. Pero funciona bien Applicative, en cuyo caso se (<*>)convierte en una forma de encadenar una versión generalizada de zipWith, así que quizás podamos imaginarnos llamarlo fzipWith.
Esta idea de cremallera en realidad nos trae un círculo completo. ¿Recuerda lo anterior de las matemáticas, sobre los functores monoidales? Como su nombre indica, estas son una forma de combinar la estructura de monoides y functores, los cuales son clases de tipos familiares de Haskell:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
¿Cómo se verían estos si los pones en una caja juntos y la agitas un poco? De Functormantendremos la idea de una estructura independiente de su parámetro de tipo , y de Monoidmantendremos la forma general de las funciones:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
No queremos asumir que hay una manera de crear un verdadero "vacío" Functor, y no podemos conjurar un valor de un tipo arbitrario, así que arreglaremos el tipo de mfEmptycomo f ().
Tampoco queremos forzar la mfAppendnecesidad de un parámetro de tipo consistente, así que ahora tenemos esto:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
¿Para qué es el tipo de resultado mfAppend? Tenemos dos tipos arbitrarios de los que no sabemos nada, por lo que no tenemos muchas opciones. Lo más sensato es mantener ambos:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
En ese punto mfAppendahora es claramente una versión generalizada de zipen listas, y podemos reconstruir Applicativefácilmente:
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Esto también nos muestra que pureestá relacionado con el elemento de identidad de a Monoid, por lo que otros buenos nombres para él podrían ser cualquier cosa que sugiera un valor unitario, una operación nula o algo así.
Eso fue largo, para resumir:
(<*>) es sólo una aplicación de función modificada, por lo que puede leerla como "ap" o "aplicar", o elírsela por completo como lo haría con una aplicación de función normal.
(<*>)también generaliza de forma aproximada zipWithen listas, por lo que puede leerlo como "zip fmapfunctor con" , de manera similar a leer como "mapear un functor con".
El primero está más cerca de la intención de la Applicativeclase de tipo, como sugiere el nombre, así que eso es lo que recomiendo.
De hecho, recomiendo el uso liberal y la no pronunciación de todos los operadores de aplicaciones levantados :
(<$>), que eleva una función de un solo argumento a una Functor
(<*>), que encadena una función de múltiples argumentos a través de una Applicative
(=<<), que une una función que ingresa a Monaden un cálculo existente
Los tres son, en el fondo, solo una aplicación de función normal, un poco condimentada.