Примеры аппликативных преобразователей Haskell

В вики на www.haskell.org рассказывается следующее об аппликативных преобразователях:

Так где же прикладные трансформаторы? Ответ заключается в том, что нам не нужны специальные преобразователи для аппликативных функторов, поскольку их можно комбинировать обычным образом. http://www.haskell.org/haskellwiki/Applicative_functor#Applicative_transfomers

Я попробовал следующее, чтобы попытаться объединить несколько аппликативных функторов. Но все, что у меня было, это куча ошибок. Вот код:

import Control.Applicative
import System.IO

ex x y = (:) <$> x <*> y 
test1 = ex "abc" ["pqr", "xyz"]  -- only this works correctly as expected
test2 = ex "abc" [Just "pqr", Just "xyz"]
test3 = ex "abc" (Just "pqr")
test4 = ex (Just 'a') ["pqr", "xyz"]
test5 = ex (return ("abc"):: IO ()) [Just "pqr", Just "xyz"]

Это приводит к множеству ошибок типа, которые, хотя я частично понимаю, я не мог их исправить.

Ошибки указаны в конце.

Итак, как мне, например, объединить аппликатив «Может быть» и аппликативный список?

Как, например, объединить государственного заявителя и кандидата в список? Есть ли какие-нибудь другие примеры, скажем, комбинирования Maybe и List, Maybe и State и, наконец, ужасного из всех заявлений IO и State?

Спасибо.

Сообщения об ошибках GHCi следуют ниже.

example.hs:6:19:
    Couldn't match expected type `[Char]' with actual type `Maybe a0'
    In the return type of a call of `Just'
    In the expression: Just "pqr"
    In the second argument of `ex', namely `[Just "pqr", Just "xyz"]'

example.hs:7:19:
    Couldn't match expected type `[[Char]]' with actual type `Maybe a0'
    In the return type of a call of `Just'
    In the second argument of `ex', namely `(Just "pqr")'
    In the expression: ex "abc" (Just "pqr")

example.hs:8:23:
    Couldn't match expected type `Maybe' with actual type `[]'
    In the second argument of `ex', namely `["pqr", "xyz"]'
    In the expression: ex (Just 'a') ["pqr", "xyz"]
    In an equation for `test4': test4 = ex (Just 'a') ["pqr", "xyz"]

example.hs:9:21:
    Couldn't match expected type `()' with actual type `[Char]'
    In the first argument of `return', namely `("abc")'
    In the first argument of `ex', namely `(return ("abc") :: IO ())'
    In the expression:
      ex (return ("abc") :: IO ()) [Just "pqr", Just "xyz"]
Failed, modules loaded: none.
Prelude>

person user1308560    schedule 25.09.2012    source источник
comment
Обратите внимание: не совсем верно, что вам больше не нужны трансформаторы при использовании аппликативов. Например. StateT s IO a - это Applicative, но это не композиция любых двух аппликативов.   -  person Turion    schedule 07.05.2018


Ответы (4)


В вики-статье говорится, что liftA2 (<*>) можно использовать для составления аппликативных функторов. Легко увидеть, как его использовать, по его типу:

o :: (Applicative f, Applicative f1) =>
     f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)
o = liftA2 (<*>)

Итак, если f равно Maybe, а f1 равно [], мы получаем:

> Just [(+1),(+6)] `o` Just [1, 6] 
Just [2,7,7,12]

Другой способ:

>  [Just (+1),Just (+6)] `o` [Just 1, Just 6]
[Just 2,Just 7,Just 7,Just 12]

Как сказал @McCann, ваша функция ex эквивалентна liftA2 (:):

test1 = liftA2 (:) "abc" ["pqr", "xyz"]

Чтобы использовать (:) с более глубоким прикладным стеком, вам потребуется несколько приложений liftA2:

*Main> (liftA2 . liftA2) (:) (Just "abc") (Just ["pqr", "xyz"])
Just ["apqr","axyz","bpqr","bxyz","cpqr","cxyz"]

Однако это работает только тогда, когда оба операнда одинаково глубоки. Таким образом, помимо двойного liftA2 вы должны использовать pure для исправления уровня:

