Spec-Zone.ru › Haskell 9

6.2.4. Применение do-нотации

ApplicativeDo
Since:

8.0.1

Разрешает использование Applicative do нотации.

Опция языка ApplicativeDo включает альтернативный перевод для do-нотации, который использует операторы <$>, <*>, а также join по возможности. Есть две основные причины для этого:

  • Мы можем использовать do-нотацию с типами, которые являются экземплярами Applicative и Functor, но не Monad
  • В некоторых монадах использование аппликативных операторов более эффективно, чем монадическое связывание. Например, это может позволить большее параллелизма.

Преобразование аппликативной do-нотации сохраняет исходную семантику, при условии, что экземпляр Applicative удовлетворяет условиям <*> = ap и pure = return (что верно для всех распространённых монадических типов). Таким образом, вы обычно можете включить ApplicativeDo без опасений за нарушение работы вашей программы. Есть одна потенциальная проблема, о которой нужно помнить; см. Важные моменты.

С ApplicativeDo нет синтаксических изменений. Единственный способ, которым это проявляется на уровне исходного кода, состоит в том, что у вас может быть выражение do , которое не требует ограничения Monad. Например, в GHCi:

Prelude> :set -XApplicativeDo
Prelude> :t \m -> do { x <- m; return (not x) }
\m -> do { x <- m; return (not x) }
  :: Functor f => f Bool -> f Bool

Этот пример требует только Functor, потому что он переводится в (\x -> not x) <$> m. Более сложный пример требует Applicative,

Prelude> :t \m -> do { x <- m 'a'; y <- m 'b'; return (x || y) }
\m -> do { x <- m 'a'; y <- m 'b'; return (x || y) }
  :: Applicative f => (Char -> f Bool) -> f Bool

Здесь GHC перевёл выражение в

(\x y -> x || y) <$> m 'a' <*> m 'b'

Можно увидеть фактический перевод, используя -ddump-ds, но будьте осторожны, вывод довольно объёмный.

Обратите внимание, что если выражение не может быть переведено с использованием только <$>, <*>, то оно понесёт ограничение Monad как обычно. Это происходит, когда есть зависимость от значения, произведённого предыдущим оператором в блоке do:

Prelude> :t \m -> do { x <- m True; y <- m x; return (x || y) }
\m -> do { x <- m True; y <- m x; return (x || y) }
  :: Monad m => (Bool -> m Bool) -> m Bool

Здесь m x зависит от значения x, произведённого первым оператором, поэтому выражение не может быть переведено с использованием <*>.

В общем случае правило, когда оператор do влечёт за собой ограничение Monad следующее. Если выражение do имеет следующий вид:

do p1 <- E1; ...; pn <- En; return E

где ни одна из переменных, определённых p1...pn, не упоминается в E1...En, и p1...pn — это все переменные или ленивые шаблоны, то выражение будет требовать только Applicative. Выражение do также может содержать операторы let в любом месте, при условии, что правые части let привязок не упоминают ни одну из p1...pn. В противном случае выражение будет требовать Monad. Блок может возвращать чистое выражение E в зависимости от результатов p1...pn и let привязок, с использованием return или pure.

Примечание: последний оператор должен точно соответствовать одному из этих шаблонов:

  • return E
  • return $ E
  • pure E
  • pure $ E

иначе GHC не может распознать его как оператор return, и преобразование для использования <$>, которое мы видели выше, не применяется. В частности, незначительные вариации, такие как return . Just $ x или let x = e in return x, не будут распознаны.

Если последний оператор не соответствует ни одному из этих форм, GHC возвращается к стандартному преобразованию do и выражение потребует ограничения Monad.

Когда операторы в выражении do имеют зависимости друг от друга, и ApplicativeDo не может вывести тип Applicative, он использует эвристический алгоритм, чтобы попытаться использовать <*> по возможности. Этот алгоритм обычно находит наилучшее решение, но в редких сложных случаях может пропустить возможность. Есть алгоритм, который находит оптимальное решение, предоставляемый в виде опции:

-foptimal-applicative-do
Since:

8.0.1

Включает альтернативный алгоритм для выбора того, где использовать <*> в сочетании с расширением языка ApplicativeDo. Этот алгоритм всегда находит оптимальное решение, но он затратный: O(n^3), поэтому эта опция может привести к длинным временам компиляции при наличии очень больших выражений do (более 100 операторов). По умолчанию алгоритм ApplicativeDo — это O(n^2).

6.2.4.1. Строгие шаблоны

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

Например, этот код потребует ограничения Monad:

> :t \m -> do { (x:xs) <- m; return x }
\m -> do { (x:xs) <- m; return x } :: Monad m => m [b] -> m b

но придание ленивости шаблону соответствует ограничению Functor:

> :t \m -> do { ~(x:xs) <- m; return x }
\m -> do { ~(x:xs) <- m; return x } :: Functor f => f [b] -> f b

«Строгое сопоставление с образцом» — это любое сопоставление с образцом, которое может потерпеть неудачу. Например, (), (x:xs), !z, и C x являются строгими шаблонами, но x и ~(1,2) не являются. Для целей ApplicativeDo, сопоставление с образцом конструктору newtype считается строгим.

При наличии строгого сопоставления с образцом в последовательности операторов ApplicativeDo помещает >>= между этим оператором и последующим. Последовательность может быть преобразована для использования <*> в других местах, но строгое сопоставление с образцом и последующий оператор всегда будут связаны с >>=, чтобы сохранить ту же семантику строгости, что и стандартная do-нотация. Если вы этого не хотите, просто добавьте ~ к сопоставлению с образцом, чтобы сделать его ленивым.

6.2.4.2. Важные моменты

Ваш код должен работать так же, как и раньше, при включенном ApplicativeDo, при использовании стандартных экземпляров Applicative. Однако, если вы определите экземпляр Functor или Applicative с помощью do-нотации, то, скорее всего, он будет преобразован GHC в бесконечный цикл. Например, если вы сделаете это:

instance Functor MyType where
    fmap f m = do x <- m; return (f x)

Тогда аппликативное преобразование превратит его в

instance Functor MyType where
    fmap f m = fmap (\x -> f x) m

И программа зациклится во время выполнения. Аналогично, экземпляр Applicative такого рода

instance Applicative MyType where
    pure = return
    x <*> y = do f <- x; a <- y; return (f a)

приведёт к бесконечному циклу, когда <*> вызывается.

Так же, как вы не определяли бы экземпляр Monad с использованием do-нотации, вы не должны определять экземпляр Functor или Applicative с использованием do-нотации (при использовании ApplicativeDo) тоже. Правильный способ определения этих экземпляров в терминах Monad — использовать операции Monad напрямую, например:

instance Functor MyType where
    fmap f m = m >>= return . f

instance Applicative MyType where
    pure = return
    (<*>) = ap

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.12.1/docs/users_guide/exts/applicative_do.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API