Как в Haskell встроить одну монаду Free в другую?

У меня есть две бесплатные монады для разных операций в разных контекстах. Однако один (major) DSL должен содержать другой (action), если конкретная операция находится в контексте:

import Control.Monad.Free

data ActionFunctor next = Wait Timeout next
                        | Read URI next

instance Functor ActionFunctor where
  fmap f (Wait timeout next)  = Wait timeout (f next)
  fmap f (Read uri next)      = Read uri (f next)

type Action = Free ActionFunctor


data MajorFunctor next = Log LogText next
                       | Act Action next
                       | Send Message

instance Functor MajorFunctor where
  fmap f (Log text next)    = Log text (f next)
  fmap f (Act action next)  = Act action (f next)
  fmap f (Send message)     = Send message

type Major = Free MajorFunctor

Проблема в том, что GHC будет жаловаться MajorFunctor на то, что Action в Act Action next является своего рода (* -> *), а не просто типом. Это связано с тем, что в определении data ActionFunctor он должен принимать next в качестве параметра типа, а в строке Act Action такого параметра нет. Но даже сообщение для меня ясно, я не уверен, должен ли я объявлять такой дополнительный параметр типа в функторе Major:

data MajorFunctor actionNext next = ...

Это выглядит странно, потому что только один конструктор данных будет использовать параметр, в то время как такое раскрытие превратит каждое MajorFunctor в MajorFunctor actionNext, и это выглядит полностью раскрывающим слишком много деталей. Поэтому я взглянул на трансформатор FreeT, чтобы убедиться, что это то, что мне нужно. Однако в моем случае мне нужно вызывать интерпретатор Action только тогда, когда программа DSL имеет такую ​​операцию, а не каждый bind в монадической программе, поэтому я также не уверен, что преобразователь является хорошим решением.


person snowmantw    schedule 05.07.2016    source источник


Ответы (2)


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

{-# LANGUAGE ExistentialQuantification #-}
data MajorFunctor next = Log LogText next
                       | forall a. Act (Action a) (a -> next)
                       | Send Message

instance Functor MajorFunctor where
  fmap f (Log text next)    = Log text (f next)
  fmap f (Act action next)  = Act action (fmap f next)
  fmap f (Send message)     = Send message
person Cactus    schedule 05.07.2016

Более простое решение, не требующее экзистенциала, состоит в том, чтобы встроить параметр next в файл Action.

data MajorFunctor next = Log LogText next
                       | Act (Action next)
                       | Send Message

instance Functor MajorFunctor where
  fmap f (Log text next) = Log text (f next)
  fmap f (Act action) = Act (fmap f action)
  fmap f (Send message) = Send message

Это говорит: «Когда вы выполняете Action, он возвращает next», тогда как решение @Cactus говорит: «Когда вы выполняете Action, он возвращает что-то (неизвестного (экзистенциального) типа), которое может (только) быть повернуто в в next".

Лемма Ко-Йонеды говорит, что эти два решения изоморфны. Моя версия проще, но версия Cactus может быть быстрее для некоторых операций, таких как повторение fmaps над большими Actions.

person Benjamin Hodgson♦    schedule 05.07.2016
comment
Хорошее решение! Какое умное применение Coyoneda. ^-^ - person Lynn; 05.07.2016