Каковы практические применения аппликативного стиля? - PullRequest
63 голосов
/ 18 августа 2011

Я программист Scala, сейчас изучаю Haskell.Легко найти примеры практического использования и примеры из реальной жизни для концепций ОО, таких как декораторы, шаблоны стратегий и т. Д. Эти книги заполнены.

Я пришел к выводу, что это как-то не так для функциональных понятий.Показательный пример: аппликативные .

Я изо всех сил пытаюсь найти практические случаи использования для аппликативов.Почти все учебники и книги, с которыми я сталкивался до сих пор, содержат примеры [] и Maybe.Я ожидал, что аппликативы будут более применимыми, учитывая все внимание, которое они получают в сообществе FP.

Мне кажется, я понимаю концептуальную основу аппликативных (возможно, я ошибаюсь), и я долго ждал своего момента просветления.Но, похоже, этого не происходит.Никогда во время программирования у меня не было момента, когда я с радостью кричал: «Эврика! Я могу использовать здесь аппликатив!»(за исключением еще раз, для [] и Maybe).

Может, кто-нибудь подскажет мне, как можно использовать аппликативы в повседневном программировании?Как мне начать определять шаблон?Спасибо!

Ответы [ 11 ]

65 голосов
/ 18 августа 2011

Аппликативы хороши, когда у вас есть простая старая функция с несколькими переменными, и у вас есть аргументы, но они заключены в какой-то контекст. Например, у вас есть простая старая функция сцепления (++), но вы хотите применить ее к 2 строкам, которые были получены с помощью ввода-вывода. Тогда на помощь приходит тот факт, что IO является аппликативным функтором:

Prelude Control.Applicative> (++) <$> getLine <*> getLine
hi
there
"hithere"

Даже если вы явно просили не Maybe примеры, мне кажется, что это отличный вариант использования, поэтому я приведу пример. У вас есть обычная функция от нескольких переменных, но вы не знаете, есть ли у вас все необходимые значения (некоторые из них, возможно, не смогли вычислить, что привело к Nothing). По сути, поскольку у вас есть «частичные значения», вы хотите превратить свою функцию в частичную функцию, которая не определена, если какой-либо из ее входов не определен. Тогда

Prelude Control.Applicative> (+) <$> Just 3 <*> Just 5
Just 8

но

Prelude Control.Applicative> (+) <$> Just 3 <*> Nothing
Nothing

это именно то, что вы хотите.

Основная идея заключается в том, что вы «поднимаете» обычную функцию в контекст, где ее можно применить к любому количеству аргументов, как вам нравится. Дополнительная сила Applicative по сравнению с базовым Functor заключается в том, что он может поднимать функции произвольной арности, тогда как fmap может поднимать только унарную функцию.

47 голосов
/ 18 августа 2011

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

Зачем мне использовать аппликативный интерфейс вместо монадического, когда оба доступны?

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

В этом примере у нас есть тип Foo, и мы хотим построить случайные значения этого типа,Используя экземпляр монады для IO, мы могли бы написать

data Foo = Foo Int Double

randomFoo = do
    x <- randomIO
    y <- randomIO
    return $ Foo x y

Аппликативный вариант немного короче.

randomFoo = Foo <$> randomIO <*> randomIO

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

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

Зачем мне использовать аппликатив, не являющийся монадой?

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

Примером этого являются аппликативные парсеры.В то время как монадические парсеры поддерживают последовательную композицию с использованием (>>=) :: Monad m => m a -> (a -> m b) -> m b, аппликативные парсеры используют только (<*>) :: Applicative f => f (a -> b) -> f a -> f b.Типы делают разницу очевидной: в монадических парсерах грамматика может изменяться в зависимости от ввода, тогда как в аппликативном парсере грамматика является фиксированной.

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

16 голосов
/ 21 августа 2011

Я думаю о Functor, Applicative и Monad как о шаблонах проектирования.

Представьте, что вы хотите написать класс Future [T]. То есть класс, который содержит значения, которые должны быть рассчитаны.

В мышлении Java вы можете создать его как

trait Future[T] {
  def get: T
}

Где блоки 'get' пока значение не доступно.

Вы можете понять это и переписать это, чтобы принять обратный вызов:

trait Future[T] {
  def foreach(f: T => Unit): Unit
}

Но что произойдет, если в будущем будет два варианта использования? Это означает, что вам нужно вести список обратных вызовов. Кроме того, что произойдет, если метод получит Future [Int] и ему нужно будет вернуть расчет, основанный на Int внутри? Или что вы делаете, если у вас есть два фьючерса, и вам нужно рассчитать что-то на основе значений, которые они предоставят?

