I would like to have a type which can contain values 0 to n, where n lives on the type level.
I was trying something like:
import GHC.TypeLits
import Da
You can directly translate Idris's Fin
into the usual Haskell mishmash of sort-of-dependently-typed features.
data Fin n where
FZ :: Fin (S n)
FS :: Fin n -> Fin (S n)
(!) :: Vec n a -> Fin n -> a
(x :> xs) ! FZ = x
(x :> xs) ! (FS f) = xs ! f
With TypeInType
you can even have singleton Fin
s!
data Finny n (f :: Fin n) where
FZy :: Finny (S n) FZ
FSy :: Finny n f -> Finny (S n) (FS f)
This allows you to fake up dependent quantification over runtime stuff, eg,
type family Fin2Nat n (f :: Fin n) where
Fin2Nat (S _) FZ = Z
Fin2Nat (S n) (FS f) = S (Fin2Nat n f)
-- tighten the upper bound on a given Fin as far as possible
tighten :: Finny n f -> Fin (S (Fin2Nat n f))
tighten FZy = FZ
tighten (FSy f) = FS (tighten f)
but, ugh, it kinda sucks to have to duplicate everything at the value and type level, and writing out all your kind variables (n
) can get pretty tedious.
If you're really sure you need an efficient runtime representation of Fin
, you can do basically what you did in your question: stuff a machine Int
into a newtype
and use a phantom type for its size. But the onus is on you, the library implementer, to make sure the Int
fits the bound!
newtype Fin n = Fin Int
-- fake up the constructors
fz :: Fin (S n)
fz = Fin 0
fs :: Fin n -> Fin (S n)
fs (Fin n) = Fin (n+1)
This version lacks real GADT constructors, so you can't manipulate type equalities using pattern matching. You have to do it yourself using unsafeCoerce
. You can give clients a type-safe interface in the form of fold
, but they have to be willing to write all their code in a higher-order style, and (since fold
is a catamorphism) it becomes harder to look at more than one layer at a time.
-- the unsafeCoerce calls assert that m ~ S n
fold :: (forall n. r n -> r (S n)) -> (forall n. r (S n)) -> Fin m -> r m
fold k z (Fin 0) = unsafeCoerce z
fold k z (Fin n) = unsafeCoerce $ k $ fold k z (Fin (n-1))
Oh, and you can't do type level computation (as we did with Fin2Nat
above) with this representation of Fin
, because type level Int
s don't permit induction.
For what it's worth, Idris's Fin
is just as inefficient as the GADT one above. The docs contain the following caveat:
It's probably not a good idea to use
Fin
for arithmetic, and they will be exceedingly inefficient at run time.
I've heard noises about a future version of Idris being able to spot "Nat
with types"-style datatypes (like Fin
) and automatically erase the proofs and pack the values into machine integers, but as far as I know we're not there yet.