*Main> (liftA2 . liftA2) (:) (pure "abc") (Just ["pqr", "xyz"])
Just ["apqr","axyz","bpqr","bxyz","cpqr","cxyz"]
person nponeccop    schedule 25.09.2012

Рассмотрим следующие сигнатуры типов:

liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

В совокупности получается тип:

liftA2 (<*>) :: (Applicative f, Applicative g) 
             => f (g (a -> b)) -> f (g a) -> f (g b)

Это действительно комбинация двух Applicative. Фактически, это комбинация ровно двух Applicative. Другими словами, хотя вы можете комбинировать Applicative обычным способом, это никоим образом не делается автоматически. Все должно быть явно поднято правильное количество раз.

Ваша функция ex эквивалентна функции liftA2 (:), имеющей тип (Applicative f) => f a -> f [a] -> f [a]. Просматривая ваши примеры, делая некоторые предположения о том, что вы хотели сделать:

test1 = ex "abc" ["pqr", "xyz"]

Здесь f равно [], и мы применяем его к аргументам типа [Char] и [[Char]].

test2 = ex "abc" [Just "pqr", Just "xyz"]

Второй аргумент имеет тип [Maybe [Char]], поэтому нам нужно поднять дважды. Первый аргумент также необходимо поднять, поскольку он имеет тип [Char] и должен быть [Maybe Char].

test3 = ex "abc" (Just "pqr")

На этот раз второй аргумент имеет тип Maybe [Char], поэтому f равно Maybe, и нам нужен только один подъем. Следовательно, первый аргумент должен иметь тип Maybe Char.

test4 = ex (Just 'a') ["pqr", "xyz"]

На этот раз первый аргумент - Maybe Char, а второй - [[Char]], так что у вас есть два совершенно разных Applicative; оба должны быть подняты, чтобы получить либо [Maybe Char], либо Maybe [Char].

test5 = ex (return ("abc"):: IO ()) [Just "pqr", Just "xyz"]

Сигнатура типа здесь не имеет смысла; ты наверное хотел IO [Char]. Второй аргумент имеет тип [Maybe [Char]]. Как и в предыдущем примере, они не совпадают, но на этот раз у вас есть три Applicatives. Если вы хотите что-то вроде IO [Maybe a], вам нужно будет поднять (:) все три раза, например liftA2 (liftA2 ex).

Такой способ объединения Applicatives называется «композицией функторов», и на странице, на которую вы ссылаетесь, упоминаются библиотеки, которые определяют явный конструктор типа композиции. Например, с использованием библиотеки transformers для описания пятого примера можно использовать такой тип, как Compose IO (Compose [] Maybe). Этот составной тип определяется как Applicative экземпляр вышеупомянутым общим способом и применяет правильное количество подъемных операций. Обратной стороной является то, что вам нужно будет оборачивать и распаковывать newtype слоев, которые для этого требуются.


В качестве дополнения это заявление:

Так где же прикладные трансформаторы? Ответ заключается в том, что нам не нужны специальные преобразователи для аппликативных функторов, поскольку их можно комбинировать обычным образом.

... немного подделка. Это правда, что композиция из двух Applicative тоже Applicative, но это не единственный способ совместить Applicative!

Рассмотрим StateT s m a, что эквивалентно s -> m (s, a), хотя он определяется немного иначе. Это также может быть записано как композиция из трех функторов: ((->) s), m и ((,) s), и результирующий экземпляр Functor будет правильным, но экземпляр Applicative будет полностью неверным. Если вместо этого вы начнете с State s a = s -> (a, s), невозможно будет определить StateT s m, составив State s и m.

Теперь обратите внимание, что некомпозиционная комбинация StateT s (Either e) по сути является упрощенной версией типичной монады комбинатора синтаксического анализатора, используемой в библиотеках, таких как Parsec, и такие синтаксические анализаторы являются одним из хорошо известных мест, где использование стиля Applicative является популярным. Таким образом, кажется более чем заблуждением предполагать, что комбинации в стиле преобразователя монад почему-то не нужны или излишни, когда речь идет о Applicative!

