10

I am experimenting with depedent types in Haskell and came across the following in the paper of the 'singletons' package:

replicate2 :: forall n a. SingI n => a -> Vec a n
replicate2 a = case (sing :: Sing n) of
  SZero -> VNil
  SSucc _ -> VCons a (replicate2 a)

So I tried to implement this myself, just toget a feel of how it works:

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE TypeOperators       #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}

import           Data.Singletons
import           Data.Singletons.Prelude
import           Data.Singletons.TypeLits

data V :: Nat -> * -> * where
  Nil  :: V 0 a
  (:>) :: a -> V n a -> V (n :+ 1) a

infixr 5 :>

replicateV :: SingI n => a -> V n a
replicateV = replicateV' sing
  where replicateV' :: Sing n -> a -> V n a
        replicateV' sn a = case sn of
            SNat -> undefined -- what can I do with this?

Now the problem is that the Sing instance for Nat does not have SZero or SSucc. There is only one constructor called SNat.

> :info Sing
data instance Sing n where
  SNat :: KnownNat n => Sing n

This is different than other singletons that allow matching, such as STrue and SFalse, such as in the following (useless) example:

data Foo :: Bool -> * -> * where
  T :: a -> Foo True a
  F :: a -> Foo False a

foo :: forall a b. SingI b => a -> Foo b a
foo a = case (sing :: Sing b) of
  STrue -> T a
  SFalse -> F a

You can use fromSing to get a base type, but this of course does allow GHC to check the type of the output vector:

-- does not typecheck
replicateV2 :: SingI n => a -> V n a
replicateV2 = replicateV' sing
  where replicateV' :: Sing n -> a -> V n a
        replicateV' sn a = case fromSing sn of
              0 -> Nil
              n -> a :> replicateV2 a

So my question: how to implement replicateV?

EDIT

The answer given by erisco explains why my approach of deconstructing an SNat does not work. But even with the type-natural library, I am unable to implement replicateV for the V data type using GHC's build-in Nat types.

For example the following code compiles:

replicateV :: SingI n => a -> V n a
replicateV = replicateV' sing
  where replicateV' :: Sing n -> a -> V n a
        replicateV' sn a = case TN.sToPeano sn of
            TN.SZ       -> undefined
            (TN.SS sn') -> undefined

But this does not seem to give enough information to the compiler to infer whether n is 0 or not. For example the following gives a compiler error:

replicateV :: SingI n => a -> V n a
replicateV = replicateV' sing
  where replicateV' :: Sing n -> a -> V n a
        replicateV' sn a = case TN.sToPeano sn of
            TN.SZ       -> Nil
            (TN.SS sn') -> undefined

This gives the following error:

src/Vec.hs:25:28: error:
    • Could not deduce: n1 ~ 0
      from the context: TN.ToPeano n1 ~ 'TN.Z
        bound by a pattern with constructor:
                   TN.SZ :: forall (z0 :: TN.Nat). z0 ~ 'TN.Z => Sing z0,
                 in a case alternative
        at src/Vec.hs:25:13-17
      ‘n1’ is a rigid type variable bound by
        the type signature for:
          replicateV' :: forall (n1 :: Nat) a1. Sing n1 -> a1 -> V n1 a1
        at src/Vec.hs:23:24
      Expected type: V n1 a1
        Actual type: V 0 a1
    • In the expression: Nil
      In a case alternative: TN.SZ -> Nil
      In the expression:
        case TN.sToPeano sn of {
          TN.SZ -> Nil
          (TN.SS sn') -> undefined }
    • Relevant bindings include
        sn :: Sing n1 (bound at src/Vec.hs:24:21)
        replicateV' :: Sing n1 -> a1 -> V n1 a1 (bound at src/Vec.hs:24:9)

So, my original problem still remains, I am still unable to do anything usefull with the SNat.

Sam De Meyer
  • 2,031
  • 1
  • 25
  • 32
  • This brings out everything I hate about GHC's built-in `Nat` type. The impossibility of proving things like `(n + 1) - 1 ~ n`, as well as the awkwardness around checking if `n ~ 0`. `replicateV2` is a fundamentally recursive operation for which you need induction over the vector length. Without having an inductive definition for `Nat` you go nowhere. Let me make this explicit: any solution to your problem will _have_ to use something that bypasses the type system (either via a plugin or `unsafeCoerce`). On the other hand, you can do everything safely and easily with `data Nat = Z | S Nat`. – Alec Sep 29 '17 at 07:55
  • This is what I was starting to fear for, that there actually is no solution to this problem. Yet I was hoping there was some common workaround that I did not know of. I'll wait until the bounty is over hoping for a miracle. But if not, then I guess your comment answers my question, and I'll resort to using `unsafeCoerce`. – Sam De Meyer Sep 29 '17 at 14:02
  • The reason I am so keen on using the built-in `Nat` is that some of the libraries I am using also use the built-in `Nat` types. Especially `Numeric.LinearAlgebra.Static` from `hmatrix`. I am constantly encountering problems with proofs involving `Nat`s when trying even the simplest things such as iterating over matrix rows, etc. – Sam De Meyer Sep 29 '17 at 14:09
  • Even then, you'll probably want to try `unsafeCoerce`ing your way through some minimal set of arithmetic axioms. I suggest starting from . – Alec Sep 29 '17 at 14:59
  • Hey, your links does not seem to work :) – Sam De Meyer Sep 29 '17 at 16:14
  • Sorry. I think SO may not support angle-bracketed links in comments. [Here](https://hackage.haskell.org/package/constraints-0.9.1/docs/Data-Constraint-Nat.html) is the link. – Alec Sep 29 '17 at 16:16
  • Hmm, looks interesting. I'm not immediately sure how to use this package, but I'll take a better look at it this weekend as I am working on something else now. If you believe this package can be used to actually implement `replicateV`, then I'd be much obliged to see how it works out (and I'd give you due credit for it). If not I'll see what I can come up with this weekend. – Sam De Meyer Sep 29 '17 at 16:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155651/discussion-between-sam-de-meyer-and-alec). – Sam De Meyer Sep 30 '17 at 09:38

2 Answers2

4

There are two notions of naturals at play here. One is "literal naturals" (i.e. 0, 1, 2, and so on) and the other is "Peano naturals" (i.e. Z, S Z, S (S Z), and so on). The one the paper is using is clearly Peano naturals but the one singletons uses is literal naturals.

Thankfully there is another package called type-natural which defines Peano naturals as well as conversion to literal naturals and conversion from literal naturals.

erisco
  • 14,154
  • 2
  • 40
  • 45
1

From the comments, I'm worried I must be missing something terrifically obvious, but here's my take on it. The whole point of:

replicate2 :: forall n a. SingI n => a -> Vec a n
replicate2 a = case (sing :: Sing n) of
  SZero -> VNil
  SSucc _ -> VCons a (replicate2 a)

is that, in order to return VNil :: Vec a 0 when the function has general return type Vec a n, you need to specialize the n to 0, and pattern-matching on GADTs provides a way to do this, as long as you have a constructor, like SZero, that implies n ~ 0.

Now the SNats in the singleton package have no such constructor. The only way to get one, as far as I can see, is to build a whole new singleton type for naturals and implement the necessary type families. Maybe you can do it in a way that wraps the Nats, so you're closer to SZero :: Sing (SN 0), SNonZero :: Sing (SN n) than a Peano construction, but I don't know.

Of course, there's another way to specialize a function that returns Vec a n to return Vec a 0, namely type classes.

If you are willing to abandon some of the explicit singleton machinery and switch to type classes (and also allow overlapping and undecidable instances), the following seems to work. I had to slightly modify the definition of V to use n :- 1 instead of n :+ 1, but I don't think that poses a problem.

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE ScopedTypeVariables   #-}

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE OverlappingInstances  #-}
{-# LANGUAGE UndecidableInstances  #-}

import           Data.Singletons
import           Data.Singletons.Prelude
import           Data.Singletons.TypeLits

data V :: Nat -> * -> * where
  Nil  :: V 0 a
  (:>) :: a -> V (n :- 1) a -> V n a

infixr 5 :>

class VC n a where
  replicateV :: a -> V n a

instance VC 0 a where
  replicateV _ = Nil

instance VC (n :- 1) a => VC n a where
  replicateV x = x :> replicateV x

instance (Show a) => Show (V n a) where
  show Nil = "Nil"
  show (x :> v) = show x ++ " :> " ++ show v

headV :: V (n :+ 1) a -> a
headV (x :> _) = x

tailV :: ((n :+ 1) :- 1) ~ n => V (n :+ 1) a -> V n a
tailV (_ :> v) = v

main = do print (replicateV False   :: V 0 Bool)
          print (replicateV 1       :: V 1 Int)
          print (replicateV "Three" :: V 3 String)
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • It seems to be me who has missed something terribly obvious---that is, using typeclasses. In fact your answer is so simple that I am embarrassed it didn't cross my mind. On first sight it seems that this workaround can be used wherever you would normally try to use a pattern match on an `SNat` (if it were defined inductively). Having to use an extra typeclass is a bit ugly but it solves the practical problems I had. I guess after staring at it for too long I developed some kind of tunnel vision. Anyway, unless a better solution pops up in the coming days I'll happily give you the bounty. – Sam De Meyer Sep 29 '17 at 22:39