Criando uma concatenação completamente dependente


10

Um bom fato verdadeiro sobre concatenação é que, se eu conhecer duas variáveis ​​na equação:

a ++ b = c

Então eu conheço o terceiro.

Gostaria de capturar essa ideia em minha própria concat, para usar uma dependência funcional.

{-# 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

Agora, conjuro uma lista heterogênea da seguinte forma:

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

Mas quando tento declarar isso, Concatabletenho um problema

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)

Não satisfiz minha terceira dependência funcional. Ou melhor, o compilador acredita que não. Isso ocorre porque o compilador acredita que, em nossa segunda instância, pode ser o caso bs ~ (a ': cs). E poderia ser o caso se Concatable as (a ': cs) cs.

Como posso ajustar minhas instâncias para que todas as três dependências sejam atendidas?


11
O principal problema parece ser bs cs -> as, porque precisamos de informações não locais sobre bse csdecidir se asdeve ser um contra ou um nada. Precisamos descobrir como representar essa informação; que contexto adicionaríamos a uma assinatura de tipo para garantir quando não puder ser deduzida diretamente?
luqui 27/12/19

3
Para expandir o que luqui disse: imagine que sabemos bse cs, e queremos explorar o fundep, ou seja, queremos reconstruir as. Para fazer isso de maneira determinística, esperamos poder comprometer-se com uma única instância e seguir essa receita. Concretamente, assuma bs = (Int ': bs2)e cs = (Int ': cs2). Qual instância escolhemos? É possível que tais Intem csvem de bs(e asestá vazio). Também é possível que venha de (não vazio) ase que Intapareça novamente csmais tarde. Precisamos nos aprofundar cspara saber e GHC não fará isso.
chi

11
Em termos gerais, o GHC aceitará fundeps que podem ser provados usando uma forma simples de indução das instâncias. Aqui, um deles exige que seja provado um tipo de dupla indução (ou ao que parece), e o compilador não vai tão longe.
chi

Respostas:


10

Então, como sugerem os comentários, o GHC não vai descobrir por si só, mas podemos ajudá-lo com um pouco de programação em nível de tipo. Vamos apresentar alguns TypeFamilies. Todas essas funções são traduções bastante diretas da manipulação de lista levantadas para o nível de tipo:

-- | 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)

Com essas ferramentas à nossa disposição, podemos chegar ao objetivo da hora, mas primeiro vamos definir uma função com as propriedades desejadas:

  • Capacidade de deduzir csa partir asebs
  • Capacidade de deduzir asa partir bsecs
  • Capacidade de deduzir bsa partir asecs

Voila:

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

Vamos testá-lo:

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]

E, finalmente, o objetivo final:

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]

3
Bem feito! Eu até suspeitava que isso fosse impossível, mas você o resolveu de forma transparente e elegante.
luqui 27/12/19

Obrigado, @luqui
lehins
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.