2016-11-25 3 views
9

Я пытаюсь работать с индексированной свободной монадой (Олег Киселев имеет an intro). Я также хочу, чтобы свободная монада была построена из копродукта функторов a la Data Types a la carte. Тем не менее, у меня возникли проблемы с тем, чтобы получить класс типа инъекции coproduct. Вот что у меня есть до сих пор:Инъекция Индексированный функтор в Functor Coproduct

{-# LANGUAGE DataKinds #-} 
{-# LANGUAGE FlexibleContexts #-} 
{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE GADTs #-} 
{-# LANGUAGE KindSignatures #-} 
{-# LANGUAGE MultiParamTypeClasses #-} 
{-# LANGUAGE PolyKinds #-} 
{-# LANGUAGE RankNTypes #-} 
{-# LANGUAGE RebindableSyntax #-} 
{-# LANGUAGE TypeFamilies #-} 
{-# LANGUAGE TypeOperators #-} 
{-# LANGUAGE UndecidableInstances #-} 

module Example where 

import Control.Monad.Indexed 
import Data.Kind 
import Data.TASequence.FastCatQueue 
import Prelude hiding ((>>), (>>=)) 

-- * Indexed free machinery 

-- For use with `RebindableSyntax` 
(>>=) 
    :: (IxMonad m) 
    => m s1 s2 a -> (a -> m s2 s3 b) -> m s1 s3 b 
(>>=) = (>>>=) 
(>>) 
    :: (IxApplicative m) 
    => m s1 s2 a -> m s2 s3 b -> m s1 s3 b 
f >> g = imap (const id) f `iap` g 

type family Fst x where 
    Fst '(a, b) = a 
type family Snd x where 
    Snd '(a, b) = b 

newtype IKleisliTupled m ia ob = IKleisliTupled 
    { runIKleisliTupled :: Snd ia -> m (Fst ia) (Fst ob) (Snd ob) 
    } 

data Free f s1 s2 a where 
    Pure :: a -> Free f s s a 
    Impure :: 
    f s1 s2 a -> 
     FastTCQueue (IKleisliTupled (Free f)) '(s2, a) '(s3, b) -> 
     Free f s1 s3 b 

instance IxFunctor (Free f) where 
    imap f (Pure a) = Pure $ f a 
    imap f (Impure a g) = Impure a (g |> IKleisliTupled (Pure . f)) 
instance IxPointed (Free f) where 
    ireturn = Pure 
instance IxApplicative (Free f) where 
    iap (Pure f) (Pure a) = ireturn $ f a 
    iap (Pure f) (Impure a g) = Impure a (g |> IKleisliTupled (Pure . f)) 
    iap (Impure a f) m = Impure a (f |> IKleisliTupled (`imap` m)) 
instance IxMonad (Free f) where 
    ibind f (Pure a) = f a 
    ibind f (Impure a g) = Impure a (g |> IKleisliTupled f) 

-- * Example application 

data FileStatus 
    = FileOpen 
    | FileClosed 
data File i o a where 
    Open :: FilePath -> File 'FileClosed 'FileOpen() 
    Close :: File 'FileOpen 'FileClosed() 
    Read :: File 'FileOpen 'FileOpen String 
    Write :: String -> File 'FileOpen 'FileOpen() 

data MayFoo 
    = YesFoo 
    | NoFoo 
data Foo i o a where 
    Foo :: Foo 'NoFoo 'YesFoo() 

data MayBar 
    = YesBar 
    | NoBar 
data Bar i o a where 
    Bar :: Bar 'YesBar 'NoBar() 

-- * Coproduct of indexed functors 

infixr 5 `SumP` 
data SumP f1 f2 t1 t2 x where 
    LP :: f1 sl1 sl2 x -> SumP f1 f2 '(sl1, sr) '(sl2, sr) x 
    RP :: f2 sr1 sr2 x -> SumP f1 f2 '(sl, sr1) '(sl, sr2) x 

-- * Attempt 1 

class Inject l b where 
    inj :: forall a. l a -> b a 

instance Inject (f i o) (f i o) where 
    inj = id 

instance Inject (fl il ol) (SumP fl fr '(il, s) '(ol, s)) where 
    inj = LP 

instance (Inject (f i' o') (fr is os)) => 
     Inject (f i' o') (SumP fl fr '(s, is) '(s, os)) where 
    inj = RP . inj 

send 
    :: Inject (t i o) (f is os) 
    => t i o b -> Free f is os b 
send t = Impure (inj t) (tsingleton (IKleisliTupled Pure)) 

-- Could not deduce `(Inject (Bar 'YesBar 'NoBar) f s30 s40)` 
prog 
    :: (Inject (File 'FileClosed 'FileOpen) (f s1 s2) 
    ,Inject (Foo 'NoFoo 'YesFoo) (f s2 s3) 
    ,Inject (Bar 'YesBar 'NoBar) (f s3 s4) 
    ,Inject (File 'FileOpen 'FileClosed) (f s4 s5)) 
    => Free f s1 s5() 
prog = do 
    send (Open "/tmp/foo.txt") 
    x <- send Read 
    send Foo 
    send (Write x) 
    send Bar 
    send Close 

-- * Attempt 2 

bsend :: (t i o b -> g is os b) -> t i o b -> Free g is os b 
bsend f t = Impure (f t) (tsingleton (IKleisliTupled Pure)) 

-- Straightforward but not very usable 

bprog 
    :: 
    Free 
     (File `SumP` Bar `SumP` Foo) 
     '('FileClosed, '('YesBar, 'NoFoo)) 
     '('FileClosed, '('NoBar, 'YesFoo)) 
    () 
bprog = do 
    bsend LP (Open "/tmp/foo.txt") 
    x <- bsend LP Read 
    bsend (RP . RP) Foo 
    bsend (RP . LP) Bar 
    bsend LP (Write x) 
    bsend LP Close 

-- * Attempt 3 

class Inject' f i o (fs :: j -> j -> * -> *) where 
    type I f i o fs :: j 
    type O f i o fs :: j 
    inj' :: forall x. f i o x -> fs (I f i o fs) (O f i o fs) x 

instance Inject' f i o f where 
    type I f i o f = i 
    type O f i o f = o 
    inj' = id 

-- Illegal polymorphic type: forall (s :: k1). '(il, s) 

instance Inject' fl il ol (SumP fl fr) where 
    type I fl il ol (SumP fl fr) = forall s. '(il, s) 
    type O fl il ol (SumP fl fr) = forall s. '(ol, s) 
    inj' = LP 

instance (Inject' f i' o' fr) => 
     Inject' f i' o' (SumP fl fr) where 
    type I f i' o' (SumP fl fr) = forall s. '(s, I f i' o' fr) 
    type O f i' o' (SumP fl fr) = forall s. '(s, O f i' o' fr) 
    inj' = RP . inj 

So Попытка 1 разрушает вывод типа. У попытки 2 есть плохая эргономика для пользователя. Попытка 3 кажется правильной, но я не могу понять, как сделать связанные экземпляры экземпляров работать. Как должна выглядеть эта инъекция?

ответ

6

Представьте сначала стандартный способ обработки открытых сумм. Я делаю это для простых неиндексированных функторов для простоты и потому, что построение одинаково для индексированных. Затем я представлю некоторые усовершенствования, включенные GHC 8.

Сначала мы определяем n-арные суммы функций как GADT, индексированные списком функторов. Это более удобно и чище, чем использование двоичных сумм.

{-# language 
    RebindableSyntax, TypeInType, TypeApplications, 
    AllowAmbiguousTypes, GADTs, TypeFamilies, ScopedTypeVariables, 
    UndecidableInstances, LambdaCase, EmptyCase, TypeOperators, ConstraintKinds, 
    FlexibleContexts, MultiParamTypeClasses, FlexibleInstances #-} 

import Data.Kind 

data NS :: [* -> *] -> * -> * where 
    Here :: f x -> NS (f ': fs) x 
    There :: NS fs x -> NS (f ': fs) x 

instance Functor (NS '[]) where 
    fmap _ = \case {} 

instance (Functor f, Functor (NS fs)) => Functor (NS (f ': fs)) where 
    fmap f (Here fx) = Here (fmap f fx) 
    fmap f (There ns) = There (fmap f ns) 

Проектирование и инъекционного можно либо сделать

  • непосредственно с классом, но это требует дублирования или бессвязные экземпляров.
  • Косвенно, сначала вычисляя индекс элемента, в который мы хотели бы ввести, а затем используя индекс (натуральный номер) для определения экземпляров класса без перекрытия.

Последнее решение является предпочтительным, так что давайте посмотрим, что:

data Nat = Z | S Nat 

type family Find (x :: a) (xs :: [a]) :: Nat where 
    Find x (x ': xs) = Z 
    Find x (y ': xs) = S (Find x xs) 

class Elem' (n :: Nat) (f :: * -> *) (fs :: [* -> *]) where 
    inj' :: forall x. f x -> NS fs x 
    prj' :: forall x. NS fs x -> Maybe (f x) 

instance (gs ~ (f ': gs')) => Elem' Z f gs where 
    inj'   = Here 
    prj' (Here fx) = Just fx 
    prj' _   = Nothing 

instance (Elem' n f gs', (gs ~ (g ': gs'))) => Elem' (S n) f gs where 
    inj'   = There . inj' @n 
    prj' (Here _) = Nothing 
    prj' (There ns) = prj' @n ns 

type Elem f fs = (Functor (NS fs), Elem' (Find f fs) f fs) 

inj :: forall fs f x. Elem f fs => f x -> NS fs x 
inj = inj' @(Find f fs) 

prj :: forall f x fs. Elem f fs => NS fs x -> Maybe (f x) 
prj = prj' @(Find f fs) 

Сейчас в GHCI:

> :t inj @[Maybe, []] (Just True) 
inj @[Maybe, []] (Just True) :: NS '[Maybe, []] Bool 

Однако наш Find тип семьи несколько проблематично, поскольку его сокращение часто блокируются переменными типа. GHC запрещает разветвление на неравенство переменных типа, потому что объединение может, возможно, впоследствии создавать разные переменные для равных типов (и принятие преждевременных решений, основанных на неравенстве, может привести к потере решений).

Например:

> :kind! Find Maybe [Maybe, []] 
Find Maybe [Maybe, []] :: Nat 
= 'Z -- this works 
> :kind! forall (a :: *)(b :: *). Find (Either b) [Either a, Either b] 
forall (a :: *)(b :: *). Find (Either b) [Either a, Either b] :: Nat 
= Find (Either b) '[Either a, Either b] -- this doesn't 

Во втором примере GHC не обязывает к неравенству a и b поэтому он не может переступить через первый элемент списка.

Это исторически вызвало довольно неприятные недостатки вывода типов в типах данных a la Carte и библиотеках расширяемых эффектов. Например, даже если у нас есть только один эффект State s в сумме функтора, запись (x :: n) <- get в контексте, где известны только Num n, результаты с ошибкой ввода типа, поскольку GHC не может вычислить индекс эффекта State, когда параметр состояния переменная типа.

Однако с GHC 8 мы можем написать значительно более мощное семейство типов Find, которое будет выглядеть в выражениях типов, чтобы увидеть, есть ли уникальный возможный индекс позиции.Например, если мы пытаемся найти эффект State s, если в списке эффектов есть только один State, мы можем безопасно вернуть его позицию, не глядя на параметр s, а затем GHC сможет объединить s с другим параметр состояния, содержащийся в списке.

Во-первых, нам нужен общий обход выражений типа:

import Data.Type.Bool 

data Entry = App | forall a. Con a 

type family (xs :: [a]) ++ (ys :: [a]) :: [a] where 
    '[]  ++ ys = ys 
    (x ': xs) ++ ys = x ': (xs ++ ys) 

type family Preord (x :: a) :: [Entry] where 
    Preord (f x) = App ': (Preord f ++ Preord x) 
    Preord x  = '[ Con x] 

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

> :kind! Preord (Maybe Int) 
Preord (Maybe Int) :: [Entry] 
= '['App, 'Con Maybe, 'Con Int] 
> :kind! Preord [Either String, Maybe] 
Preord [Either String, Maybe] :: [Entry] 
= '['App, 'App, 'Con (':), 'App, 'Con Either, 'App, 'Con [], 
    'Con Char, 'App, 'App, 'Con (':), 'Con Maybe, 'Con '[]] 

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

type family (x :: a) == (y :: b) :: Bool where 
    x == x = True 
    _ == _ = False 

type family PreordList (xs :: [a]) (i :: Nat) :: [(Nat, [Entry])] where 
    PreordList '[]  _ = '[] 
    PreordList (a ': as) i = '(i, Preord a) ': PreordList as (S i) 

type family Narrow (e :: Entry) (xs :: [(Nat, [Entry])]) :: [(Nat, [Entry])] where 
    Narrow _ '[]      = '[] 
    Narrow e ('(i, e' ': es) ': ess) = If (e == e') '[ '(i, es)] '[] ++ Narrow e ess 

type family Find_ (es :: [Entry]) (ess :: [(Nat, [Entry])]) :: Nat where 
    Find_ _  '[ '(i, _)] = i 
    Find_ (e ': es) ess  = Find_ es (Narrow e ess) 

type Find x ys = Find_ (Preord x) (PreordList ys Z) 

Теперь мы имеем:

> :kind! forall (a :: *)(b :: *). Find (Either a) [Maybe, [], Either b] 
forall (a :: *)(b :: *). Find (Either a) [Maybe, [], Either b] :: Nat 
= 'S ('S 'Z) 

Это Find может быть использован в любом коде с участием открытых сумм, и это работает для индексированных и Неиндексированных типов все же.

Here's Примерный код с видом инъекции/проекции, представленным выше, для неиндексированных растяжимых эффектов.

+1

Спасибо! Я знаком с вашим 'NS'. Проблема в том, что расширение его работы с индексированными функторами для меня не очевидно. 'data NS (fs :: [* -> *]) is os (a :: *)' Какие должны быть типы 'is' и' os' (если мы хотим поддерживать индексированные функторы, где каждый функтор может быть проиндексирован типами другого типа)? Уровень hlist типа? Список открытых профсоюзов? – JesseC

+0

Кстати, экземпляр 'Functor (NS fs)' можно напрямую передать в терминах утверждения, что все 'fs' являются функторами: http://lpaste.net/5782847892658061312 – dfeuer

+1

Не могли бы вы объяснить, как это помогает чтобы дать * обход * типа, а не только дерево, представляющее его, или сам тип? – dfeuer

1

Ага, я получил его для работы! Ключевая вещь, которую я взял у András Kovács, вторая попытка (связанная в комментарии) - это трюк с оставлением главы генерала экземпляра, а затем уточнение с ограничением равенства.

{-# LANGUAGE FlexibleInstances, GADTs, MultiParamTypeClasses, 
    RankNTypes, RebindableSyntax, TypeFamilies, TypeInType, 
    TypeOperators, UndecidableInstances #-} 

module Example2 (res, prog') where 

import Control.Monad.Indexed 
import Data.TASequence.FastCatQueue 
import Prelude hiding ((>>), (>>=)) 

-- * Indexed free machinery 

(>>=) 
    :: (IxMonad m) 
    => m s1 s2 a -> (a -> m s2 s3 b) -> m s1 s3 b 
(>>=) = (>>>=) 
(>>) 
    :: (IxApplicative m) 
    => m s1 s2 a -> m s2 s3 b -> m s1 s3 b 
f >> g = imap (const id) f `iap` g 

type family Fst x where 
    Fst '(a, b) = a 
type family Snd x where 
    Snd '(a, b) = b 

newtype IKleisliTupled m ia ob = IKleisliTupled 
    { runIKleisliTupled :: Snd ia -> m (Fst ia) (Fst ob) (Snd ob) 
    } 

tApp :: (TASequence s, IxMonad m) => s (IKleisliTupled m) x y -> (IKleisliTupled m) x y 
tApp fs = 
    case tviewl fs of 
    TAEmptyL -> IKleisliTupled ireturn 
    f :< fs' -> 
     IKleisliTupled 
     (\a -> runIKleisliTupled f a >>= runIKleisliTupled (tApp fs')) 

data Free f s1 s2 a where 
    Pure :: a -> Free f s s a 
    Impure :: 
    f s1 s2 a -> 
     FastTCQueue (IKleisliTupled (Free f)) '(s2, a) '(s3, b) -> 
     Free f s1 s3 b 

instance IxFunctor (Free f) where 
    imap f (Pure a) = Pure $ f a 
    imap f (Impure a g) = Impure a (g |> IKleisliTupled (Pure . f)) 
instance IxPointed (Free f) where 
    ireturn = Pure 
instance IxApplicative (Free f) where 
    iap (Pure f) (Pure a) = ireturn $ f a 
    iap (Pure f) (Impure a g) = Impure a (g |> IKleisliTupled (Pure . f)) 
    iap (Impure a f) m = Impure a (f |> IKleisliTupled (`imap` m)) 
instance IxMonad (Free f) where 
    ibind f (Pure a) = f a 
    ibind f (Impure a g) = Impure a (g |> IKleisliTupled f) 

-- * Example application 

data FileStatus 
    = FileOpen 
    | FileClosed 
data File i o a where 
    Open :: FilePath -> File 'FileClosed 'FileOpen() 
    Close :: File 'FileOpen 'FileClosed() 
    Read :: File 'FileOpen 'FileOpen String 
    Write :: String -> File 'FileOpen 'FileOpen() 

foldFile :: File i o a -> a 
foldFile (Open _) =() 
foldFile Close =() 
foldFile Read = "demo" 
foldFile (Write _) =() 

data MayFoo 
    = YesFoo 
    | NoFoo 
data Foo i o a where 
    Foo :: Foo 'NoFoo 'YesFoo() 

data MayBar 
    = YesBar 
    | NoBar 
data Bar i o a where 
    Bar :: Bar 'YesBar 'NoBar() 

-- * Coproduct of indexed functors 

infixr 5 `SumP` 
data SumP f1 f2 t1 t2 x where 
    LP :: f1 sl1 sl2 x -> SumP f1 f2 '(sl1, sr) '(sl2, sr) x 
    RP :: f2 sr1 sr2 x -> SumP f1 f2 '(sl, sr1) '(sl, sr2) x 

newtype VoidFunctor is os a = VoidFunctor (VoidFunctor is os a) 
absurd :: VoidFunctor is os a -> b 
absurd a = a `seq` spin a where 
    spin (VoidFunctor b) = spin b 

extract :: Free VoidFunctor '() '() a -> a 
extract (Pure a) = a 
extract (Impure f _) = absurd f 

runPure 
    :: (forall j p b. f j p b -> b) 
    -> Free (f `SumP` fs) '(i, is) '(o, os) a 
    -> Free fs is os a 
runPure _ (Pure a) = Pure a 
runPure f (Impure (RP cmd) q) = Impure cmd (tsingleton k) 
    where k = IKleisliTupled $ \a -> runPure f $ runIKleisliTupled (tApp q) a 
runPure f (Impure (LP cmd) q) = runPure f $ runIKleisliTupled (tApp q) (f cmd) 

-- * Injection 

class Inject l b where 
    inj :: forall a. l a -> b a 

instance Inject (f i o) (f i o) where 
    inj = id 

instance {-# OVERLAPPING #-} 
    (is ~ '(il, s), os ~ '(ol, s)) => 
    Inject (fl il ol) (SumP fl fr is os) where 
    inj = LP 

instance (Inject (f i' o') (fr is' os'), is ~ '(s, is'), os ~ '(s, os')) => 
     Inject (f i' o') (SumP fl fr is os) where 
    inj = RP . inj 

send 
    :: Inject (t i o) (f is os) 
    => t i o b -> Free f is os b 
send t = Impure (inj t) (tsingleton (IKleisliTupled Pure)) 

-- * In use 

prog = do 
    send (Open "/tmp/foo.txt") 
    x <- send Read 
    send Foo 
    send (Write x) 
    send Bar 
    send Close 
    ireturn x 

prog' :: 
    Free 
    (File `SumP` Foo `SumP` Bar `SumP` VoidFunctor) 
    '('FileClosed, '('NoFoo, '('YesBar, '()))) 
    '('FileClosed, '('YesFoo, '('NoBar, '()))) 
    String 
prog' = prog 

res :: String 
res = extract . runPure (\Bar ->()) . runPure (\Foo ->()) . runPure foldFile $ prog 

P.S. Я посмотрю, смогу ли я перейти к более красивой открытой кодировке или если я тоже столкнусь с непостижимыми проблемами GHC.

+0

Тем временем я нашел проблему с моим кодом, который был моей ошибкой, а не GHC (просто смешивая индексы в ограничениях). Теперь все работает отлично, [здесь код] (http://lpaste.net/347242). –

+0

@ AndrásKovács Ооо, интересно. Я, наконец, добрался до этого, и я столкнулся с неудобством. Если вы не хотите, чтобы профсоюз был «конкретным», что важно для композиционности, вам нужно сделать первый подход здесь: http://lpaste.net/357141 Кажется, что вы должны быть в состоянии сделать только вторую. Можете ли вы представить себе способ кодирования, чтобы вторая была возможна? – JesseC

+0

@ AndrásKovács Другая проблема, с которой я сталкиваюсь при попытке написать 'runFile'. Я могу заставить его работать, но только с помощью 'unsafeCoerce'. Есть ли способ избежать этого? http://lpaste.net/357146 – JesseC

Смежные вопросы