Desculpe, eu realmente não sei minha matemática, então estou curioso para saber como pronunciar as funções na typeclass Applicative
Saber sua matemática, ou não, é irrelevante aqui, eu acho. Como você provavelmente sabe, Haskell empresta alguns bits de terminologia de vários campos da matemática abstrata, mais notavelmente a Teoria das Categorias , de onde obtemos functores e mônadas. O uso desses termos em Haskell diverge um pouco das definições matemáticas formais, mas eles geralmente são próximos o suficiente para serem bons termos descritivos de qualquer maneira.
A Applicativeclasse de tipo fica em algum lugar entre Functore Monad, portanto, seria de se esperar que tivesse uma base matemática semelhante. A documentação do Control.Applicativemódulo começa com:
Este módulo descreve uma estrutura intermediária entre um functor e uma mônada: ele fornece expressões puras e sequenciamento, mas sem vinculação. (Tecnicamente, um forte functor monoidal frouxo.)
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Não tão cativante quanto Monad, eu acho.
Tudo isso basicamente se resume a que Applicativenão corresponde a nenhum conceito que seja particularmente interessante matematicamente, então não há termos prontos por aí que capturem a forma como é usado em Haskell. Portanto, deixe a matemática de lado por enquanto.
Se quisermos saber como chamá- (<*>)lo, talvez seja útil saber o que significa basicamente.
Então, o que há com Applicative, de qualquer maneira, e por que não podemos chamá-lo assim?
ApplicativeNa prática, o que equivale a uma maneira de elevar funções arbitrárias a um Functor. Considere a combinação de Maybe(sem dúvida o não trivial mais simples Functor) e Bool(da mesma forma o tipo de dados não trivial mais simples).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
A função fmappermite-nos levantar notde trabalhar em Boolpara trabalhar em Maybe Bool. Mas e se quisermos levantar (&&)?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Bem, não é isso que queremos de forma alguma ! Na verdade, é praticamente inútil. Podemos tentar ser inteligente e sneak outro Boolpara Maybeatravés da parte traseira ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... mas isso não é bom. Por um lado, está errado. Por outro lado, é feio . Poderíamos continuar tentando, mas descobrimos que não há como levantar uma função de vários argumentos para trabalhar em um arbitrárioFunctor . Irritante!
Por outro lado, poderíamos fazer isso facilmente se Maybeusássemos a Monadinstância de:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Agora, é muito complicado traduzir uma função simples - é por isso que Control.Monadfornece uma função para fazer isso automaticamente liftM2,. O 2 em seu nome se refere ao fato de que ele funciona em funções de exatamente dois argumentos; funções semelhantes existem para funções de 3, 4 e 5 argumentos. Essas funções são melhores , mas não perfeitas, e especificar o número de argumentos é feio e desajeitado.
O que nos leva ao artigo que introduziu a classe do tipo Aplicativa . Nele, os autores fazem essencialmente duas observações:
- Transformar funções com vários argumentos em um
Functoré uma coisa muito natural de se fazer
- Fazer isso não requer todos os recursos de um
Monad
A aplicação da função normal é escrita pela simples justaposição de termos, então para tornar a "aplicação elevada" o mais simples e natural possível, o artigo apresenta operadores infixados para substituir a aplicação, elevada para oFunctor , e uma classe de tipo para fornecer o que é necessário para isso .
Tudo isso nos leva ao seguinte ponto: (<*>)simplesmente representa a aplicação da função - então por que pronunciá-lo de forma diferente do que você faz com o "operador de justaposição" de espaço em branco?
Mas se isso não for muito satisfatório, podemos observar que o Control.Monadmódulo também fornece uma função que faz a mesma coisa para mônadas:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Onde ap, é claro, é a abreviação de "aplicar". Como qualquer um Monadpode ser Applicative, e apprecisa apenas do subconjunto de recursos presentes neste último, talvez possamos dizer que, se (<*>)não fosse um operador, deveria ser chamado ap.
Também podemos abordar as coisas de outra direção. A Functoroperação de levantamento é chamada fmapporque é uma generalização da mapoperação em listas. Que tipo de função nas listas funcionaria (<*>)? Há o que apfunciona nas listas, é claro, mas isso não é particularmente útil por si só.
Na verdade, talvez haja uma interpretação mais natural para listas. O que vem à mente quando você olha a seguinte assinatura de tipo?
listApply :: [a -> b] -> [a] -> [b]
Há algo muito tentador na ideia de alinhar as listas em paralelo, aplicando cada função da primeira ao elemento correspondente da segunda. Infelizmente para nosso velho amigo Monad, essa operação simples viola as leis da mônada se as listas tiverem comprimentos diferentes. Mas vale uma multa Applicative, caso em que (<*>)se torna uma forma de encadear uma versão generalizada de zipWith, então talvez possamos imaginar chamá-la fzipWith?
Esta ideia compactada realmente nos traz um círculo completo. Lembra daquela coisa de matemática anterior, sobre functores monoidais? Como o nome sugere, essas são uma forma de combinar a estrutura de monoides e functores, ambos os quais são classes do tipo Haskell familiares:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Como eles ficariam se você os colocasse em uma caixa e sacudisse um pouco? A partir de Functor, manteremos a ideia de uma estrutura independente de seu parâmetro de tipo e de Monoidmanteremos a forma geral das funções:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Não queremos presumir que existe uma maneira de criar um verdadeiro "vazio" Functore não podemos conjurar um valor de um tipo arbitrário, portanto, corrigiremos o tipo de mfEmptyas f ().
Também não queremos forçar mfAppenda necessidade de um parâmetro de tipo consistente, então agora temos isto:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Qual é o tipo de resultado mfAppend? Temos dois tipos arbitrários dos quais nada sabemos, portanto, não temos muitas opções. O mais sensato é apenas manter ambos:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
Nesse ponto mfAppendagora é claramente uma versão generalizada de zipnas listas, e podemos reconstruir Applicativefacilmente:
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Isso também nos mostra que pureestá relacionado ao elemento de identidade de a Monoid, então outros nomes bons para ele podem ser qualquer coisa que sugira um valor unitário, uma operação nula ou algo parecido.
Isso foi demorado, para resumir:
(<*>) é apenas um aplicativo de função modificada, então você pode lê-lo como "ap" ou "aplicar", ou elimina-o inteiramente da maneira que faria com um aplicativo de função normal.
(<*>)também generaliza zipWithem listas, de modo que você pode lê-lo como "zip functores com", similarmente a ler fmapcomo "mapear um functor com".
O primeiro está mais próximo da intenção da Applicativeclasse de tipo - como o nome sugere - então é isso que eu recomendo.
Na verdade, eu encorajo o uso liberal e a não pronúncia de todos os operadores de aplicativos suspensos :
(<$>), que transforma uma função de argumento único em um Functor
(<*>), que encadeia uma função com vários argumentos por meio de um Applicative
(=<<), que vincula uma função que insere um Monadem um cálculo existente
Todos os três são, no fundo, apenas aplicações de funções regulares, um pouco apimentadas.