Почему for из Data.Traversable принимает монадические действия?

Я работал над следующим небольшим фрагментом кода:

import           Control.Monad
import           Data.Aeson
import qualified Data.HashMap.Strict as HashMap
import           Data.Map (Map)
import qualified Data.Map as Map
import           GHC.Generics

-- definitions of Whitelisted, WhitelistComment and their FromJSON instances
-- omitted for brevity

data Whitelist = Whitelist
  { whitelist :: Map Whitelisted WhitelistComment
  } deriving (Eq, Ord, Show)

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . forM (HashMap.toList v) $ \(a, b) -> do
      a' <- parseJSON (String a)
      b' <- parseJSON b
      return (a', b')
  parseJSON _ = mzero

когда я понял, что могу переписать блок do в аппликативном стиле:

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . forM (HashMap.toList v) $ \(a, b) ->
      (,) <$> parseJSON (String a) <*> parseJSON b
  parseJSON _ = mzero

и с этим я мог бы также заменить forM на for. Прежде чем внести изменения выше, я сначала переключился на for:

instance FromJSON Whitelist where
  parseJSON (Object v) =
    fmap (Whitelist . Map.fromList) . for (HashMap.toList v) $ \(a, b) -> do
      a' <- parseJSON (String a)
      b' <- parseJSON b
      return (a', b')
  parseJSON _ = mzero

и, к моему удивлению, это все еще скомпилировано. Учитывая определение for:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

Я думал, что ограничение Applicative не позволит мне использовать нотацию do/return в действии, переданном for.

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


person ppb    schedule 14.12.2016    source источник
comment
Вам не хватает того, что Applicative является суперклассом Monad? forM, liftM и т. д. — напоминания о том, что Applicative не был (в Prelude) надклассом Monad.   -  person Alec    schedule 14.12.2016
comment
@ Алек, разве не наоборот? В том смысле, что я мог передать код в аппликативном стиле в forM без каких-либо изменений, но не в обратном направлении? Я не совсем уверен, как можно было бы использовать return при наличии ограничения Applicative.   -  person ppb    schedule 14.12.2016
comment
parseJSON работает в монаде Parser, которая также является аппликативной (разумеется). return (и запись do) проверяет, что Parser является монадой -- так оно и есть. for проверяет, что это также аппликатив - так оно и есть. Все работает просто отлично.   -  person chi    schedule 14.12.2016


Ответы (2)


Первый короткий ответ: Parser имеет экземпляр Applicative. Фрагмент

do
  a' <- parseJSON a
  b' <- parseJSON b
  return (a', b')

имеет тип Parser (Whitelisted, WhitelistComment), который унифицируется с f b в сигнатуре типа

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

Поскольку есть экземпляр Applicative Parser, он также удовлетворяет этому ограничению. (Кажется, я правильно понял типы для a' и b')


Второй короткий ответ заключается в том, что Monad строго мощнее, чем Applicative, везде, где вам нужен Applicative, вместо него можно использовать Monad. С момента реализации Monad-Applicative предложения все Monad тоже Applicative. Класс Monad теперь выглядит как

class Applicative m => Monad m where 
    ...

Monad строго мощнее, чем Applicative, везде, где вам нужен Applicative, вместо него можно использовать Monad со следующими заменами:

  • ap вместо <*>
  • return вместо pure
  • liftM вместо fmap

Если вы пишете какой-то новый тип SomeMonad и предоставили экземпляр для класса Monad, вы также можете использовать его для предоставления экземпляров для Applicative и Functor.

import Control.Monad

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

instance Functor SomeMonad where
    fmap = liftM
