Как закодировать возможные переходы состояний в типе?

Я пытаюсь воспроизвести в Haskell этот фрагмент кода Idris, который обеспечивает правильную последовательность действий через типы:

 data DoorState = DoorClosed | DoorOpen
 data DoorCmd : Type ->
                DoorState ->
 DoorState ->
                Type where
      Open : DoorCmd     () DoorClosed DoorOpen
      Close : DoorCmd    () DoorOpen   DoorClosed
      RingBell : DoorCmd () DoorClosed DoorClosed
      Pure : ty -> DoorCmd ty state state
      (>>=) : DoorCmd a state1 state2 ->
              (a -> DoorCmd b state2 state3) ->
              DoorCmd b state1 state3

Благодаря перегрузке оператора (>>=) можно написать монадический код, например:

do Ring 
   Open 
   Close

но компилятор отклоняет неправильные переходы, например:

do Ring
   Open 
   Ring
   Open

Я попытался следовать этому шаблону в следующем фрагменте Haskell:

 data DoorState = Closed | Opened

 data DoorCommand (begin :: DoorState) (end :: DoorState) a where
   Open  :: DoorCommand 'Closed 'Opened ()
   Close :: DoorCommand 'Opened 'Closed ()
   Ring  :: DoorCommand 'Closed 'Closed ()

   Pure  :: x -> DoorCommand b e x
   Bind  :: DoorCommand b e x -> (x -> DoorCommand e f y) -> DoorCommand b f y

 instance Functor (DoorCommand b e) where
   f `fmap` c = Bind c (\ x -> Pure (f x))

 -- instance Applicative (DoorCommand b e) where
 --    pure    = Pure
 --    f <*> x = Bind f (\ f' -> Bind x (\ x' -> Pure (f' x')))

 -- instance Monad (DoorCommand b e) where
 --   return = Pure
 --   (>>=) = Bind

Но, конечно, это не удается: экземпляры Applicative и Monad не могут быть определены правильно, поскольку для правильной последовательности операций им требуются два разных экземпляра. Конструктор Bind можно использовать для обеспечения правильной последовательности, но я не могу использовать "более приятную" нотацию.

Как я мог написать этот код, чтобы иметь возможность использовать do-notation, например. чтобы предотвратить недопустимые последовательности Commands ?


person insitu    schedule 22.10.2016    source источник
comment
Хотя я только слышал об этом трюке, я подозреваю, что вы ищете индексированные монады.   -  person duplode    schedule 23.10.2016
comment
Если вы хотите, вы всегда можете использовать RebindableSyntax для получения do без необходимости делать фактическое Monad.   -  person Alec    schedule 23.10.2016


Ответы (1)


То, что вы ищете, действительно является параметризованной монадой Аткея, теперь более широко известный как индексированная монада.

class IFunctor f where
    imap :: (a -> b) -> f i j a -> f i j b
class IFunctor m => IMonad m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b

IMonad — это класс монадоподобных вещей, m :: k -> k -> * -> * описывающих пути через ориентированный граф типов, принадлежащих виду k. >>>= связывает вычисление, которое переводит состояние уровня типа из i в j, в вычисление, которое переводит его из j в k, возвращая большее вычисление из i в k. ireturn позволяет вам преобразовать чистое значение в монадическое вычисление, которое не изменяет состояние уровня типа.

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

data IFree f i j a where
    IReturn :: a -> IFree f i i a
    IFree :: f i j (IFree f j k a) -> IFree f i k a

instance IFunctor f => IFunctor (IFree f) where
    imap f (IReturn x) = IReturn (f x)
    imap f (IFree ff) = IFree $ imap (imap f) ff
instance IFunctor f => IMonad (IFree f) where
    ireturn = IReturn
    IReturn x >>>= f = f x
    IFree ff >>>= f = IFree $ imap (>>>= f) ff

Мы можем бесплатно построить вашу монаду Door из следующего функтора:

data DoorState = Opened | Closed
data DoorF i j next where
    Open :: next -> DoorF Closed Opened next
    Close :: next -> DoorF Opened Closed next
    Ring :: next -> DoorF Closed Closed next

instance IFunctor DoorF where
    imap f (Open x) = Open (f x)
    imap f (Close x) = Close (f x)
    imap f (Ring x) = Ring (f x)

type Door = IFree DoorF

open :: Door Closed Opened ()
open = IFree (Open (IReturn ()))
close :: Door Opened Closed ()
close = IFree (Close (IReturn ()))
ring :: Door Closed Closed ()
ring = IFree (Ring (IReturn ()))

Вы можете open дверь, из-за которой закрытая в данный момент дверь становится открытой, close дверь, открытая в данный момент, или ring звонок двери, которая остается закрытой, предположительно потому, что обитатель дома не хочет вас видеть.

Наконец, языковое расширение RebindableSyntax означает, что мы можем заменить стандартный класс Monad нашим собственным IMonad.

