Что такое индексированная монада?

98

Что такое индексированная монада и мотивация этой монады?

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

Какой был бы пример того, как это может помочь отслеживать побочные эффекты (или любой другой допустимый пример)?

Сиби
источник

Ответы:

123

Как всегда, терминология, которую используют люди, не совсем последовательна. Есть множество идей, вдохновленных монадами, но, строго говоря, не совсем. Термин «индексированная монада» является одним из числа (включая «монаду» и «параметризованную монаду» (имя Атки для них)) терминов, используемых для характеристики одного такого понятия. (Еще одно такое понятие, если вам интересно, - это «монада параметрических эффектов» Кацуматы, индексируемая моноидом, где return индексируется нейтрально, а привязка накапливается в своем индексе.)

Прежде всего, проверим виды.

IxMonad (m :: state -> state -> * -> *)

То есть тип «вычисления» (или «действия», если хотите, но я буду придерживаться «вычисления»), выглядит так:

m before after value

где before, after :: stateи value :: *. Идея состоит в том, чтобы уловить средства для безопасного взаимодействия с внешней системой, которая имеет некоторое предсказуемое понятие состояния. Тип вычисления сообщает вам, в каком состоянии он должен быть beforeзапущен, в каком состоянии он будет выполняться afterи (как с обычными монадами *), какой тип valueвычислений производит.

Обычные кусочки и части по- *своему похожи на монаду, а state-мы - на игру в домино.

ireturn  ::  a -> m i i a    -- returning a pure value preserves state
ibind    ::  m i j a ->      -- we can go from i to j and get an a, thence
             (a -> m j k b)  -- we can go from j to k and get a b, therefore
             -> m i k b      -- we can indeed go from i to k and get a b

Таким образом, порожденное понятие "стрелы Клейсли" (функция, производящая вычисления) имеет вид

a -> m i j b   -- values a in, b out; state transition i to j

и мы получаем композицию

icomp :: IxMonad m => (b -> m j k c) -> (a -> m i j b) -> a -> m i k c
icomp f g = \ a -> ibind (g a) f

и, как всегда, законы точно это гарантируют ireturnи icompдают нам категорию

      ireturn `icomp` g = g
      f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)

или, в комедийной подделке C / Java / что угодно,

      g(); skip = g()
      skip; f() = f()
{g(); h()}; f() = h(); {g(); f()}

Зачем беспокоиться? Смоделировать «правила» взаимодействия. Например, вы не можете извлечь DVD, если его нет на диске, и вы не можете вставить DVD в дисковод, если он уже есть. Так

data DVDDrive :: Bool -> Bool -> * -> * where  -- Bool is "drive full?"
  DReturn :: a -> DVDDrive i i a
  DInsert :: DVD ->                   -- you have a DVD
             DVDDrive True k a ->     -- you know how to continue full
             DVDDrive False k a       -- so you can insert from empty
  DEject  :: (DVD ->                  -- once you receive a DVD
              DVDDrive False k a) ->  -- you know how to continue empty
             DVDDrive True k a        -- so you can eject when full

instance IxMonad DVDDrive where  -- put these methods where they need to go
  ireturn = DReturn              -- so this goes somewhere else
  ibind (DReturn a)     k  = k a
  ibind (DInsert dvd j) k  = DInsert dvd (ibind j k)
  ibind (DEject j)      k  = DEject j $ \ dvd -> ibind (j dvd) k

Имея это место, мы можем определить "примитивные" команды

dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()

dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd

из которых собираются другие с помощью ireturnи ibind. Теперь я могу написать (заимствование - doобозначение)

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'

но не физически невозможное

discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject      -- ouch!

В качестве альтернативы можно напрямую определить свои примитивные команды

data DVDCommand :: Bool -> Bool -> * -> * where
  InsertC  :: DVD -> DVDCommand False True ()
  EjectC   :: DVDCommand True False DVD

а затем создать экземпляр универсального шаблона

data CommandIxMonad :: (state -> state -> * -> *) ->
                        state -> state -> * -> * where
  CReturn  :: a -> CommandIxMonad c i i a
  (:?)     :: c i j a -> (a -> CommandIxMonad c j k b) ->
                CommandIxMonad c i k b

