Создание полностью зависимой конкатенации

10

Хороший факт о конкатенации заключается в том, что если я знаю какие-либо две переменные в уравнении:

a ++ b = c

Тогда я знаю третий.

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

{-# Language DataKinds, GADTs, FlexibleContexts, FlexibleInstances, FunctionalDependencies, KindSignatures, PolyKinds, TypeOperators, UndecidableInstances #-}
import Data.Kind (Type)

class Concatable
   (m  :: k -> Type)
   (as :: k)
   (bs :: k)
   (cs :: k)
   | as bs -> cs
   , as cs -> bs
   , bs cs -> as
   where
     concat' :: m as -> m bs -> m cs

Теперь я заклинаю разнородный список примерно так:

data HList ( as :: [ Type ] ) where
  HEmpty :: HList '[]
  HCons  :: a -> HList as -> HList (a ': as)

Но когда я пытаюсь объявить это как Concatableпроблему

instance Concatable HList '[] bs bs where
  concat' HEmpty bs = bs
instance
  ( Concatable HList as bs cs
  )
    => Concatable HList (a ': as) bs (a ': cs)
  where
    concat' (HCons head tail) bs = HCons head (concat' tail bs)

Я не удовлетворяю свою третью функциональную зависимость. Вернее, компилятор считает, что нет. Это потому, что компилятор считает, что в нашем втором случае это может быть так bs ~ (a ': cs). И это может быть в случае, если Concatable as (a ': cs) cs.

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

Сриотчизм О'Зайк
источник
1
Ключевая проблема, похоже, заключается в том bs cs -> as, что нам нужна нелокальная информация о bsи csрешить, asдолжны ли быть минусы или ноль. Нам нужно выяснить, как представлять эту информацию; какой контекст мы добавим к сигнатуре типа, чтобы гарантировать ее, когда она не может быть выведена напрямую?
Луки
3
Чтобы расширить то, что сказал Луки: представьте, что мы знаем bsи cs, и мы хотим использовать фундеп, то есть мы хотим реконструировать as. Чтобы сделать это детерминированным способом, мы ожидаем, что сможем зафиксировать один экземпляр и следовать этому рецепту. Конкретно предположим bs = (Int ': bs2)и cs = (Int ': cs2). Какой экземпляр мы выбираем? Вполне возможно , что такое Intв csисходит от bsasпуста). Также возможно, что asвместо этого есть (непустое) , и это Intпоявится снова csпозже. Нам нужно копнуть глубже, csчтобы знать, и GHC не будет этого делать.
Чи
1
Грубо говоря, GHC будет принимать fundeps, которые можно доказать, используя простую форму индукции из примеров. Здесь, один из них требует доказательства двойной индукции (или так кажется), и компилятор не пойдет так далеко.
Чи

Ответы:

10

Так что, как показывают комментарии, GHC не собирается разбираться в этом самостоятельно, но мы можем помочь с небольшим программированием на уровне типов. Давайте представим некоторые TypeFamilies. Все эти функции - довольно простые переводы манипулирования списком, поднятые до уровня типа:

-- | This will produce the suffix of `cs` without `as`
type family DropPrefix (as :: [Type]) (cs :: [Type]) where
  DropPrefix '[] cs = cs
  DropPrefix (a ': as) (a ': cs) = DropPrefix as cs

-- Similar to the logic in the question itself: list concatenation. 
type family Concat (as :: [Type]) (bs :: [Type]) where
  Concat '[] bs = bs
  Concat (head ': tail) bs = head ': Concat tail bs

-- | Naive list reversal with help of concatenation.
type family Reverse (xs :: [Type]) where
  Reverse '[] = '[]
  Reverse (x ': xs) = Concat (Reverse xs) '[x]

-- | This will produce the prefix of `cs` without `bs`
type family DropSuffix (bs :: [Type]) (cs :: [Type]) where
  DropSuffix bs cs = Reverse (DropPrefix (Reverse bs) (Reverse cs))

-- | Same definition of `HList` as in the question
data HList (as :: [Type]) where
  HEmpty :: HList '[]
  HCons :: a -> HList as -> HList (a ': as)

-- | Definition of concatenation at the value level
concatHList :: (cs ~ Concat as bs) => HList as -> HList bs -> HList cs
concatHList HEmpty bs = bs
concatHList (HCons head tail) bs = HCons head (concatHList tail bs)

Имея в своем распоряжении эти инструменты, мы на самом деле можем достичь цели на час, но сначала давайте определим функцию с желаемыми свойствами:

  • Способность выводить csиз asиbs
  • Способность выводить asиз bsиcs
  • Способность выводить bsиз asиcs

Вуаля:

concatH ::
     (cs ~ Concat as bs, bs ~ DropPrefix as cs, as ~ DropSuffix bs cs)
  => HList as
  -> HList bs
  -> HList cs
concatH = concatHList

Давайте проверим это:

foo :: HList '[Char, Bool]
foo = HCons 'a' (HCons True HEmpty)

bar :: HList '[Int]
bar = HCons (1 :: Int) HEmpty
λ> :t concatH foo bar
concatH foo bar :: HList '[Char, Bool, Int]
λ> :t concatH bar foo
concatH bar foo :: HList '[Int, Char, Bool]

И, наконец, конечная цель:

class Concatable (m :: k -> Type) (as :: k) (bs :: k) (cs :: k)
  | as bs -> cs
  , as cs -> bs
  , bs cs -> as
  where
  concat' :: m as -> m bs -> m cs

instance (cs ~ Concat as bs, bs ~ DropPrefix as cs, as ~ DropSuffix bs cs) =>
         Concatable HList as bs cs where
  concat' = concatH
λ> :t concat' HEmpty bar
concat' HEmpty bar :: HList '[Int]
λ> :t concat' foo bar
concat' foo bar :: HList '[Char, Bool, Int]
λ> :t concat' bar foo
concat' bar foo :: HList '[Int, Char, Bool]
lehins
источник
3
Отлично сработано! Я даже подозревал, что это может быть невозможно, но вы решили это прозрачно и элегантно.
Luqi
Спасибо, @luqui
lehins