(>>=) = (>>>=)
m >> n = m >>>= const n
return = ireturn
fail = undefined

door :: Door Open Open ()
door = do
    close
    ring
    open

Однако я заметил, что вы на самом деле не используете структуру привязки своей монады. Ни один из ваших стандартных блоков Open, Close или Ring не возвращает значение. Поэтому я думаю, что вам действительно нужен следующий, более простой тип списка, выровненного по типу:

data Path g i j where
    Nil :: Path g i i
    Cons :: g i j -> Path g j k -> Path g i k

Функционально Path :: (k -> k -> *) -> k -> k -> * подобен связному списку, но имеет дополнительную структуру на уровне типов, снова описывающую путь через ориентированный граф, узлы которого находятся в k. Элементами списка являются ребра g. Nil говорит, что вы всегда можете найти путь от узла i к самому себе, а Cons напоминает нам, что путешествие в тысячу миль начинается с одного шага: если у вас есть ребро от i до j и путь от j до k, вы можете объедините их, чтобы сделать путь от i до k. Он называется списком с выравниванием по типу, потому что конечный тип одного элемента должен совпадать с начальным типом следующего.

На другой стороне улицы Карри-Ховард, если g является бинарным логическим отношением, то Path g строит свое рефлексивное транзитивное замыкание. Или, категорически, Path g — это тип морфизмов в свободной категории графа g. Составление морфизмов в свободной категории — это просто (перевернутое) добавление списков, выровненных по типу.

instance Category (Path g) where
    id = Nil
    xs . Nil = xs
    xs . Cons y ys = Cons y (xs . ys)

Тогда мы можем записать Door через Path:

data DoorAction i j where
    Open :: DoorAction Closed Opened
    Close :: DoorAction Opened Closed
    Ring :: DoorAction Closed Closed

type Door = Path DoorAction

open :: Door Closed Opened
open = Cons Open Nil
close :: Door Opened Closed
close = Cons Close Nil
ring :: Door Closed Closed
ring = Cons Ring Nil

door :: Door Open Open
door = open . ring . close

Вы не получаете нотацию do (хотя я думаю, что RebindableSyntax позволяет вам перегружать литералы списков), но построение вычислений с помощью (.) выглядит как последовательность чистых функций, что, как мне кажется, является довольно хорошей аналогией того, что ты все равно делаешь. Для меня требуются дополнительные умственные способности — редкий и ценный природный ресурс — для использования индексированных монад. Лучше избегать сложности монад, когда подойдет более простая структура.

person Benjamin Hodgson♦    schedule 23.10.2016
comment
Спасибо за подробный и очень полезный ответ. Я подозреваю, что мог бы использовать RebindableSyntax, чтобы фактически получить do-нотацию в случае списка, выровненного по типу. - person insitu; 23.10.2016
comment
Обратите внимание, что использование индексированных монад в этом (Боба Атки) смысле хорошо, поскольку все операции оказывают предсказуемое влияние на состояние двери. Если вы когда-нибудь захотите отдать некоторый контроль, например. для людей по другую сторону двери (которые могут решать, открывать ее или нет), вы выиграете от чего-то более гибкого. - person pigworker; 23.10.2016
comment
Конечно. Я прочитал этот другой пост SO об индексированных монадах: /28690448/what-is-indexed-monad и сейчас занимаюсь вашей статьей :) Можно ли добиться такого же поведения без препроцессора HSE? Я должен попробовать сам, конечно, но пока не нашел времени, и я подозреваю, что ответ да. - person insitu; 23.10.2016
comment
Конечно. Все, что делает SHE, — это скрывает одноэлементную кодировку, чтобы все выглядело как программирование с зависимым типом. - person pigworker; 23.10.2016
comment
@insitu да, я думаю, вы могли бы так оскорблять RebindableSyntax. Но с инженерной точки зрения я не думаю, что это очень хорошая идея использовать нотацию do для вещей, которые не являются монадами. - person Benjamin Hodgson♦; 23.10.2016
comment
@pigworker Из любопытства, что вы думаете об этом посте утверждая, что ваши индексированные монады и монады Атки одинаково сильны? Я думаю, это просто вопрос того, что более естественно использовать в данном приложении? - person Benjamin Hodgson♦; 23.10.2016
comment
Замечу, что перевод монад на индексированных множествах в индексированные монады на множествах происходит через продолжения, т. е. путем введения дополнительных управляющих операторов. Когда вы используете монады в индексированных наборах, вы можете придерживаться стиля алгебраических эффектов для большего количества операций. - person pigworker; 23.10.2016
comment
@BenjaminHodgson Интересно, что Идрис пошел по этому пути: в примерах, приведенных в Type Driven Development with Idris, изобилуют злоупотребления нотациями do. Я согласен с тем, что в Haskell do-нотация настолько тесно связана с монадами, что использование ее для чего-то другого может вызвать проблемы с обслуживанием. - person insitu; 23.10.2016