instance IxMonad (CommandIxMonad c) where
  ireturn = CReturn
  ibind (CReturn a) k  = k a
  ibind (c :? j)    k  = c :? \ a -> ibind (j a) k

Фактически, мы сказали, что такое примитивные стрелки Клейсли (что такое одно «домино»), а затем построили над ними подходящее понятие «вычислительной последовательности».

Обратите внимание, что для каждой индексированной монады m«диагональ без изменений» m i iявляется монадой, но в целом таковой m i jне является. Более того, значения не индексируются, а вычисления индексируются, поэтому индексированная монада - это не просто обычная идея монады, созданной для какой-то другой категории.

Теперь посмотрим еще раз на тип стрелки Клейсли.

a -> m i j b

Мы знаем, что должны быть в состоянии, iчтобы начать, и предсказываем, что любое продолжение начнется с состояния j. Мы много знаем об этой системе! Это не рискованная операция! Когда мы вставляем dvd в привод, он входит! Привод DVD не может сказать, в каком состоянии находится после каждой команды.

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

Какой инструмент лучше?

type f :-> g = forall state. f state -> g state

class MonadIx (m :: (state -> *) -> (state -> *)) where
  returnIx    :: x :-> m x
  flipBindIx  :: (a :-> m b) -> (m a :-> m b)  -- tidier than bindIx

Страшное печенье? Не совсем по двум причинам. Во- первых, это выглядит скорее как то , что монада, потому что это монада, но над (state -> *)чем *. Во-вторых, если вы посмотрите на тип стрелки Клейсли,

a :-> m b   =   forall state. a state -> m b state

вы получаете тип вычислений с предусловием a и постусловием b, как в старой доброй логике Хоара. Утверждениям в программной логике потребовалось менее полувека, чтобы пересечь соответствие Карри-Ховарда и стать типами Haskell. Тип returnIxговорит: «Вы можете достичь любого выполненного постусловия, просто ничего не делая», что является правилом логики Хоара для «пропуска». Соответствующая композиция - это правило логики Хоара для ";".

Давайте закончим, рассмотрев тип bindIx, добавив в него все квантификаторы.

bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i

Эти foralls имеют противоположную полярность. Мы выбираем начальное состояние iи вычисление, с которого можно начинать i, с постусловием a. Мир выбирает любое промежуточное состояние, которое jему нравится, но он должен предоставить нам свидетельство того, что постусловие bвыполняется, и из любого такого состояния мы можем продолжить, чтобы bудержаться. Итак, последовательно мы можем достичь состояния bиз состояния i. Освободив контроль над состояниями «после», мы можем моделировать непредсказуемые вычисления.

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

свинарник
источник
1
Как вы можете передать True/ Falsevalues ​​в качестве аргументов типа DVDDrive? Это какое-то расширение, или здесь действительно вводятся логические значения?
Берги
8
@Bergi Булевы переменные были «приподняты», чтобы существовать на уровне типов. Это возможно в Haskell с использованием DataKindsрасширения и в языках с зависимой типизацией ... ну, в этом вся суть.
Дж. Абрахамсон,
Не могли бы вы немного поподробнее MonadIx, возможно, привести примеры? Это лучше теоретически или лучше для практического применения?
Кристиан Конкл,
2
@ChristianConkle Я понимаю, что это не очень помогает. Но вы поднимаете совершенно другой вопрос. Локально, когда я говорю, что MonadIx «лучше», я имею в виду в контексте моделирования взаимодействий с непредсказуемой средой. Например, если вашему DVD-приводу разрешено выдавать DVD-диски, он не любит, когда вы пытаетесь их вставить. Некоторые практические ситуации ведут себя так же плохо. Другие имеют большую предсказуемость (то есть вы можете сказать, в каком состоянии начинается любое продолжение, а не то, что операции не завершаются сбоем), и в этом случае с IxMonad легче работать.
Свинарник
1
Когда вы «позаимствуете» обозначение do в ответе, было бы полезно сказать, что это действительно действительный синтаксис с RebindableSyntaxрасширением. Было бы неплохо упомянуть и о других необходимых расширениях, например о вышеупомянутомDataKinds
гигабайты
46

Я знаю как минимум три способа определить индексированную монаду.