person Cirdec    schedule 14.12.2016
comment
Спасибо. Я вижу, насколько Monad мощнее, и что с AMP каждый Monad также является Applicative. Но когда в подписи говорится, что ей конкретно нужен Applicative, не означает ли это, что она хочет ограничить эти полномочия и работать только с Applicative интерфейсом? - person ppb; 14.12.2016
comment
Да, for разрешено использовать только полномочия Applicative объекта, который вы передаете. Но вы можете использовать полномочия монад для создания значений этого типа - то, как вы делаете вещи внутри своей лямбды, является черным ящиком для for. - person amalloy; 14.12.2016
comment
@amalloy Думаю, теперь я понял - for не важно, как было создано значение, важно только, чтобы оно удовлетворяло ограничению Applicative. Я думал, что ограничение Applicative будет наложено на то, что может делать сама лямбда, а не только на то, что она возвращает. - person ppb; 14.12.2016
comment
И кажется, самое главное, что я упустил, это создание Applicative по сравнению с работой с ним :-( - person ppb; 14.12.2016
comment
@ppb Речь идет не о производстве и работе с типом. Речь идет о том, что вы (и система типов) знаете о типе. Когда вы вызываете for :: forall f. ..., вы передаете for как его аргументы, так и тип f. Вы знаете о f больше, чем о Applicative f или Monad f; ты знаешь, что такое f! Это Parser. Вы можете использовать все, что знаете об этом типе, включая сумасшедшие вещи, которые даже не являются частью ни Applicative, ни Monad, например parseJSON (которое вы уже используете). Если вы знаете, что f — это Parser для использования parseJSON, вы также знаете, что это Monad для использования >>=. - person Cirdec; 15.12.2016

Это просто обычная двойственность вызывающей стороны и исполнителя, когда одна сторона получает гибкость, а другая — ограничения.

for предоставляет вам этот интерфейс:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

Вы, как вызывающая сторона, можете выбрать любой тип f для его создания, поэтому вы можете использовать его так, как если бы он был:

for :: Traversable t => t a -> (a -> Parser b) -> Parser (t b)

Очевидно, что после того, как вы это сделаете, не будет никаких причин, по которым вы не сможете использовать какую-либо специфичную для Parser функциональность в функции, которую вы передаете for, в том числе Monad.

С другой стороны, разработчик for ограничен полиморфизмом интерфейса for. Им приходится работать с любым выбором f, поэтому они могут только использовать интерфейс Applicative в коде, который они пишут для реализации for. Но это ограничивает только код самого for, а не передаваемую в него функцию.

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

for :: forall t f. (Traversable t, Applicative f) => t a -> (forall g. Applicative g => a -> g b) -> f (t b)

Теперь сама предоставленная лямбда должна быть полиморфной в g (с учетом ограничения Applicative). Вызывающий for по-прежнему может выбирать f, а разработчик ограничен в использовании только Applicative функций. Но вызывающая сторона for является реализатором аргумента функции, так что теперь, когда эта функция сама по себе является полиморфной, вызывающая сторона for ограничена использованием только функций Applicative, а реализатор for получает свободу использования с любым типом, который им нравится (включая, возможно, использование функций монады для объединения его с другими внутренними значениями). С этой конкретной сигнатурой типа разработчик for должен будет создать экземпляр g с тем же типом, который вызывающий for выбрал для f, чтобы получить окончательное возвращаемое значение f (t b). Но вызывающая сторона for по-прежнему будет ограничена системой типов предоставлением функции, которая работает для любого Applicative g.

Дело в том, что если вы выбираете, с каким типом создавать экземпляр полиморфной подписи, то вы не ограничены этим интерфейсом. Вы можете выбрать тип, а затем использовать любые другие функции этого типа, которые вам нравятся, при условии, что вы по-прежнему предоставляете биты информации, которые требует от вас интерфейс. т. е. вы можете использовать не-33_ функциональность для создания t a и не-35_ функциональность для создания a -> f b, все, что требуется, это предоставить эти входные данные. И действительно, вам почти нужно использовать функции, характерные для a и b. Реализатор полиморфной подписи не получает этой свободы, полиморфизм ограничивает его выполнением только тех действий, которые будут работать при любом возможном выборе.

Кроме того, аналогично тому, как типы ранга 2 добавляют «еще один уровень» этой двойственности с обратными ролями (а типы ранга N допускают произвольное количество уровней), аналогичная двойственность также наблюдается (снова перевернутая) в самих ограничениях. Рассмотрим еще раз подпись:

for :: (Traversable t, Applicative f) => t a -> (a -> f b) -> f (t b)

вызывающий for ограничивается ограничениями Traversable и Applicative при выборе типов t и f. Разработчик получает свободу использовать любые функции, подразумеваемые этими ограничениями, не беспокоясь о том, как доказать, что ограничения соблюдены.

person Ben    schedule 14.12.2016