The problem is that sameNat n (sing :: Sing 0)
gives you a usable n ~ 0
proof in the case that n is zero (when you pattern match on Just Refl
), but if n is not zero it just gives you Nothing
. That doesn't tell you anything at all about n
, so as far as the type checker is aware you can call exactly the same set of things inside the Nothing
branch as you could without calling sameNat
in the first place (in particular, you can't use sPred
because that requires that 1 <= n
).
So we need to pattern match on something that either provides evidence that n ~ 0
or provides evidence that 1 <= n
. Something like this:
data IsZero (n :: Nat)
where Zero :: (0 ~ n) => IsZero n
NonZero :: (1 <= n) => IsZero n
deriving instance Show (IsZero n)
Then we could write replicate''
this way:
isZero :: forall n. SNat n -> IsZero n
isZero n = _
replicate'' :: SNat n -> a -> Vect n a
replicate'' n x = case isZero n
of Zero -> Nil
NonZero -> x ::> replicate'' (sPred n) x
Of course that's just moved the problem to implementing the isZero
function, which hasn't really bought us anything, but I'm going to stick with it because it's handy to have this as the basis of any other inductive definitions you want to make using Nat
.
So, implementing isZero
. We could handle the zero case with sameNat
of course, but that doesn't help the non-zero case. The singletons package also provides Data.Singletons.Decide
, which gives you a way of getting a proof of equality or inequality of types based on their singletons. So we can do this:
isZero :: forall n. SNat n -> IsZero n
isZero n = case n %~ (SNat @0)
of Proved Refl -> Zero
Disproved nonsense -> NonZero
Sadly this doesn't work either! The Proved
case is fine (and the same as sameNat
giving us Just Refl
, basically). But the "proof of inequality" comes in the form of nonsense
being bound to a function of type (n :~: 0) -> Void
, and if we assume totality (without shenanigans) then the existence of such a function "proves" that we can't construct a n :~: 0
value, which proves that n
definitely isn't 0
. But this is just too far from a proof that 1 <= n
; we can see that if n
isn't 0 then it must be at least 1, from the properties of natural numbers, but GHC doesn't know this.
Another way to go would be to use singleton's Ord
support and pattern match on SNat @1 :%<= n
:
isZero :: forall n. SNat n -> IsZero n
isZero n = case (SNat @1) %:<= n
of STrue -> NonZero
SFalse -> Zero
But that doesn't work either, because the STrue
and SFalse
are just singletons for type level True
and False
, disconnected from the original comparison. We don't get a proof that 0 ~ n
or 1 <= n
from either side of this (and similarly can't get it to work by comparing with SNat @0
either). This is type-checker boolean blindness, basically.
Ultimately I was never able to satisfactorily solve this in my code. As far as I can tell we're missing a primitive; we either need to be able to compare singletons in a way that gives us <
or <=
constraints on the corresponding types, or we need a switch on whether a Nat
is zero or nonzero.
So I cheated:
isZero :: forall n. SNat n -> IsZero n
isZero n = case n %~ (SNat @0)
of Proved Refl -> Zero
Disproved _ -> unsafeCoerce (NonZero @1)
Since NonZero
only contains evidence that n
is 1 or more, but not any other information about n
, you can just unsafely coerce a proof that 1 is 1 or more.
Here's a full working example:
{-# LANGUAGE DataKinds
, GADTs
, KindSignatures
, ScopedTypeVariables
, StandaloneDeriving
, TypeApplications
, TypeOperators
#-}
import GHC.TypeLits ( type (<=), type (-) )
import Data.Singletons.TypeLits ( Sing (SNat), SNat, Nat )
import Data.Singletons.Prelude.Enum ( sPred )
import Data.Singletons.Decide ( SDecide ((%~))
, Decision (Proved, Disproved)
, (:~:) (Refl)
)
import Unsafe.Coerce ( unsafeCoerce )
data IsZero (n :: Nat)
where Zero :: (0 ~ n) => IsZero n
NonZero :: (1 <= n) => IsZero n
deriving instance Show (IsZero n)
isZero :: forall n. SNat n -> IsZero n
isZero n = case n %~ (SNat @0)
of Proved Refl -> Zero
Disproved _ -> unsafeCoerce (NonZero @1)
data Vect (n :: Nat) a
where Nil :: Vect 0 a
(::>) :: a -> Vect (n - 1) a -> Vect n a
deriving instance Show a => Show (Vect n a)
replicate'' :: SNat n -> a -> Vect n a
replicate'' n x = case isZero n
of Zero -> Nil
NonZero -> x ::> replicate'' (sPred n) x
head'' :: (1 <= n) => Vect n a -> a
head'' (x ::> _) = x
main :: IO ()
main = putStrLn
. (:[])
. head''
$ replicate''
(SNat @1000000000000000000000000000000000000000000000000000000)
'\x1f60e'
Note that unlike K. A. Buhr's suggested approach using unsafeCoerce
, here the code for replicate is actually using the type checker to verify that it constructs a Vect n a
in accordance to the SNat n
provided, whereas their suggestion requires you to trust that the code does this (the actual meat of the work is done by iterate
counting on Int
) and only makes sure that the callers use the SNat n
and the Vect n a
consistently. The only bit of code you have to just trust (unchecked by the compiler) is that a Refuted _ :: Decision (n :~: 0)
really does imply 1 <= n
, inside isZero
(which you can reuse to write lots of other functions that need to switch on whether a SNat
is zero or not).
As you try to implement more functionality with your Vect
, you'll find that a lot of "obvious" things GHC doesn't know about the properties of Nat
are quite painful. Data.Constraint.Nat
from the constraints
package has a lot of useful proofs you can use (for example, if you try to implement drop :: (k <= n) => SNat k -> Vect n a -> Vect (n - k) a
, you'll probably end up needing leTrans
so that when you know that 1 <= k
then also 1 <= n
and you can actually pattern match to strip off another element). Avoiding this kind of hasochism is where K. A. Buhr's approach can be a great help, if you want to just implement your operation with code you trust and unsafeCoerce the types to line up.