-
ApplicativeDo -
- Since:
-
8.0.1
Разрешает использование
Applicativedoнотации.
Опция языка 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 Ereturn $ Epure Epure $ 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