Но если вам известны концепции FP, вы знаете, что вместо работы непосредственно с T вы можете манипулировать экземпляром Future.

trait Future[T] {
  def map[U](f: T => U): Future[U]
}

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

Как только вы начинаете на этом пути, вы не можете остановиться там. Вы понимаете, что для того, чтобы манипулировать двумя фьючерсами, вам просто нужно моделировать как аппликатив, чтобы создавать фьючерсы, вам нужно определение монады для будущего и т. Д.

ОБНОВЛЕНИЕ: Как предложено @Eric, я написал сообщение в блоге: http://www.tikalk.com/incubator/blog/functional-programming-scala-rest-us

13 голосов
/ 18 августа 2011

Я наконец понял, как аппликативы могут помочь в повседневном программировании с этой презентацией:

http://applicative -errors-scala.googlecode.com / svn / artifacts / 0.6 / chunk-html / index.html

Автор показывает, как аппликативы могут помочь в комбинировании проверок и обработке сбоев.

Презентация в Scala, но автор также предоставляет полный пример кода для Haskell, Java и C #.

9 голосов
/ 18 августа 2011

Я думаю, что Applicatives облегчает общее использование монадического кода.Сколько раз вы сталкивались с ситуацией, когда вы хотели применить функцию, но эта функция не была монадической, и значение, к которому вы хотите применить ее, является монадическим?Для меня: довольно много раз!
Вот пример, который я только что написал вчера:

ghci> import Data.Time.Clock
ghci> import Data.Time.Calendar
ghci> getCurrentTime >>= return . toGregorian . utctDay

по сравнению с использованием Applicative:

ghci> import Control.Applicative
ghci> toGregorian . utctDay <$> getCurrentTime

Эта форма выглядит"более естественный" (по крайней мере, на мой взгляд)

8 голосов
/ 19 августа 2011

Предупреждение: мой ответ довольно проповеднический / извиняющийся. Так что подайте в суд на меня.

Ну, как часто в повседневном программировании на Haskell вы создаете новые типы данных? Похоже, вы хотите знать, когда создавать свой собственный экземпляр Applicative, и, если честно, если вы не используете собственный синтаксический анализатор, вам, вероятно, не понадобится делать это очень много. Используя аппликативные экземпляры, вы должны научиться делать это часто.

Applicative не является «шаблоном дизайна», как декораторы или стратегии. Это абстракция, которая делает его гораздо более распространенным и в целом полезным, но гораздо менее ощутимым. Причина, по которой вам трудно найти «практическое применение», заключается в том, что пример, используемый для этого, почти слишком прост. Вы используете декораторы для размещения полос прокрутки на окнах. Вы используете стратегии, чтобы унифицировать интерфейс для агрессивных и защитных ходов для вашего шахматного бота. Но для чего нужны аппликативы? Ну, они намного более обобщены, поэтому трудно сказать, для чего они, и это нормально. Аппликаторы удобны в качестве парсинга комбинаторов; веб-платформа Yesod использует Applicative для настройки и извлечения информации из форм. Если вы посмотрите, вы найдете миллион и один использует для Applicative; это повсюду. Но так как это настолько абстрактно, вам просто нужно почувствовать это, чтобы распознать множество мест, где это может помочь облегчить вашу жизнь.

5 голосов
/ 18 августа 2011

Исходя из Applicative из "Functor", он обобщает "fmap", чтобы легко выразить действия по нескольким аргументам (liftA2) или последовательности аргументов (используя <*>).

Выход из Applicative из "Monad" не позволяет вычислению зависеть от вычисляемого значения. В частности, вы не можете сопоставить шаблон и выполнить ветвление возвращаемого значения, обычно все, что вы можете сделать, это передать его другому конструктору или функции.

Таким образом, я вижу Applicative как зажатый между Functor и Monad. Распознавание, когда вы не переходите на значения из монадических вычислений, - это один из способов узнать, когда переключаться на Applicative.

4 голосов
/ 10 февраля 2012

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

4 голосов
/ 18 августа 2011

Вот пример, взятый из пакета aeson:

data Coord = Coord { x :: Double, y :: Double }

instance FromJSON Coord where
   parseJSON (Object v) = 
      Coord <$>
        v .: "x" <*>
        v .: "y"
2 голосов
/ 19 августа 2011

Я думаю, что было бы целесообразно просмотреть источники пакетов в Hackage и воочию увидеть, как аппликативные функторы и тому подобное используются в существующем коде Haskell.

...