6

Consider this definition of zip for the usual vectors length indexed by Peano numerals:

{-# language DataKinds          #-}
{-# language KindSignatures     #-}
{-# language GADTs              #-}
{-# language TypeOperators      #-}
{-# language StandaloneDeriving #-}
{-# language FlexibleInstances  #-}
{-# language FlexibleContexts   #-}

module Vector
  where

import Prelude hiding (zip)

data N
  where
    Z :: N
    S :: N -> N

data Vector (n :: N) a
  where
    VZ :: Vector Z a
    (:::) :: a -> Vector n a -> Vector (S n) a

infixr 1 :::

deriving instance Show a => Show (Vector n a)

class Zip z
  where
    zip :: z a -> z b -> z (a, b)

instance Zip (Vector n) => Zip (Vector (S n))
  where
    zip (x ::: xs) (y ::: ys) = (x, y) ::: zip xs ys

instance Zip (Vector Z)
  where
    zip _ _ = VZ

-- ^
-- λ :t zip (1 ::: 2 ::: 3 ::: VZ) (4 ::: 5 ::: 6 ::: VZ)
-- zip (1 ::: 2 ::: 3 ::: VZ) (4 ::: 5 ::: 6 ::: VZ)
--   :: (Num a, Num b) => Vector ('S ('S ('S 'Z))) (a, b)
-- λ zip (1 ::: 2 ::: 3 ::: VZ) (4 ::: 5 ::: 6 ::: VZ)
-- (1,4) ::: ((2,5) ::: ((3,6) ::: VZ))

Typing in unary numbers is wearysome (even though I have a macro for that). Fortunately, there is GHC.TypeLits. Let us use it:

module Vector
  where

import Prelude hiding (zip)
import GHC.TypeLits

data Vector (n :: Nat) a
  where
    VZ :: Vector 0 a
    (:::) :: a -> Vector n a -> Vector (n + 1) a

infixr 1 :::

deriving instance Show a => Show (Vector n a)

class Zip z
  where
    zip :: z a -> z b -> z (a, b)

instance Zip (Vector n) => Zip (Vector (n + 1))
  where
    zip (x ::: xs) (y ::: ys) = (x, y) ::: zip xs ys

instance Zip (Vector 0)
  where
    zip _ _ = VZ

— But no:

    • Illegal type synonym family application in instance:
        Vector (n + 1)
    • In the instance declaration for ‘Zip (Vector (n + 1))’
   |
28 | instance Zip (Vector n) => Zip (Vector (n + 1))
   |                            ^^^^^^^^^^^^^^^^^^^^

So I replace the class with an ordinary function:

zip :: Vector n a -> Vector n b -> Vector n (a, b)
zip (x ::: xs) (y ::: ys) = (x, y) ::: zip xs ys
zip VZ VZ = VZ

— But now I cannot make use of inductive reasoning anymore:

Vector.hs:25:47: error:
    • Could not deduce: n2 ~ n1
      from the context: n ~ (n1 + 1)
        bound by a pattern with constructor:
                   ::: :: forall a (n :: Nat). a -> Vector n a -> Vector (n + 1) a,
                 in an equation for ‘zip’
        at Vector.hs:25:6-13
      or from: n ~ (n2 + 1)
        bound by a pattern with constructor:
                   ::: :: forall a (n :: Nat). a -> Vector n a -> Vector (n + 1) a,
                 in an equation for ‘zip’
        at Vector.hs:25:17-24
      ‘n2’ is a rigid type variable bound by
        a pattern with constructor:
          ::: :: forall a (n :: Nat). a -> Vector n a -> Vector (n + 1) a,
        in an equation for ‘zip’
        at Vector.hs:25:17-24
      ‘n1’ is a rigid type variable bound by
        a pattern with constructor:
          ::: :: forall a (n :: Nat). a -> Vector n a -> Vector (n + 1) a,
        in an equation for ‘zip’
        at Vector.hs:25:6-13
      Expected type: Vector n1 b
        Actual type: Vector n2 b
    • In the second argument of ‘zip’, namely ‘ys’
      In the second argument of ‘(:::)’, namely ‘zip xs ys’
      In the expression: (x, y) ::: zip xs ys
    • Relevant bindings include
        ys :: Vector n2 b (bound at Vector.hs:25:23)
        xs :: Vector n1 a (bound at Vector.hs:25:12)
   |
25 | zip (x ::: xs) (y ::: ys) = (x, y) ::: zip xs ys
   |                                               ^^

Am I failing to observe something obvious? These TypeLits cannot be useless?.. How is it supposed to work?

Ignat Insarov
  • 4,660
  • 18
  • 37
  • 1
    "These TypeLits cannot be useless?..." - you're currently experiencing Stage 1, Denial – Benjamin Hodgson Aug 19 '18 at 13:25
  • @BenjaminHodgson, they're certainly limited, but I wouldn't go as far as to call them *useless*. One thing they're good for is APIs. `type family ToRealNat (n :: TL.Nat) :: Nat where {ToRealNat 0 = 'Z; ToRealNat n = 'S (ToRealNat (n-1))}`. You can also go the other way. That provides a much nicer interface than making users type in their Peano numerals by hand. – dfeuer Aug 19 '18 at 18:30
  • @dfeuer How safe is this? It is evident that, for n < 0, the function is undefined. I gave it a try in repl, and it appears that the type synonym would simply be left unresolved. I wonder if that is the defined behaviour, rather than compiler stack overflow. And whether there is a way to bake in the `n >= 0` restriction. – Ignat Insarov Aug 22 '18 at 11:32
  • @dfeuer And how is that supposed to work? A definition like `type V (n :: TL.Nat) = Vector (ToRealNat n); zip' :: V n a -> V n b -> V n (a, b); zip' = zip` typechecks, but its call site does not, even with type annotations. _(Unless the vector is trivial, that is, `VZ`)_. – Ignat Insarov Aug 22 '18 at 12:12
  • @IgnatInsarov, you probably want something like `class (n' ~ ToRealNat n, n ~ FromRealNat n') => ToRealNatC n n'; instance (n' ~ ToRealNat n, n ~ FromRealNat n') => ToRealNatC n n'; zip' :: ToRealNatC n n' => Vector n' a -> Vector n' b -> Vector n' (a, b)`. That will give you much better inference. – dfeuer Aug 22 '18 at 15:18

1 Answers1

6

There is no induction on TypeLits, which by default does make them nearly useless, but you can improve the situation in two ways.

Use ghc-typelits-natnormalise. It's a GHC plugin which adds an arithmetic solver to the type checker, and causes GHC to consider many equal Nat expressions equal. This is very convenient and is compatible with the next solution. Your zip works with this out of the box.

Postulate whatever properties you need. You should only postulate proofs of true statements, and only proofs of equalities or other computationally irrelevant data types, in order to avoid potential memory safety issues. For example, your zip works the following way:

{-# language
    RankNTypes, TypeApplications, TypeOperators,
    GADTs, TypeInType, ScopedTypeVariables #-}

import GHC.TypeLits
import Data.Type.Equality
import Unsafe.Coerce

data Vector (n :: Nat) a
  where
    VZ :: Vector 0 a
    (:::) :: a -> Vector n a -> Vector (n + 1) a

lemma :: forall n m k. (n :~: (m + 1)) -> (n :~: (k + 1)) -> m :~: k
lemma _ _ = unsafeCoerce (Refl @n)

vzip :: Vector n a -> Vector n b -> Vector n (a, b)
vzip VZ VZ = VZ
vzip ((a ::: (as :: Vector m a)) :: Vector n a)
     ((b ::: (bs :: Vector k b)) :: Vector n b) =
  case lemma @n @m @k Refl Refl of
    Refl -> (a, b) ::: vzip as bs
András Kovács
  • 29,931
  • 3
  • 53
  • 99
  • By chance you know the reasoning behind _not_ providing induction on `Nat` in GHC/base by default? For instance, is it hard, but planned, or is it dangerous and forbidden? – Ignat Insarov Aug 19 '18 at 14:11
  • No particular reason. My impression is that `TypeLits` was initially intended as no more than a second-class syntactic sugar. While `Nat` is now fairly good with the plugin, `Symbol` is still just a black-box with not many operations, and is mainly used to abstract over record field names and such. – András Kovács Aug 19 '18 at 14:34
  • 2
    You can even postulate an induction principle: `indNat :: p 0 -> (forall k. p k -> p (k+1)) -> SNat n -> p n`. Each such principle has two variants: a constant-time version when `p` only has one constructor (e.g., equality evidence) and a version that actually builds up the result value step by step. – dfeuer Aug 19 '18 at 18:15
  • 1
    (The constant-time version is dangerous, of course, since the passed function may not be total.) – dfeuer Aug 19 '18 at 18:25
  • @dfeuer I think this would be very useful as a library. – Asad Saeeduddin Aug 18 '21 at 16:35
  • Apparently it already does exist as a library https://hackage.haskell.org/package/singleton-typelits – Asad Saeeduddin Aug 19 '21 at 23:17