Я буду называть эти варианты индексированными монадами а-ля X , где X простирается до компьютерных ученых Боба Этки, Конора Макбрайда и Доминика Орчарда, как я склонен думать о них. Части этих конструкций имеют гораздо более длинную, яркую историю и более приятные интерпретации с помощью теории категорий, но я впервые узнал о них, связанных с этими именами, и я стараюсь, чтобы этот ответ не стал слишком эзотерическим.

Аткей

Стиль индексированной монады Боба Атки заключается в работе с двумя дополнительными параметрами для работы с индексом монады.

Таким образом, вы получите определения, которые люди использовали в других ответах:

class IMonad m where
  ireturn  ::  a -> m i i a
  ibind    ::  m i j a -> (a -> m j k b) -> m i k b

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

Макбрайд

Следующая форма индексированной монады - это определение Конора Макбрайда из его статьи «Клейсли-стрелы возмутительной удачи» . Вместо этого он использует единственный параметр для индекса. Это придает определению индексированной монады довольно умную форму.

Если мы определим естественное преобразование, используя параметричность следующим образом

type a ~> b = forall i. a i -> b i 

тогда мы можем записать определение Макбрайда как

class IMonad m where
  ireturn :: a ~> m a
  ibind :: (a ~> m b) -> (m a ~> m b)

