Spec-Zone.ru › Haskell 9

6.2.4. Аппликативная do-нотация

ApplicativeDo
С версии:

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
С версии:

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, не следует определять экземпляры 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.14.1/docs/users_guide/exts/applicative_do.html

Spec-Zone.ru

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