простое обобщение класса типов Applicative (Functor);сопоставление с образцом на конструкторах - PullRequest
6 голосов
/ 10 сентября 2011

Я пытался «научить меня Хаскеллу» через онлайн-книгу ЛЯХ .

Автор описывает поведение функторов типа Applicative как своего рода способность извлекать функцию из одного функтора и отображать ее поверх второго функтора; это через функцию <*>, объявленную для класса типа Applicative:

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

В качестве простого примера тип Maybe является экземпляром Applicative в следующей реализации:

    instance Applicative Maybe where  
    pure = Just  
    Nothing <*> _ = Nothing  
    (Just f) <*> something = fmap f something  

Пример поведения, упомянутого ранее:

ghci> Just (*2) <*> Just 10         -- evaluates to Just 20

поэтому оператор <*> извлекает функцию (* 2) из ​​первого операнда и отображает ее во втором операнде.

Теперь в аппликативных типах оба операнда <*> относятся к одному и тому же типу, поэтому я подумал, почему бы не попробовать реализовать обобщение этого поведения, где два операнда являются функторами разных типов, поэтому я мог оценить как то так:

Just (2*) <*:*> [1,2,3,4]  -- should evaluate to [2,4,6,8]

Так вот что я придумал:

import Control.Applicative

class (Applicative f, Functor g) => DApplicative f g where
    pure1 :: a -> f a
    pure1 = pure
    (<*:*>) :: f ( a -> b )  -> g a -> g b      -- referred below as (1)

instance DApplicative Maybe [] where    -- an "instance pair" of this class
    (Just func) <*:*> g = fmap func g

main = do putStrLn(show x)
    where x = Just (2*) <*:*> [1,2,3,4]  -- it works, x equals [2,4,6,8]

Теперь, хотя вышеприведенное работает, мне интересно, сможем ли мы добиться большего успеха; Можно ли дать реализацию по умолчанию для <*: *>, которая может быть применена к различным парам f & g, в объявлении для самого DApplicative f g? И это приводит меня к следующему вопросу: есть ли способ сопоставления с образцом в конструкторах разных типов данных?

Я надеюсь, что мои вопросы имеют какой-то смысл, и я не просто извергаю чепуху (если да, пожалуйста, не будьте слишком резкими; я всего лишь новичок в ФП после его сна ...)

Ответы [ 2 ]

5 голосов
/ 10 сентября 2011

Это имеет смысл, но в конечном итоге это не особенно полезно в его нынешнем виде. Проблема именно в том, что вы заметили: нет способа предоставить значение по умолчанию, которое делает разумные вещи с различными типами, или вообще преобразовать из f в g. Таким образом, у вас будет квадратичный взрыв в количестве экземпляров, которые вам нужно написать.

Вы не завершили DApplicative экземпляр. Вот полная реализация для Maybe и []:

instance DApplicative Maybe [] where    -- an "instance pair" of this class
    (Just func) <*:*> g = fmap func g
    Nothing     <*:*> g = []

Это объединяет поведение как Maybe, так и [], потому что с Just он делает то, что вы ожидаете, но с Nothing он ничего не возвращает, пустой список.

Итак, вместо того, чтобы писать DApplicative, который принимает два разных типа, что если бы у вас был способ объединить два аппликатива f и g в один тип? Если вы обобщите это действие, вы можете использовать стандарт Applicative с новым типом.

Это можно сделать с помощью стандартной формулировки аппликаций, как

liftAp :: f (g (a -> b)) -> f (g a) -> f (g b)
liftAp l r = (<*>) <$> l <*> r

но вместо этого давайте изменим Maybe:

import Control.Applicative

newtype MaybeT f a = MaybeT { runMaybeT :: f (Maybe a) }

instance (Functor f) => Functor (MaybeT f) where
    fmap f (MaybeT m) = MaybeT ((fmap . fmap) f m)

instance (Applicative f) => Applicative (MaybeT f) where
    pure a = MaybeT (pure (pure a))
    (MaybeT f) <*> (MaybeT m) = MaybeT ( (<*>) <$> f <*> m)

Теперь вам просто нужен способ преобразовать что-то во внутреннем аппликативном f в комбинированное аппликативное MaybeT f:

lift :: (Functor f) => f a -> MaybeT f a
lift = MaybeT . fmap Just

Это похоже на множество шаблонов, но GHC может автоматически получить почти все из них.

Теперь вы можете легко использовать комбинированные функции:

*Main Control.Applicative> runMaybeT $ pure (*2) <*> lift [1,2,3,4]
[Just 2,Just 4,Just 6,Just 8]

*Main Control.Applicative> runMaybeT $ MaybeT (pure Nothing) <*> lift [1,2,3,4]
[Nothing,Nothing,Nothing,Nothing]

Такое поведение в Nothing может быть удивительным, но если вы думаете о списке как о представлении индетерминизма, вы, вероятно, сможете увидеть, как он может быть полезен. Если вам нужно двойное поведение возврата либо Just [a] или Nothing, вам просто нужен преобразованный список ListT Maybe a.

Это не совсем то же самое, что экземпляр, который я написал для DApplicative. Причина в из-за типов. DApplicative преобразует f в g. Это возможно только тогда, когда вы знаете конкретные f и g. Чтобы обобщить его, результат должен сочетать поведение как f, так и g, как это делает реализация.

Все это работает и с Монадами. Преобразованные типы, такие как MaybeT, предоставляются библиотеками монадных преобразователей, такими как mtl .

1 голос
/ 10 сентября 2011

Ваш DApplicative экземпляр для Maybe не завершен: что должно произойти, если первый аргумент <*:*> равен Nothing?

Выбор того, что делать для каждой комбинации, неясен. Для двух списков <*> будет генерировать все комбинации: [(+2),(+10)] <*> [3,4] дает [5,6,13,14]. В течение двух ZipList s у вас есть поведение, похожее на почтовый индекс: (ZipList [(+2),(+10)]) <*> (ZipList [3,4]) дает [5,14]. Таким образом, вы должны выбрать одно из двух возможных поведений для DApplicative списка и ZipList, «правильной» версии не существует.

...