Это выглядит совсем иначе, чем у Атки, но это больше похоже на обычную монаду, вместо того, чтобы строить монаду (m :: * -> *), мы строим ее (m :: (k -> *) -> (k -> *).

Интересно, что вы можете фактически восстановить стиль индексированной монады Атки от Макбрайда, используя умный тип данных, который Макбрайд в своем неподражаемом стиле предпочитает читать как «по ключу».

data (:=) :: a i j where
   V :: a -> (a := i) i

Теперь ты можешь это понять

ireturn :: IMonad m => (a := j) ~> m (a := j)

который расширяется до

ireturn :: IMonad m => (a := j) i -> m (a := j) i

может быть вызван только тогда, когда j = i, и тогда внимательное чтение ibindможет вернуть то же самое, что и Atkey ibind. Вам необходимо передать эти (: =) структуры данных, но они восстанавливают мощь представления Atkey.

С другой стороны, представление Атки недостаточно убедительно, чтобы восстановить все варианты использования версии Макбрайда. Власть была получена строго.

Еще одна приятная вещь заключается в том, что проиндексированная монада МакБрайда - это явно монада, это просто монада в другой категории функторов. Он работает с эндофункторами в категории функторов от (k -> *)до, (k -> *)а не с категорией функторов от *до *.

Забавное упражнение - выяснить, как выполнить преобразование Макбрайда в Аткей для индексированных комонад . Я лично использую тип данных «At» для конструкции «at key» в статье Макбрайда. Я действительно подошел к Бобу Этки на ICFP 2013 и упомянул, что вывернул его наизнанку, превратив его в «пальто». Он выглядел явно обеспокоенным. Эта линия лучше разыгралась в моей голове. знак равно

Фруктовый сад

Наконец, третий, гораздо менее часто упоминаемый заявитель на имя «индексированной монады», связан с Домиником Орчардом, где вместо этого он использует моноид уровня типа для объединения индексов. Вместо того, чтобы вдаваться в подробности конструкции, я просто сделаю ссылку на этот доклад:

https://github.com/dorchard/effect-monad/blob/master/docs/ixmonad-fita14.pdf

Эдвард КМЕТТ
источник
1
Прав ли я, что монада Орчарда эквивалентна монаде Атки, поскольку мы можем перейти от первого ко второму, взяв моноид эндоморфизма, и вернуться назад, кодируя моноидальные присоединения CPS при переходе между состояниями?
Андраш Ковач
Мне это кажется правдоподобным.
Эдвард КМЕТТ
Тем не менее, основываясь на том, что он сказал мне на ICFP 2013, я считаю, что Орчард намеревался, чтобы его семейства типов действовали как настоящий моноид, а не как произвольная категория, в которой некоторые стрелки не могут соединяться, так что в этой истории может быть больше чем это, поскольку конструкция Атки позволяет вам легко ограничить некоторые действия Клейсли от связи с другими - во многих отношениях это и есть суть этого и версии Макбрайда.
Эдвард КМЕТТ
2
Чтобы расширить «внимательное чтение ibind»: введите псевдоним типа Atkey m i j a = m (a := j) i. Используя это как mв определении Atkey, восстанавливаются две сигнатуры, которые мы ищем: ireturnAtkin :: a -> m (a := i) iи ibindAtkin :: m (a := j) i -> (a -> m (b := k) j) -> m (b := k) i. Первый из них получают композиции: ireturn . V. Второй: (1) построение функции forall j. (a := j) j -> m (b := k) jпутем сопоставления с образцом, а затем передача восстановленного значения aвторому аргументу ibindAtkin.
WorldSEnder
23

В качестве простого сценария предположим, что у вас есть монада состояний. Тип состояния является сложным и большим, но все эти состояния можно разделить на два набора: красные и синие состояния. Некоторые операции в этой монаде имеют смысл, только если текущее состояние - синее состояние. Среди них некоторые сохранят состояние синим ( blueToBlue), а другие сделают состояние красным ( blueToRed). В обычной монаде мы могли бы написать

blueToRed  :: State S ()
blueToBlue :: State S ()

foo :: State S ()
foo = do blueToRed
         blueToBlue

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

data Red
data Blue

-- assume a new indexed State monad
blueToRed  :: State S Blue Red  ()
blueToBlue :: State S Blue Blue ()

foo :: State S ?? ?? ()
foo = blueToRed `ibind` \_ ->
      blueToBlue          -- type error

Ошибка типа возникает из-за того, что второй индекс blueToRed( Red) отличается от первого индекса blueToBlue( Blue).

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

data State old new a = State (old -> (new, a))

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

push :: a -> State old (a,old) ()
pop  :: State (a,new) new a

В качестве другого примера предположим, что вам нужна ограниченная монада ввода-вывода, которая не разрешает доступ к файлам. Вы можете использовать, например,

openFile :: IO any FilesAccessed ()
newIORef :: a -> IO any any (IORef a)
-- no operation of type :: IO any NoAccess _

Таким образом, для действия, имеющего тип IO ... NoAccess (), статически гарантируется отсутствие доступа к файлам. Вместо этого действие типа IO ... FilesAccessed ()может получить доступ к файлам. Наличие индексированной монады означало бы, что вам не нужно создавать отдельный тип для ограниченного ввода-вывода, что потребовало бы дублирования каждой функции, не связанной с файлом, в обоих типах ввода-вывода.

чи
источник
19

Индексированная монада - это не конкретная монада, как, например, монада состояния, а своего рода обобщение концепции монады с дополнительными параметрами типа.

В то время как "стандартное" монадическое значение имеет тип, Monad m => m aзначение в индексированной монаде будет IndexedMonad m => m i j awhere iи jявляются индексными типами, так что iэто тип индекса в начале монадического вычисления и jв конце вычисления. В некотором смысле, вы можете думать об этом iкак о типе ввода и jкак о типе вывода.

Используя в Stateкачестве примера, с состоянием вычисления State s aсохраняет состояние типа в sтечение вычисления и возвращает результат типа a. Индексированная версия IndexedState i j a- это вычисление с отслеживанием состояния, при котором состояние может измениться на другой тип во время вычисления. Начальное состояние имеет тип iи состояние, а конец вычисления имеет тип j.

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

шанг
источник
5

Может быть важно посмотреть, как индексирование используется в зависимых типах (например, в agda). Это может объяснить, как в целом помогает индексирование, а затем перенести этот опыт на монады.

Индексирование позволяет устанавливать отношения между конкретными экземплярами типов. Затем вы можете рассуждать о некоторых ценностях, чтобы установить, сохраняется ли эта связь.

Например (в agda) вы можете указать, что некоторые натуральные числа связаны с ними _<_, а тип указывает, какие числа они есть. Затем вы можете потребовать, чтобы некоторая функция получила свидетельство m < n, потому что только тогда функция работает правильно - и без предоставления такого свидетельства программа не будет компилироваться.

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

Индексированные монады позволяют кодировать часть того, что делают системы зависимых типов, для более точного управления побочными эффектами.

Сасса Н.Ф.
источник