Разрабатываем детали типа монадированной свободной монады
Я использую бесплатную монаду для создания DSL. Как часть языка, существует команда input
, цель состоит в том, чтобы отразить, какие типы ожидаются входным примитивом на уровне типа для дополнительной безопасности.
Например, я хочу написать следующую программу.
concat :: Action '[String, String] ()
concat = do
(x :: String) <- input
(y :: String) <- input
output $ x ++ " " ++ y
Наряду с оценочной функцией
eval :: Action params res -> HList params -> [String]
eval = ...
Что работает следующим образом.
> eval concat ("a" `HCons` "b" `HCons` HNil)
["a b"]
Вот что я до сих пор.
data HList i where
HNil :: HList '[]
HCons :: h -> HList t -> HList (h ': t)
type family Append (a :: [k]) (b :: [k]) :: [k] where
Append ('[]) l = l
Append (e ': l) l' = e ': (Append l l')
data ActionF next where
Input :: (a -> next) -> ActionF next
Output :: String -> next -> ActionF next
instance Functor ActionF where
fmap f (Input c) = Input (fmap f c)
fmap f (Output s n) = Output s (f n)
data FreeIx f i a where
Return :: a -> FreeIx f '[] a
Free :: f (FreeIx f i a) -> FreeIx f i a
type Action i a = FreeIx ActionF i a
liftF :: Functor f => f a -> FreeIx f i a
liftF = Free . fmap Return
input :: forall a . Action '[a] a
input = liftF (Input id)
output :: String -> Action '[] ()
output s = liftF (Output s ())
bind :: Functor f => FreeIx f t a -> (a -> FreeIx f v b) -> FreeIx f (Append t v) b
bind (Return a) f = f a
bind (Free x) f = Free (fmap (flip bind f) x)
Проблема заключается в том, что liftF
не вводит проверку.
liftF :: Functor f => Proxy i -> f a -> FreeIx f i a
liftF p = Free . fmap Return
Это правильный подход?
Я думал, что вдохновение может прийти из пакета monad. Это и привело к определению Return
и Free
.
Еще одна предыстория: я видел в нескольких местах, что пользователи будут определять DSL таким образом, а затем определить функцию оценки eval :: Action a -> [String] -> a
или что-то подобное. Ясная проблема с этим подходом заключается в том, что все аргументы должны иметь один и тот же тип, и нет статической гарантии того, что будет предоставлено правильное количество аргументов. Это попытка решить эту проблему.
Ответы
Ответ 1
Я нашел удовлетворительное решение этой проблемы. Вот краткий взгляд на конечный результат:
addTwo = do
(x :: Int) <- input
(y :: Int) <- input
output $ show (x + y)
eval (1 ::: 2 ::: HNil) addTwo = ["3"]
Для этого требуется большое количество шагов. Во-первых, нам нужно заметить, что тип данных ActionF
сам индексируется. Мы адаптируем FreeIx
для создания индексированной монады, используя списки свободных моноидов. Конструктор Free
для FreeIx
должен будет зафиксировать свидетеля конечности одного из двух его индексов для использования в доказательствах. Мы будем использовать систему из-за András Kovács для написания доказательств о добавлении списков уровня типов, чтобы сделать доказательства как ассоциативности, так и правильной идентичности. Мы описываем индексированные монады так же, как Олег Гренрус. Мы будем использовать расширение RebindbableSyntax
для записи выражений для IxMonad
, используя обычную нотацию do
.
Пролог
В дополнение ко всем расширениям, которые уже требуется вашему примеру, и RebindbableSyntax
, которые были упомянуты выше, нам также понадобится UndecidableInstances
для тривиальной цели повторного использования определения семейства типов.
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE RebindableSyntax #-}
Мы будем использовать :~:
GADT из Data.Type.Equality
для управления равенством типов.
import Data.Type.Equality
import Data.Proxy
Поскольку мы будем перепроверять синтаксис Monad
, мы скроем все Monad
из импорта Prelude
. Расширение RebindableSyntax
использует для обозначения do
любые функции >>=
, >>
и fail
в области видимости.
import Prelude hiding (Monad, (>>=), (>>), fail, return)
У нас также есть несколько бит нового кода библиотеки общего назначения. Я дал HList
конструктор инфикса, :::
.
data HList i where
HNil :: HList '[]
(:::) :: h -> HList t -> HList (h ': t)
infixr 5 :::
Я переименовал семейство типов Append
++
, чтобы отобразить оператор ++
в списках.
type family (++) (a :: [k]) (b :: [k]) :: [k] where
'[] ++ l = l
(e ': l) ++ l' = e ': l ++ l'
Полезно говорить о ограничениях формы forall i. Functor (f i)
. Они не существуют в Haskell
вне GADT, которые фиксируют ограничения, такие как Dict
GADT в ограничениях. Для наших целей достаточно определить версию Functor
с дополнительным игнорируемым аргументом.
class Functor1 (f :: k -> * -> *) where
fmap1 :: (a -> b) -> f i a -> f i b
Индексированный ActionF
ActionF
Functor
чего-то не хватало, он не смог захватить информацию о типе уровня о требованиях методов. Мы добавим дополнительный тип индекса i
, чтобы зафиксировать это. Input
требуется один тип, '[a]
, а Output
не требует типов, '[]
. Мы будем называть этот новый тип параметра индексом функтора.
data ActionF i next where
Input :: (a -> next) -> ActionF '[a] next
Output :: String -> next -> ActionF '[] next
Мы напишем экземпляры Functor
и Functor1
для ActionF
.
instance Functor (ActionF i) where
fmap f (Input c) = Input (fmap f c)
fmap f (Output s n) = Output s (f n)
instance Functor1 ActionF where
fmap1 f = fmap f
FreeIx Reconstructed
Мы сделаем два изменения в FreeIx
. Мы изменим, как построены индексы. Конструктор Free
будет ссылаться на индекс из базового функтора и выражать FreeIx
с индексом, что свободная моноидальная сумма (++
) индекса от базового функтора и индекс из обернутого FreeIx
, Мы также потребуем, чтобы Free
фиксировал свидетеля доказательства того, что индекс базового функтора конечен.
data FreeIx f (i :: [k]) a where
Return :: a -> FreeIx f '[] a
Free :: (WitnessList i) => f i (FreeIx f j a) -> FreeIx f (i ++ j) a
Мы можем определить экземпляры Functor
и Functor1
для FreeIx
.
instance (Functor1 f) => Functor (FreeIx f i) where
fmap f (Return a) = Return (f a)
fmap f (Free x) = Free (fmap1 (fmap f) x)
instance (Functor1 f) => Functor1 (FreeIx f) where
fmap1 f = fmap f
Если мы хотим использовать FreeIx
с обычным, неиндексированным функтором, мы можем поднять эти значения на неограниченный проиндексированный функтор IxIdentityT
. Это не нужно для ответа.
data IxIdentityT f i a = IxIdentityT {runIxIdentityT :: f a}
instance Functor f => Functor (IxIdentityT f i) where
fmap f = IxIdentityT . fmap f . runIxIdentityT
instance Functor f => Functor1 (IxIdentityT f) where
fmap1 f = fmap f
Доказательства
Нам нужно будет доказать два свойства о добавлении списков уровней типов. Чтобы написать liftF
, нам нужно будет доказать правильное тождество xs ++ '[] ~ xs
. Мы будем называть это доказательство appRightId
для добавления правильного идентификатора. Чтобы написать bind
, нам нужно доказать ассоциативность xs ++ (yz ++ zs) ~ (xs ++ ys) ++ zs
, которую будем называть appAssoc
.
Доказательства записываются в виде списка преемников, который по существу является списком прокси, по одному для каждого типа type SList xs ~ HFMap Proxy (HList xs)
.
data SList (i :: [k]) where
SNil :: SList '[]
SSucc :: SList t -> SList (h ': t)
Следующее доказательство ассоциативности наряду с методом написания этого доказательства
из-за András Kovács. Используя только SList
для списка типов xs
, мы деконструируем и используем Proxy
для других списков типов, мы можем задерживать (возможно, бесконечно) необходимость WitnessList
экземпляров для ys
и zs
.
appAssoc ::
SList xs -> Proxy ys -> Proxy zs ->
(xs ++ (ys ++ zs)) :~: ((xs ++ ys) ++ zs)
appAssoc SNil ys zs = Refl
appAssoc (SSucc xs) ys zs =
case appAssoc xs ys zs of Refl -> Refl
Refl
, конструктор для :~:
, может быть создан только тогда, когда компилятор имеет доказательство того, что оба типа равны. Согласование шаблонов на Refl
вводит доказательство равенства типа в текущую область.
Мы можем доказать правое тождество аналогичным образом
appRightId :: SList xs -> xs :~: (xs ++ '[])
appRightId SNil = Refl
appRightId (SSucc xs) = case appRightId xs of Refl -> Refl
Чтобы использовать эти доказательства, мы создаем списки свидетелей для класса списков конечного типа.
class WitnessList (xs :: [k]) where
witness :: SList xs
instance WitnessList '[] where
witness = SNil
instance WitnessList xs => WitnessList (x ': xs) where
witness = SSucc witness
Подъемное
Оборудованный appRightId
, мы можем определить значения подъема из базового функтора в FreeIx
.
liftF :: forall f i a . (WitnessList i, Functor1 f) => f i a -> FreeIx f i a
liftF = case appRightId (witness :: SList i) of Refl -> Free . fmap1 Return
Явный forall
для ScopedTypeVariables
. Свидетельству о конечности индекса WitnessList i
требуется как конструктор Free
, так и appRightId
. Доказательство appRightId
используется, чтобы убедить компилятор, что построенный FreeIx f (i ++ '[]) a
имеет тот же тип, что и FreeIx f i a
. Этот '[]
появился из Return
, который был обернут в базовый функтор.
Наши две команды Input
и Output
записываются в терминах liftF
.
type Action i a = FreeIx ActionF i a
input :: Action '[a] a
input = liftF (Input id)
output :: String -> Action '[] ()
output s = liftF (Output s ())
IxMonad и привязка
Чтобы использовать RebindableSyntax
, мы определим класс IxMonad
с теми же именами функций (>>=)
, (>>)
и fail
как Monad
, но разными типами. Этот класс описан в ответе Олега Гренруса.
class Functor1 m => IxMonad (m :: k -> * -> *) where
type Unit :: k
type Plus (i :: k) (j :: k) :: k
return :: a -> m Unit a
(>>=) :: m i a -> (a -> m j b) -> m (Plus i j) b
(>>) :: m i a -> m j b -> m (Plus i j) b
a >> b = a >>= const b
fail :: String -> m i a
fail s = error s
Реализация bind
для FreeIx
требует доказательства ассоциативности, appAssoc
. Единственный экземпляр WitnessList
в области видимости, WitnessList i
- это тот, который был захвачен деконструированным конструктором Free
. Еще раз, явный forall
для ScopedTypeVariables
.
bind :: forall f i j a b. (Functor1 f) => FreeIx f i a -> (a -> FreeIx f j b) -> FreeIx f (i ++ j) b
bind (Return a) f = f a
bind (Free (x :: f i1 (FreeIx f j1 a))) f =
case appAssoc (witness :: SList i1) (Proxy :: Proxy j1) (Proxy :: Proxy j)
of Refl -> Free (fmap1 (`bind` f) x)
bind
является единственной интересной частью экземпляра IxMonad
для FreeIx
.
instance (Functor1 f) => IxMonad (FreeIx f) where
type Unit = '[]
type Plus i j = i ++ j
return = Return
(>>=) = bind
Мы закончили
Вся сложная часть выполнена. Мы можем написать простой интерпретатор для Action xs ()
самым прямым способом. Единственный трюк, который требуется, - избегать сопоставления шаблонов в конструкторе HList
:::
, пока не будет известен список типов i
, поскольку мы уже сопоставлены с Input
.
eval :: HList i -> Action i () -> [String]
eval inputs action =
case action of
Return () -> []
Free (Input f) ->
case inputs of
(x ::: xs) -> eval xs (f x)
Free (Output s next) -> s : eval inputs next
Если вам интересно узнать о предполагаемом типе addTwo
addTwo = do
(x :: Int) <- input
(y :: Int) <- input
output $ show (x + y)
это
> :t addTwo
addTwo :: FreeIx ActionF '[Int, Int] ()
Ответ 2
У меня есть новое решение, которое является простым и общеприменимым.
До сих пор в потоке мы использовали монады, индексированные моноидом, но здесь я полагаюсь на другое популярное понятие индексированной монады, которая имеет типичные переходы (стиль логики Хоар):
return :: a -> m i i a
(>>=) :: m i j a -> (a -> m j k b) -> m i k b
Я считаю, что два подхода эквивалентны (по крайней мере теоретически), поскольку мы получаем монаду Хоара путем индексирования с моноидом эндоморфизма, и мы также можем пойти в обратном направлении с помощью CPS, кодирующего моноидальные приложения в состояниях. На практике язык типа Haskell и тип уровня языка довольно слабый, поэтому перемещение назад и вперед между двумя представлениями не является вариантом.
Существует проблема, хотя с указанным выше типом для >>=
: это означает, что мы должны вычислить typestate в порядке сверху вниз, i. е. он заставляет следующее определение для IxFree
:
data IxFree f i j a where
Pure :: a -> IxFree f i i a
Free :: f i j (IxFree f j k a) -> IxFree f i k a
Итак, если у нас есть выражение Free exp
, то мы сначала переходим от i
до j
после конструктора exp
, а затем получаем от j
до k
, проверяя субэксперсии exp
. Это означает, что если мы попытаемся скопировать типы input
в список, мы получим обратный список:
-- compute transitions top-down
test = do
(x :: Int) <- input -- prepend Int to typestate
(y :: String) <- input -- prepend String to typestate
return () -- do nothing
Если мы добавим типы в конец списка, порядок будет правильным. Но для того, чтобы эта работа в Haskell (особенно для работы eval
) потребовала бы изнурительного количества написания доказательств, если это вообще возможно.
Пусть вместо этого вычисляется типичное снизу вверх. Он делает все виды вычислений, где мы создаем некоторую структуру данных в зависимости от дерева синтаксиса, гораздо более естественного, и в частности это делает нашу работу очень легкой здесь.
{-# LANGUAGE
RebindableSyntax, DataKinds,
GADTs, TypeFamilies, TypeOperators,
PolyKinds, StandaloneDeriving, DeriveFunctor #-}
import Prelude hiding (Monad(..))
class IxFunctor (f :: ix -> ix -> * -> *) where
imap :: (a -> b) -> f i j a -> f i j b
class IxFunctor m => IxMonad (m :: ix -> ix -> * -> *) where
return :: a -> m i i a
(>>=) :: m j k a -> (a -> m i j b) -> m i k b -- note the change of index orders
(>>) :: m j k a -> m i j b -> m i k b -- here too
a >> b = a >>= const b
fail :: String -> m i j a
fail = error
data IxFree f i j a where
Pure :: a -> IxFree f i i a
Free :: f j k (IxFree f i j a) -> IxFree f i k a -- compute bottom-up
instance IxFunctor f => Functor (IxFree f i j) where
fmap f (Pure a) = Pure (f a)
fmap f (Free fa) = Free (imap (fmap f) fa)
instance IxFunctor f => IxFunctor (IxFree f) where
imap = fmap
instance IxFunctor f => IxMonad (IxFree f) where
return = Pure
Pure a >>= f = f a
Free fa >>= f = Free (imap (>>= f) fa)
liftf :: IxFunctor f => f i j a -> IxFree f i j a
liftf = Free . imap Pure
Теперь реализация Action
становится простой.
data ActionF i j next where
Input :: (a -> next) -> ActionF i (a ': i) next
Output :: String -> next -> ActionF i i next
deriving instance Functor (ActionF i j)
instance IxFunctor ActionF where
imap = fmap
type family (++) xs ys where -- I use (++) here only for the type synonyms
'[] ++ ys = ys
(x ': xs) ++ ys = x ': (xs ++ ys)
type Action' xs rest = IxFree ActionF rest (xs ++ rest)
type Action xs a = forall rest. IxFree ActionF rest (xs ++ rest) a
input :: Action '[a] a
input = liftf (Input id)
output :: String -> Action '[] ()
output s = liftf (Output s ())
data HList i where
HNil :: HList '[]
(:::) :: h -> HList t -> HList (h ': t)
infixr 5 :::
eval :: Action' xs r a -> HList xs -> [String]
eval (Pure a) xs = []
eval (Free (Input k)) (x ::: xs) = eval (k x) xs
eval (Free (Output s nxt)) xs = s : eval nxt xs
addTwice :: Action [Int, Int] ()
addTwice = do
x <- input
y <- input
output (show $ x + y)
Чтобы сделать вещи менее запутанными для пользователей, я ввел синонимы типов с более дружественными схемами индексов: Action' xs rest a
означает, что действие читается из xs
, и за ним могут следовать действия, содержащие rest
. Action
- это синоним типа, эквивалентный тому, который появляется в вопросе нити.
Мы можем реализовать различные DSL-ы с этим подходом. Обратный порядок ввода дает немного вращения, но мы можем делать обычные индексированные монады все равно. Здесь монада с индексированным состоянием, например:
data IxStateF i j next where
Put :: j -> next -> IxStateF j i next
Get :: (i -> next) -> IxStateF i i next
deriving instance Functor (IxStateF i j)
instance IxFunctor IxStateF where imap = fmap
put s = liftf (Put s ())
get = liftf (Get id)
type IxState i j = IxFree IxStateF j i
evalState :: IxState i o a -> i -> (a, o)
evalState (Pure a) i = (a, i)
evalState (Free (Get k)) i = evalState (k i) i
evalState (Free (Put s k)) i = evalState k s
test :: IxState Int String ()
test = do
n <- get
put (show $ n * 100)
Теперь я считаю, что этот подход является более практичным, чем индексирование с моноидами, потому что у Haskell нет классных классов или функций класса первого класса, которые сделали бы моноидный подход приемлемым. Было бы неплохо иметь класс VerifiedMonoid
, например, в Idris
или Agda
, который включает доказательства корректности, помимо обычных методов. Таким образом, мы могли бы написать FreeIx
, который является общим в выборе моноида индекса, и не ограничивается отмененными списками или чем-то еще.
Ответ 3
В скором времени об индексированных монадах: они являются монадами, индексированными моноидами. Для сравнения monad:
class Monad m where
return :: a -> m a
bind :: m a -> (a -> m b) -> m b
-- or `bind` alternatives:
fmap :: (a -> b) -> m a -> m b
join :: m (m a) -> m a
Моноид - это тип, оснащенный mempty
- идентификационным элементом, и (<>) :: a -> a -> a
двоичная ассоциативная операция. На уровне уровня мы могли бы иметь тип Unit
и Plus
ассоциативный двоичный тип операции. Обратите внимание: список представляет собой свободный моноид на уровне значения, а HList
- на уровне типа.
Теперь мы можем определить индексированный класс моноида:
class IxMonad m where
type Unit
type Plus i j
return :: a -> m Unit a
bind :: m i a -> (a -> m j b) -> m (Plus i j) b
--
fmap :: (a -> b) -> m i a -> m i b
join :: m i (m j a) -> m (Plus i j) a
Вы можете указать законы монады для индексированной версии. Вы заметите, что для выравнивания индексов они должны подчиняться моноидным законам.
С помощью бесплатной монады вы хотите оснастить Functor
операциями return
и join
. Немного изменив ваше определение:
data FreeIx f i a where
Return :: a -> FreeIx f '[] a -- monoid laws imply we should have `[] as index here!
Free :: f (FreeIx f k a) -> FreeIx f k a
bind :: Functor f => FreeIx f i a -> (a -> FreeIx f j b) -> FreeIx f (Append i j) b
bind (Return a) f = f a
bind (Free x) f = Free (fmap (flip bind f) x)
Я должен признать, что я не уверен на 100%, как индексы конструктора Free
оправданы, но они, похоже, работают. Если мы рассмотрим функцию wrap :: f (m a) -> m a
класса MonadFree
с законом:
wrap (fmap f x) ≡ wrap (fmap return x) >>= f
и комментарий о Free
в Free
пакете
На практике вы можете просто просмотреть Free f
a столько слоев f
, которые обернуты вокруг значений типа a
, где (>>=)
выполняет подстановку и трансплантирует новые слои f
для каждого из свободных переменных.
тогда идея состоит в том, что значения упаковки не влияют на индекс.
Тем не менее, вы хотите поднять любое значение f
на произвольное индексированное монадическое значение. Это очень разумное требование. Но единственное действительное значение определения заставляет поднятое значение иметь индекс '[]
- Unit
или mempty
:
liftF :: Functor f => f a -> FreeIx f '[] a
liftF = Free . fmap Return
Если вы попытаетесь изменить определение return
на :: a -> FreeIx f k a
(k
, а не []
- чистое значение может иметь произвольный индекс), то определение bind
не будет вводить проверку.
Я не уверен, что вы можете сделать бесплатную индексированную монаду только с небольшими исправлениями. Одна идея - поднять произвольную монаду в индексированную монаду:
data FreeIx m i a where
FreeIx :: m a -> FreeIx m k a
liftF :: Proxy i -> f a -> FreeIx f i a
liftF _ = FreeIx
returnIx :: Monad m => a -> FreeIx m i a
returnIx = FreeIx . return
bind :: Monad m => FreeIx m i a -> (a -> FreeIx m j b) -> FreeIx m (Append i j) b
bind (FreeIx x) f = FreeIx $ x >>= (\x' -> case f x' of
FreeIx y -> y)
Этот подход немного похож на обман, так как мы всегда можем переопределить значение.
Другой подход - напомнить Functor
его индексированный функтор или сразу начать с индексированного функтора, как в ответе Cirdec.
Ответ 4
Если вы готовы пожертвовать неявным порядком и использовать явные аксессоры, ваш Action '[Int, Int]
может быть реализован с помощью ReaderT (HList '[Int, Int])
. Если вы используете существующую библиотеку типа vinyl
, которая предоставляет объективы, вы можете написать что-то вроде этого:
-- Implemented with pseudo-vinyl
-- X and Y are Int fields, with accessors xField and yField
addTwo :: ReaderT (PlainRec '[X, Y]) Output ()
addTwo = do
x <- view (rGet xField)
y <- view (rGet yField)
lift . output $ show (x + y) -- output :: String -> Output ()
Безопасность типа обеспечивается принудительным распространением: rGet xField
вводит требование, чтобы X
был членом записи.
Для более простой иллюстрации без машин типового уровня сравните:
addTwo :: ReaderT (Int, Int) IO ()
addTwo = do
x <- view _1
y <- view _2
lift . putStrLn $ show (x + y)
Мы теряем свойство упорядочения, что является значительной потерей, особенно если упорядочение имеет смысл, например. представляет собой порядок взаимодействия с пользователем.
Кроме того, теперь мы должны использовать runReaderT
(~ eval
). Мы не можем, скажем, чередовать ввод пользователя с выходом.
Ответ 5
EDIT: я опубликовал более общий альтернативный ответ. я ухожу этот ответ здесь, поскольку это может быть полезным примером для построение целевой монады вручную.
Мое решение делает то, о чем попросил OP (хотя он включает ручную запись экземпляра монады, поэтому есть место для уточнения).
Пакет effect-monad
(который упоминается в OP) уже содержит эффект, который обрабатывает чтение из HList
. Он называется ReadOnceReader
. Однако для Output
нам также нужен эффект Writer
, и мне кажется, что библиотека не позволяет нам объединить эти два.
Мы все еще можем взять идею ReadOnceReader
и вручную написать AST для желаемого языка. Конечно, AST должен быть проиндексированной монадой. Это было бы аккуратно, если бы мы могли сделать это через индексированную свободную монаду или оперативную монаду. До сих пор у меня не было успеха со свободными монадами. Я мог бы обновить свой ответ после того, как посмотрел на операционные монады.
Отборочный:
{-# LANGUAGE
RebindableSyntax, DataKinds, ScopedTypeVariables,
GADTs, TypeFamilies, TypeOperators,
PolyKinds, StandaloneDeriving, DeriveFunctor #-}
import Prelude hiding (Monad(..))
data HList (xs :: [*]) where
Nil :: HList '[]
(:>) :: x -> HList xs -> HList (x ': xs)
infixr 5 :>
type family (++) (xs :: [*]) (ys :: [*]) where
'[] ++ ys = ys
(x ': xs) ++ ys = x ': (xs ++ ys)
Индексированные монады должны обеспечивать способ объединения (Plus
) индексов с идентификатором (Unit
). Короче говоря, индексы должны быть моноидами.
class IxMonad (m :: k -> * -> *) where
type Unit m :: k
type Plus m (i :: k) (j :: k) :: k
return :: a -> m (Unit m) a
(>>=) :: m i a -> (a -> m j b) -> m (Plus m i j) b
fail :: m i a
Здесь интересен тип Input
: мы добавляем тип ввода к полученному индексу следующего вычисления:
data Action i a where
Return :: a -> Action '[] a
Input :: (x -> Action xs a) -> Action (x ': xs) a
Output :: String -> Action i a -> Action i a
deriving instance Functor (Action i)
Экземпляр IxMonad
и интеллектуальные конструкторы полностью стандартные, а функция eval
также реализована прямо.
instance IxMonad Action where
type Unit Action = '[]
type Plus Action i j = i ++ j
return = Return
Return a >>= f = f a
Input k >>= f = Input ((>>= f) . k)
Output s nxt >>= f = Output s (nxt >>= f)
fail = undefined
input :: Action '[a] a
input = Input Return
output :: String -> Action '[] ()
output s = Output s (Return ())
eval :: Action xs a -> HList xs -> [String]
eval (Return a) xs = []
eval (Input k) (x :> xs) = eval (k x) xs
eval (Output s nxt) xs = s : eval nxt xs
Теперь все работает по желанию:
concat' :: Action '[String, String] ()
concat' = do
(x :: String) <- input
(y :: String) <- input
output $ x ++ " " ++ y
main = print $ eval concat' ("a" :> "b" :> Nil)
-- prints ["a b"]
Ответ 6
У меня есть рабочая реализация индексированной свободной монады на github за несколько лет назад:
https://github.com/ekmett/indexed/blob/master/src/Indexed/Monad/Free.hs
Он использует форму индексированной монады, предложенную Конором Макбрайдом в Kleisli Arrows of Outrageous Fortune, и которая может быть адаптирована для обеспечения 2- в монашеской монаде в стиле Боба Атки, как описано в статье.