Каковы практические применения аппликативного стиля?

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

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

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

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

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

Ответы

Ответ 1

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

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

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

Ответ 2

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

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

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

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

но

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

который именно вы хотите.

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

Ответ 3

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

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

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

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

data Foo = Foo Int Double

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

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

randomFoo = Foo <$> randomIO <*> randomIO

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

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

Почему я хочу использовать приложение, которое не является монадой?

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

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

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

Ответ 4

Я думаю о Functor, Applicative и Monad как шаблоны дизайна.

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

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

trait Future[T] {
  def get: T
}

Где "получить" блоки до тех пор, пока значение не будет доступно.

Вы могли бы это осознать и переписать для выполнения обратного вызова:

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

Но что происходит, если в будущем есть два использования? Это означает, что вам нужно сохранить список обратных вызовов. Кроме того, что произойдет, если метод получит "Будущее" [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

Ответ 5

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

https://web.archive.org/web/20100818221025/http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/chunk-html/index.html

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

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

Ответ 6

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

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

по сравнению с этим с использованием аппликативного:

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

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

Ответ 7

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

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

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

Ответ 8

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

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

instance FromJSON Coord where
   parseJSON (Object v) = 
      Coord <$>
        v .: "x" <*>
        v .: "y"

Ответ 9

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

Ответ 10

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

Ответ 11

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

Обратите внимание, что примеры кода - это псевдокод для моего гипотетического языка, который скрывал бы классы типов в концептуальной форме подтипирования, поэтому, если вы видите, что вызов метода для apply просто переводится в вашу модель класса типа, например. <*> в Scalaсе или Хаскелле.

Если мы отмечаем элементы массива или hashmap с null или none, чтобы указывают, что их индекс или ключ действительны, но не имеют значения, Applicativeпозволяет без какого-либо шаблона пропускать бесполезные элементы, пока применяя операции к элементам, которые имеют значение. И более что он может автоматически обрабатывать любую семантику Wrapped, которая неизвестны априори, т.е. операции на T по Hashmap[Wrapped[T]] (любой над любым уровнем композиции, например Hashmap[Wrapped[Wrapped2[T]]], потому что аппликативная композиция, но монада нет).

Я уже могу представить, как это облегчит мой код Понимаю. Я могу сосредоточиться на семантике, а не на всех чтобы получить меня там, и моя семантика будет открыта при расширении Обернуто, тогда как весь ваш пример кода isnt.

Примечательно, что я забыл указать, что ваши предыдущие примеры не эмулируйте возвращаемое значение Applicative, которое будет List, а не Nullable, Option или Maybe. Поэтому даже мои попытки ремонт ваших примеров не был имитирован Applicative.apply.

Помните, что functionToApply - это вход для Applicative.apply, поэтому контейнер поддерживает управление.

list1.apply( list2.apply( ... listN.apply( List.lift(functionToApply) ) ... ) )

Эквивалентное.

list1.apply( list2.apply( ... listN.map(functionToApply) ... ) )

И мой предложенный синтаксический сахар, который компилятор переведет к вышеуказанному.

funcToApply(list1, list2, ... list N)

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

для большинства программистов, вероятно, не требуется слияние потока управления вне закона с присваиванием

Applicative.apply - это обобщение частичного применения функций к параметризованным типам (a.k.a. generics) на любом уровне вложения (состава) параметра типа. Все дело в том, чтобы сделать более обобщенный состав возможным. Общность не может быть достигнута путем вытягивания ее за пределы завершенной оценки (то есть возвращаемого значения) функции, аналогичной луковице не может быть очищена от наизнанку.

Таким образом, это не слияние, это новая степень свободы, которая в настоящее время недоступна для вас. По нашему обсуждению поток, поэтому вы должны бросать исключения или хранить их в глобальной переменной, потому что ваш язык не имеет такой степени свободы. И это не единственное применение этих функторов теории категорий (изложено в моем комментарии в очереди модератора).

Я предоставил ссылку на пример абстрагирования валидации в Scala, F # и С#, который в настоящее время застревает в очереди модератора. Сравните нескромную версию кода С#. И причина в том, что С# не обобщен. Я интуитивно ожидаю, что шаблон С# case-specific будет взрываться геометрически по мере роста программы.

Ответ 12

Я думаю, что то, что я представил здесь, служит хорошим примером примера использования Applicative, замеченного в дикой природе.