В Haskell, почему аппликативам нужно воспринимать морфизмы и данные в одном и том же контексте? - PullRequest
2 голосов
/ 07 ноября 2019

Я новичок в Хаскеле. Это может быть глупым вопросом.

Поскольку класс типов Applicative имеет функцию apply , которая принимает функции и данные в одном контексте. Почему он не может быть другим и более общим.

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

Почему мы не можем написать что-то подобное

class Functor f => Applicative f where
    (<*>) :: Functor g => g (a -> b) -> f a -> g (f b)
    (<*>) gab fa = fmap (\g -> fmap g fa) gab

    (<<*>>) :: Functor g => (g (f a) -> f a) -> g (a -> b) -> f a -> f b
    (<<*>>) peelOuter gab fa = peelOuter $ gab <*> fa

    (>>*<<) :: Functor g => (g (f a) -> g a) -> g (a -> b) -> f a -> g b
    (>>*<<) cleanInner gab fa = cleanInner $ gab <*> fa

Это можно использовать как показано ниже

-- Extract List from maybe
elfm :: Maybe [a] -> [a]
elfm Nothing = []
elfm (Just xs) = xs

-- Fuse List elements in Maybe []
flem :: Monoid a => Maybe [a] -> Maybe a
flem Nothing = mempty
flem (Just xs) = Just $ foldl (<>) mempty xs

Just (*2) <*> [1,2,3,4]
-- Just [2,4,6,8]
(<<*>>) elfm (Just (*2)) [1,2,3,4]
-- [2,4,6,8]
(>>*<<) flem (Just (++ "Haskell")) ["Hello, "]
-- Just "Hello, Haskell"

И я прочитал, что весь смысл наличия Applicative - это недостаток Functors, поднимающих функции с несколькими аргументами. Правильно ли это?

И я не думаю, что приложение функции соответствует ожиданиям.

add :: Num a => a -> a -> a
add a b = a + b

-- I want to apply [1,2,3] as First arguments and [4,5,6] as 2nd arguments.
-- Like add 1 4, add 2 4, add 3 6
-- But it is give all possibilities of combinations like a tree

--                          <*>
--      (+1)                (+2)            (+3)
-- (1+4)(1+5)(1+6)  (2+4)(2+5)(2+6)  (3+4)(3+5)(3+6)

А также они сравниваются с пакетной обработкой, но не приведен вполне реальный пример из жизни. Пожалуйста, приведите пример для этого.

Ответы [ 2 ]

3 голосов
/ 07 ноября 2019

Каждый экземпляр Applicative обязательно имеет свою собственную реализацию <*>. Вот почему у нас есть классы типов. В вашем коде есть все методы, определенные в самом классе, для экземпляров ничего не осталось. Это означает, что класса классов не так много. Там просто куча общих функций. Все мясо делегировано аргументам peelOuter и cleanInner из ('<<*>>) и (>>*<<). Давайте посмотрим на них более внимательно. Они более или менее симметричны, так что (<<*>>) должно быть достаточно.

(<<*>>) :: Functor g => (g (f a) -> f a) -> g (a -> b) -> f a -> f b
(<<*>>) peelOuter gab fa = peelOuter $ gab <*> fa

На самом деле peelOuter должен был быть методом класса типов, но с этим связано несколько проблем.

Первая проблема состоит в том, что задействованы два функтора, и peelOuter необходимо реализовать отдельно для каждой пары функторов. То есть у нас здесь будет класс бипараметрического типа ApplicativePair, и нам потребуется отдельный экземпляр для каждой пары.

Вторая проблема заключается в том, что peelOuter не может быть реализовано для каждой пары bonaфидеры Applicative. Нельзя извлечь Id a из Maybe (Id a) или [a] из IO [a], или ...

Хуже того, пока не ясно, всегда ли это можно реализовать, когда f и g являются одним и тем же функтором. Понятно, что когда f является монадой, тогда это просто join. Однако не все аппликативы являются монадами, и join - это именно то, чего не хватает аппликативному, чтобы быть монадой. Так что peelOuter, даже если такой тип реализуем, нарушит некоторые законы монады. Это плохо? Не обязательно, если это все еще следует применимым законам. Однако вы не предоставили никаких законов, только набор функций.

1 голос
/ 07 ноября 2019

Любые два функтора: Функтор и Аппликативные функторы. Это закодировано как newtype Compose, см. Data.Functor.Compose.

Итак, ваши примеры можно решить с помощью newtype Compose.

-- Just (*2) <*> [1,2,3,4]
getCompose $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or
getCompose $ (*2) <$> Compose (Just [1,2,3,4])
-- Just [2,4,6,8]

-- (<<*>>) elfm (Just (*2)) [1,2,3,4]
elfm . getCompose $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or with toList (method of Foldable)
toList $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or
toList $ (*2) <$> Compose (Just [1,2,3,4])
-- [2,4,6,8]

-- (>>*<<) flem (Just (++ "Haskell")) ["Hello, "]
flem . getCompose $ pure (++ "Haskell") <*> Compose (Just ["Hello, "])
-- or with toList and listToMaybe
listToMaybe . toList $ pure (++ "Haskell") <*> Compose (Just ["Hello, "])
-- or
listToMaybe . toList $ (++ "Haskell") <$> Compose (Just ["Hello, "])
-- or with head :: Foldable f => f a -> Maybe a
head $ (++ "Haskell") <$> Compose (Just ["Hello, "])
-- Just "Hello, Haskell"

По поводу последнего вопроса. Вы получили ответ в комментариях @Robin Zigmond. Он писал о newtype ZipList. С ZipList вы можете сделать:

getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [4,5,6]
-- [5,7,9]

Итак, одна из целей newtype в Haskell - это возможность писать разные экземпляры для некоторого типа.

...