I have just learnt about the DataKinds
extension, type-level literals, and found out that type-levels natural numbers can be compared using constraints provided in the Data.Type.Ord
such as (>)
, and manipulated using type-level operations provided in GHC.TypeNats
such as (+)
.
With these in mind, I came across the idea of implementing a safe version of !!
for length-indexed vectors. The idea is simple: you pass in an ix :: Natural
as the index, and a vec :: Vec len a
with length len
and type a
, and you get the element at index ix
. The compiler should detect and shout at you for any illegal ix
(ix < 0 || ix >= len
) or vec
(whose len == 0
).
So I began by defining the type signature for such safeAt
(imports and magic comments of extensions ignored):
infixr 5 :.
data Vec :: Nat -> * -> * where
Nil :: Vec 0 a
(:.) :: a -> Vec m a -> Vec (m + 1) a
safeAt :: forall (ix :: Nat) (len :: Nat) (proxy :: Nat -> *) a.
(KnownNat ix, {- ix >= 0, -} len >= 1, ix < len) =>
proxy ix -> Vec len a -> a
safeAt = undefined
Working as intended:
v :: Vec 10 Int
v = 1 :. 2 :. 3 :. 4 :. 5 :. 6 :. 7 :. 8 :. 9 :. 10 :. Nil
ok :: Int
ok = safeAt (Proxy :: Proxy 3) v
oops :: Int
oops = safeAt (Proxy :: Proxy 10) v -- expected compilation error
Here I made use of a proxy to hold the ix
at the type-level so it can be checked by the type-level constraints. This however became a problem when I try to implement safeAt
:
safeAt p (x:.xs) = case natVal p of
0 -> x
n -> safeAt (??? (n - 1)) xs
I need to promote (n - 1)
to Proxy :: Proxy (n - 1)
. I cannot work out such function, not even writing out its type signature, and came to doubt if such a type-dependent function would even exist in the Haskell's typing system. I know you can pull down a Nat from type-level to term-level using natVal
, but is there a "reverse" of this? Maybe there's a magic function out there that I don't know? Also I believe there are other problems in this implementation, such as how would you comfort the compiler that (ix - 1)
is KnownNat
and (len - 1) >= 1
still given the proxy p
is not holding a 0
. Maybe there are some alternative approaches out there to implement safeAt
in a non-recursive way? I have so much confusions.
Proxies are a mostly obsolete/legacy thing since
-XTypeApplications
and-XAllowAmbiguousTypes
have been available. Using these extensions together with-XScopedTypeVariables
gives you almost everything proxies can do.In many cases, including yours, that's not enough though: you need something stronger than proxies to propagate the
KnownNat
constraint. Singletons offer that capability. ASing n
forn :: Nat
packs theKnownNat
constraint in a value like a proxy, in a way that you can carry out computations with it.Throwing in the big
singletons-base
package (kinda overkill), you can start with the following:Of course that's not all: how do we express those size constraints?
It seems obvious that
ix < len
impliesix-1 < len-1
, but this is the kind of trivial thing compilers tend to disagree with.Welcome to the joy of dependently-typed programming!
...There's definitely work on getting this kind of thing behaving smoothly, both in GHC itself and the singletons-presburger plugin but I'm not sure how well it works.
Pragmatically speaking, you may be better off with a different approach: either
finite-typelits
offers that.In the first version, you don't need any proxies or singletons. This will do: