7

Is it possible to write a type-level function that returns True if one type-level list contains another type-level list?

Here is my attempt:

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module TypePlayground where

import Data.Type.Bool

type family InList (x :: *) (xs :: [*]) where
    InList x '[] = 'False
    InList x (x ': xs) = 'True
    InList x (a ': xs) = InList x xs
type family ListContainsList (xs :: [*]) (ys :: [*]) where
    ListContainsList xs (y ': ys) = InList y xs && ListContainsList xs ys
    ListContainsList xs '[] = 'True

It works for simple cases:

data A
data B
data C

test1 :: (ListContainsList '[A, B, C] '[C, A] ~ 'True) => ()
test1 = ()
-- compiles.
test2 :: (ListContainsList '[A, B, C] '[B, C, A] ~ 'True) => ()
test2 = ()
-- compiles.
test3 :: (ListContainsList (A ': B ': '[C]) (B ': A ': '[C]) ~ 'True) => ()
test3 = ()
-- compiles.
test4 :: (ListContainsList '[A, C] '[B, C, A] ~ 'True) => ()
test4 = ()
-- Couldn't match type ‘'False’ with ‘'True’

But what about cases like this?

test5 :: (ListContainsList (A ': B ': a) a ~ 'True) => ()
test5 = ()
-- Should compile, but fails:
-- Could not deduce (ListContainsList (A : B : a0) a0 ~ 'True)
-- from the context (ListContainsList (A : B : a) a ~ 'True)
  • I don't have time to flush out and test a full answer but how about using [Data.Type.List](https://hackage.haskell.org/package/type-list-0.5.0.0/docs/Data-Type-List.html) and seeing if the union of the first list with the second is the same as the first or second list (one contains the other). – jkeuhlen Jul 05 '16 at 21:28
  • I had a similar issue albeit a bit simpler (checking whether `h` is `g ++ g'` for some `g'`) and it took me a very long while before [getting something to work](https://github.com/gallais/potpourri/blob/master/haskell/stlc/Bidirectional.hs#L212-L213) even with open types. Using the same sort of tricks (and language extensions!) may get you in the right direction. – gallais Jul 06 '16 at 07:19
  • 1
    Hm, I dont think the haskell type checker can do this sort of stuff, even though it would be totally awesome... – Hoff Jul 06 '16 at 09:29

2 Answers2

7

The trouble is that you've defined your subset type family by induction on the structure of the contained list, but you're passing in a totally polymorphic (unknown) list whose structure is a mystery to GHC. You might think that GHC would be able to use induction anyway, but you'd be wrong. In particular, just as every type has undefined values, so every kind has "stuck" types. A notable example, which GHC uses internally and exports through (IIRC) GHC.Exts:

{-# LANGUAGE TypeFamilies, PolyKinds #-}

type family Any :: k

The Any type family is in every kind. So you could have a type-level list Int ': Char ': Any, where Any is used at kind [*]. But there's no way to deconstruct the Any into ': or []; it doesn't have any such sensible form. Since type families like Any exist, GHC cannot safely use induction on types the way you wish.

If you want induction to work properly over type lists, you really need to use singletons or similar as Benjamin Hodgson suggests. Rather than passing just the type-level list, you need to pass also a GADT giving evidence that the type-level list is constructed properly. Recursively destructing the GADT performs induction over the type-level list.

The same sorts of limitations hold for type-level natural numbers.

data Nat = Z | S Nat
type family (x :: Nat) :+ (y :: Nat) :: Nat where
   'Z :+ y = y
   ('S x) :+ y = 'S (x :+ y)

data Natty (n :: Nat) where
  Zy :: Natty 'Z
  Sy :: Natty n -> Natty ('S n)

You might wish to prove

associative :: p1 x -> p2 y -> p3 z -> ((x :+ y) :+ z) :~: (x :+ (y :+ z))

but you can't, because this requires induction on x and y. You can, however, prove

associative :: Natty x -> Natty y -> p3 z -> ((x :+ y) :+ z) :~: (x :+ (y :+ z))

with no trouble.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • What are `p1`, `p2` and `p3`? – Blaisorblade Jul 11 '16 at 18:09
  • 1
    @Blaisorblade, they're (polymorphic) proxies. – dfeuer Jul 11 '16 at 19:07
  • Thank you very much for your answer (also Benjamin's answer was very helpful too)! I'm new to dependently typed programming, and it definitely seems that I have to spend some time reading singletons-related papers to have a deeper understanding about how to approach this kind of problems. – Sergey Mitskevich Jul 12 '16 at 12:32
  • 2
    @SergeyMitskevich If you're new to dependent types, I would advise against using Haskell to learn. Haskell is not a dependently-typed language; tricks like singletons are merely hacks to _simulate_ dependent types. I'd advise picking up an actual dependently-typed system first, then learning about the Haskell encodings once you understand the concept. [_The Power of Pi_](https://cs.ru.nl/~wouters/Publications/ThePowerOfPi.pdf) is my personal favourite introduction – Benjamin Hodgson Jul 12 '16 at 12:48
  • 1
    @SergeyMitskevich, I second Benjamin Hodgson's advice that you should learn at least a bit of a dependantly typed language to get a better sense of these things. Learning them in Haskell is a bit like learning about data structures for the first time in C. – dfeuer Jul 12 '16 at 18:03
5

There seems to be an obsession with Boolean type families that's endemic to the Haskell community. Don't use them! You're making more work for yourself than necessary when it comes to using the result of such a test.

Subset-ness is a proposition that may be proved with information-rich evidence. Here's one simple way of designing such evidence. First, the type of proofs that an element can be found in a list:

data Elem xs x where
    Here :: Elem (x ': xs) x
    There :: Elem xs x -> Elem (y ': xs) x

Elem is structured like a natural number (compare There (There Here) with S (S Z)) but with more types. To prove that an element is in a list you give its index.

data All f xs where
    Nil :: All f '[]
    Cons :: f x -> All f xs -> All f (x ': xs)

All is a proof that a given predicate applies to every element of a list. It's structured as a list of proofs of f.

Now the type of proofs that a list is a subset of another list is easy to write down using these bits of machinery.

type IsSubset xs ys = All (Elem ys) xs

IsSubset is represented as a list of proofs that each element of xs can be found in ys.

You can automate proof search for IsSubset values by hacking the type class system, but that's another post.

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • 2
    Cool rant, but I doubt this helps the OP at all. – Roman Cheplyaka Jul 06 '16 at 12:03
  • 1
    Having seen this pattern in Agda, I agree with the suggestion. This does *not* solve the problem on its own. One needs next a lemma stating that any list is a sublist of itself, `reflSubset :: (xs :: [*]) -> IsSubset xs xs`, and you need to prove that by induction; I suspect this is easier to write using this encoding (where you can return `IsSubset`) than using the OP's version — what would `reflSubset` return there? Maybe a proof of equality, but that's cumbersome. The signature I wrote `reflSubset` requires dependent types, but I suspect singletons allow faking it. – Blaisorblade Jul 06 '16 at 12:56
  • thank you for the informed, albeit opinionated, advice – nicolas Jul 06 '16 at 14:52
  • How would you go about proving `NotElem`? My best approach would be `data Bottom`, `type NotElem xs x = Elem xs x -> Bottom` paired with `notInEmpty :: NotElem '[] x` and `notHere :: (x :~: y -> Bottom) -> NotElem xs x -> NotElem (y : xs) x` which should be implementable fairly easily. Do you have any better ideas? – WorldSEnder Aug 03 '18 at 20:31
  • @WorldSEnder Yep, that’d work. Structurally, `NotElem xs x` is a proof that there doesn’t exist a location in `xs` where you can find `x`. An alternative would be to give a list of proofs that each element in `xs` is not `x`, ie `All (Not . (:~: x)) xs`. As an exercise you can try witnessing an isomorphism between these forms (your `notInEmpty` and `notHere` functions demonstrate one direction) – Benjamin Hodgson Aug 06 '18 at 08:12