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

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

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

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

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

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

11 ответов

Решение

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

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

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

Аппликативы хороши, когда у вас есть простая старая функция с несколькими переменными, и у вас есть аргументы, но они заключены в какой-то контекст. Например, у вас есть простая старая функция сцепления (++) но вы хотите применить его к 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 можно только поднять унарную функцию.

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

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

Это в основном вопрос стиля. Хотя монады имеют синтаксический сахар 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, Типы делают разницу очевидной: в монадических парсерах грамматика может меняться в зависимости от ввода, тогда как в аппликативном парсере грамматика является фиксированной.

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

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

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

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

trait Future[T] {
  def get: T
}

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

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

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

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

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

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

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

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

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

по сравнению с этим, используя Applicative:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Важно отметить, что я забыл указать, что ваши предыдущие примеры не эмулируют возвращаемое значение 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 предназначен для обобщения частичного применения функций к параметризованным типам (или универсальным) на любом уровне вложенности (композиции) параметра типа. Это все о создании более обобщенной композиции. Общность не может быть достигнута путем вытягивания ее за пределы завершенной оценки (т. Е. Возвращаемого значения) функции, аналогично тому, что лук не может быть очищен изнутри.

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

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

Другие вопросы по тегам