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

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

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

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

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

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


person missingfaktor    schedule 18.08.2011    source источник
comment
Впервые меня вдохновили эти две статьи: debasishg.blogspot. com / 2010/11 / explore-scalaz.html debasishg.blogspot.com/2011/02/   -  person CheatEx    schedule 18.08.2011
comment
тесно связаны: stackoverflow.com/ questions / 2120509 /   -  person Mauricio Scheffer    schedule 18.08.2011
comment
groups.google.com/forum/#!msg/scala- user / uh5w6N2eAHY /   -  person Mauricio Scheffer    schedule 18.08.2011
comment
В документе Суть шаблона итератора все о том, как Applicative суть шаблона итератора.   -  person Russell O'Connor    schedule 25.08.2011


Ответы (11)


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

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

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

person Dan Burton    schedule 19.08.2011
comment
Я удивлен, что этот ответ был отмечен галочкой, в то время как несколько других ответов, таких как ответы Хаммара и Оливера, находятся далеко внизу страницы. Я считаю, что они лучше, потому что они предоставляют отличные примеры приложений за пределами Maybe и []. Просить спрашивающего подумать немного глубже просто бесполезно. - person darrint; 04.01.2012
comment
@darrint - видимо, спрашивающий нашел это полезным, поскольку именно он пометил его как принятый. Я придерживаюсь того, что я сказал: если кто-то потратит время на эксперименты, даже с экземплярами [] и Maybe, он почувствует, какая форма Applicative и как она используется. Это то, что делает любой класс типов полезным: не обязательно точно знать, что делает каждый экземпляр, а, скорее, иметь общее представление о том, что делают комбинаторы Applicative в целом, поэтому, когда вы сталкиваетесь с новым типом данных и узнаете, что у него есть экземпляр Applicative , вы можете сразу приступить к его использованию. - person Dan Burton; 04.01.2012

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

person Tom Crockett    schedule 18.08.2011
comment
Я не уверен, что аппликативный пример ввода-вывода хорош, поскольку аппликативный не так сильно озабочен порядком, как imho, но в (| (++) getLine getLine |) упорядочение двух getLine действий становится значимым для результата ... - person hvr; 18.08.2011
comment
@hvr: Порядок (<*>) последовательностей элементов произвольный, но обычно по соглашению слева направо, так что f <$> x <*> y == do { x' <- x; y' <- y; return (f x y) } - person C. A. McCann; 18.08.2011
comment
@CAMcCann, ... так что со стилистической точки зрения это считается хорошей практикой кодирования, чтобы выразить что-то в аппликативном стиле, который на самом деле зависит от правильной последовательности эффектов (что, по-моему, лучше было бы решить с помощью монадического стиля) ? - person hvr; 18.08.2011
comment
@hvr: Имейте в виду, что в самом выражении не может зависеть от последовательности, потому что поднятая функция не может заметить разницу, и оба эффекта будут иметь место несмотря ни на что. Какой порядок выбран, определяется только экземпляром, который должен знать, какой из них правильный. Также обратите внимание, что в документации указано, что для экземпляров Monad (<*>) = ap, что исправляет порядок в соответствии с моим примером выше. - person C. A. McCann; 18.08.2011
comment
Операторы стиля ‹$› и ‹*› объявлены infixl 4, поэтому нет двусмысленного соглашения, он указывается с объявлением, что он будет группировать / связывать слева направо. Порядок эффектов r2l или l2r по-прежнему контролируется фактическим экземпляром, который для монад использует тот же порядок, что и Control.Monad.ap, который имеет идентификатор liftM2, а liftM2 задокументировано для запуска слева направо. - person Chris Kuklewicz; 18.08.2011
comment
@Chris, группировка слева направо не имеет ничего общего с выполнением слева направо. - person Rotsor; 19.08.2011
comment
Последний пункт в ответе сделал мой день. Линии мудрости. Большое Вам спасибо. - person Piyush Katariya; 08.07.2018

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

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

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

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

person hammar    schedule 18.08.2011
comment
iinm, недавно повторно добавленные монадные понимания в ghc дают почти такой же уровень компактности, как и аппликативные комбинаторы: [Foo x y | x <- randomIO, y <- randomIO] - person Dan Burton; 19.08.2011
comment
@Dan: это, конечно, короче, чем пример с 'do', но все же не без точек, что кажется желательным в мире Haskell - person Jared Updike; 23.08.2011

Я думаю о 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]
}

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

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

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

person IttayD    schedule 21.08.2011
comment
Это интересный способ представить функторы, аппликативы и монады, достойный полного поста в блоге, показывающего детали, лежащие в основе «и т. Д.». - person Eric; 25.08.2011
comment
На сегодняшний день ссылка кажется неработающей. Ссылка на машину обратного пути: https://web.archive.org/web/20140604075710/http://www.tikalk.com/incubator/functional-programming-scala-rest-us - person superjos; 16.10.2018

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

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

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

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

person paradigmatic    schedule 18.08.2011
comment
К сожалению, ссылка не работает. - person thSoft; 09.12.2016

Я думаю, что 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

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

person oliver    schedule 18.08.2011
comment
На самом деле ‹$› - это просто fmap, он реэкспортирован из Data.Functor. - person Sjoerd Visscher; 18.08.2011
comment
@Sjoerd Visscher: правильно ... Использование <$> еще более привлекательно, поскольку fmap по умолчанию не является инфиксным оператором. Значит, это должно быть примерно так: fmap (toGregorian . utctDay) getCurrentTime - person oliver; 18.08.2011
comment
Проблема с fmap в том, что он не работает, когда вы хотите применить простую функцию с несколькими аргументами к нескольким монадическим значениям; решение этой проблемы - вот где приходит на помощь Applicative. - person C. A. McCann; 18.08.2011
comment
@oliver Я думаю, что Шорд имел в виду, что то, что вы показываете, на самом деле не является примером того, где могут быть полезны аппликативы, поскольку на самом деле вы имеете дело только с функтором. Тем не менее, он демонстрирует, насколько полезен аппликативный стиль. - person kqr; 28.10.2013

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

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

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

person Chris Kuklewicz    schedule 18.08.2011

Вот пример из пакета aeson:

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

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

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

person Sukant Hajra    schedule 09.02.2012

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

person Artyom Shalkhakov    schedule 19.08.2011
comment
Здесь стоит добавить либо конкретную ссылку, либо более подробную информацию. - person Vlad Patryshev; 04.03.2012

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

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

Если мы помечаем элементы массива или хэш-карты с помощью 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 #, зависящий от конкретного случая, будет геометрически взрываться по мере роста программы.

person Shelby Moore III    schedule 14.03.2013