Creazione di una concatenazione completamente dipendente


10

Un bel fatto vero sulla concatenazione è che se conosco due variabili nell'equazione:

a ++ b = c

Allora conosco il terzo.

Vorrei catturare questa idea nel mio concat così uso una dipendenza funzionale.

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

Ora evoco un elenco eterogeneo in questo modo:

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

Ma quando provo a dichiararli perché Concatableho un 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)

Non soddisfo la mia terza dipendenza funzionale. O meglio, il compilatore ritiene che non lo facciamo. Questo perché il compilatore ritiene che nella nostra seconda istanza potrebbe essere così bs ~ (a ': cs). E potrebbe essere il caso se Concatable as (a ': cs) cs.

Come posso adattare le mie istanze in modo che tutte e tre le dipendenze siano soddisfatte?


1
Il problema principale sembra essere bs cs -> as, perché abbiamo bisogno di informazioni non locali su bse csper decidere se asdovrebbe essere un contro o uno zero. Dobbiamo scoprire come rappresentare queste informazioni; quale contesto aggiungeremmo a una firma di tipo per garantirlo quando non può essere dedotto direttamente?
Luqui,

3
Per espandere ciò che ha detto Luqui: immaginiamo di sapere bse cs, e vogliamo sfruttare il fondo, cioè vogliamo ricostruire as. Per farlo in modo deterministico, ci aspettiamo di essere in grado di impegnarci in una singola istanza e seguire quella ricetta. Concretamente, assumere bs = (Int ': bs2)e cs = (Int ': cs2). Quale istanza scegliamo? È possibile che tale Intnel csproviene bs(ed asè vuoto). È anche possibile che provenga da (non vuoto) ase che Intapparirà di nuovo in csseguito. Dobbiamo scavare più a fondo csper sapere e GHC non lo farà.
chi,

1
In parole povere, GHC accetterà fondi che possono essere provati usando una semplice forma di induzione dalle istanze. Qui, uno di loro richiede una sorta di doppia induzione per essere provato (o almeno così sembra), e il compilatore non andrà così lontano.
chi,

Risposte:


10

Quindi, come suggeriscono i commenti, GHC non lo capirà da solo, ma possiamo aiutarlo con un po 'di programmazione a livello di tipo. Presentiamo alcuni TypeFamilies. Tutte queste funzioni sono traduzioni piuttosto semplici della manipolazione di elenchi portate al livello del 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)

Con questi strumenti a nostra disposizione possiamo effettivamente raggiungere l'obiettivo dell'ora, ma prima definiamo una funzione con le proprietà desiderate:

  • Capacità di dedurre csda asebs
  • Capacità di dedurre asda bsecs
  • Capacità di dedurre bsda asecs

Ecco:

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

Proviamolo:

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 infine l'obiettivo finale:

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
Molto bene! Sospettavo persino che ciò potesse essere impossibile, ma tu l'hai risolto in modo trasparente ed elegante.
luqui,

Grazie, @luqui
lehins, il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.