person C. A. McCann    schedule 25.09.2012
comment
Подпись типа неправильная? Разве это не должно быть liftA2 (<*>) = f (g (a -> b)) -> f (g a) -> f (g b)? - person Kamel; 02.05.2015
comment
Было бы интересно увидеть пример использования Compose, чтобы увидеть, сколько синтаксических издержек он добавляет к коду. - person imz -- Ivan Zakharyaschev; 29.06.2015

Сказать, что они могут быть объединены общим способом, не означает, что они могут быть объединены неявно или невидимо или что-то в этом роде. знак равно

Вам по-прежнему нужно написать немного кода, используя разные мангеры, отличные от <*> и pure, или добавив некоторый шум newtype. Например, используя пакет TypeCompose, вы можете написать

test2 = ex (O (Just "abc")) [O (Just "pqr"), O (Just "xyz")]
person Daniel Wagner    schedule 25.09.2012
comment
Data.Functor.Compose из пакета transformers, вероятно, является наиболее часто используемым типом данных для этого. В нем также есть Data.Functor.Product, который представляет собой другой способ комбинировать аппликативные функторы. - person Sjoerd Visscher; 25.09.2012
comment
Дополнительное описание TypeCompose можно найти на странице wiki.haskell.org/TypeCompose - person imz -- Ivan Zakharyaschev; 29.06.2015

Как обычно, здесь полезно сосредоточиться на типах, чтобы выяснить, что должна означать композиция аппликативных функторов.

Если мы напишем a для типа конкретного чистого значения x, которое, следовательно, не имеет побочных эффектов, тогда мы можем поднять это чистое значение до вычисления аппликативного функтора f с помощью комбинатора pure. Но точно так же мы можем использовать функцию pure из экземпляра g Applicative, чтобы поднять pure x в функтор g.

pure (pure x) :: g (f a)

Теперь g (f a) - это тип вычислений, в которых сочетаются эффекты g и эффекты f. Глядя на ваши тесты, мы замечаем, что

test1 :: [String]

Вы использовали только один эффект в test1, а именно недетерминированность, которую дает вам экземпляр списка Applicative. Действительно, разбив его:

"abc" :: String
((:) <$>) :: [Char] -> [String -> String]
((:) <$> "abc") :: [String -> String]
((:) <$> "abc" <*> ["pqr", "xyz"]) :: [String]

Теперь, если мы хотим скомбинировать эффект отказа и эффект недетерминизма, мы ожидаем построить вычисление типа Maybe [a] или, возможно, [Maybe a]. Оказывается, они эквивалентны, потому что аппликативные функторы всегда коммутируют.

Вот вычисление типа [Maybe Char]. Он недетерминированно вернет Char, но если это произойдет, он может потерпеть неудачу:

x1 = [Just 'a', Just 'b', Just 'c']

Аналогично, вот вычисление типа [Maybe String]:

x2 = [Just "pqr", Just "xyz"]

Теперь мы хотим поднять (:) до этого комбинированного аппликативного функтора. Для этого его нужно поднять дважды:

pure (pure (:)) :: [Maybe (Char -> String -> String)]

Точно так же, чтобы применить это вычисление, нам нужно пропустить это вычисление через оба функтора. Поэтому мы можем представить новый комбинатор (<<*>>), который делает это:

(<<*>>) :: (Applicative f, Applicative f1) =>
           f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)
(<<*>>) = liftA2 (<*>)

Что теперь позволяет нам писать:

pure (pure (:)) <<*>> x1 <<*>> x2

который вы можете проверить, имеет ожидаемый тип.

Но поскольку аппликативные функторы закрыты при композиции, [Maybe a] сам по себе является аппликативным функтором, поэтому вы можете захотеть повторно использовать pure и (<*>). Модуль Data.Functor.Compose трансформаторов < Пакет / a> покажет вам, как это сделать.

person macron    schedule 25.09.2012
comment
Было бы интересно увидеть пример использования Compose, чтобы увидеть, сколько синтаксических издержек он добавляет к коду. - person imz -- Ivan Zakharyaschev; 29.06.2015