Dear all,
My apologies for the lengthy mail. There is a question at the end about
whether all types in (of?) a kind closed under a type function can be
concluded to be a member of a type class (i.e. whether kind classes are
required for what I want to do).
I have been trying to reserect Robert Dockins' TypeNats library [1], by
pulling it out of the type classes / functional dependencies space and into
the type family space. The current (discontinued) TypeNats has one Very Nice
Thing to it; naturals are coded as bits, rather than as plain successors.
Length annotations of vectors et al. now have a memory complexity of O(lg N)
as opposed to O(N). A naïve implementation attempt runs into a rather obvious
problem:
data Z
data I n
data O n
class Natural n where
type Pred n
toIntegral :: Integral i => n -> i
toPred :: Natural n => n -> Pred n
toPred _ = undefined
instance Natural Z where
type Pred Z = Z
toIntegral _ = 0
instance Natural (I n) where
type Pred (I n) = O n
toIntegral n = let i = toIntegral (toPred n) in succ (i+i)
instance Natural (O n) where
type Pred (O n) = I (Pred n)
toIntegral n = let i = toIntegral (toPred n) in i+i
This should be interpreted as follows: Z denotes the empty bit string. I
denotes a 1 and O denotes a 0. The bits are in reversed order, so 6 is coded
as O(I(I Z)). One would expect the instance declaration of O to require a
context "Natural n," because of "Pred n" and "toPred n," but the type checker
allows for this. As a matter of fact, introducing the context "Natural n" to
both the instance declarations of I and O raises the error:
Could not deduce (Natural (Pred n)) from the context (Integral a)
arising from a use of `natToIntegral'
(in the instance declaration of O n)
Leaving the context out, this implementation causes stack overflows when
evaluating "toIntegral (undefined :: I Z)", because (O Z) is not reduced to
Z. The FD solution is to introduce a class that describes the "canonical
form" of a bit string (where the canonical form of O Z is Z):
class Canonical n cfunctions for Haskell Tom Schrijv | n -> c
Since we now have type functions, we can express this in a considerably more
comprehensive manner. We can simply write:
type family CanonicalNat c
type family CanonicalNat_ c
type instance CanonicalNat Z = Z
type instance CanonicalNat (I n) = I (CanonicalNat n)
type instance CanonicalNat (O n) = CanonicalNat_ (O (CanonicalNat n))
type instance CanonicalNat_ (O Z) = Z
type instance CanonicalNat_ (O (O n)) = O (O n)
type instance CanonicalNat_ (O (I n)) = O (I n)
With the type function CanonicalNat, toPred can now be redefined to give a
predecessor of a canonical type:
toPred :: Natural n => n -> CanonicalNat (Pred n)
toPred _ = undefined
Unfortunately:
Could not deduce (Natural (CanonicalNat_ (O (CanonicalNat n))))
from the context (Integral a) arising from a use of `natToIntegral'
Adding a context to the definitions of I and O, viz.
data Natural n => I n
data Natural n => O n
doesn't help very much. I'm no inference expert, but it seems that the problem
is that inference goes up, not down (or vice versa, depending on how you
interpret these words). Also, adding contexts to the results of type
functions is a non-trivial thing [2]).
In Schrijvers et al. [3], it is mentioned that work is being undertaken in the
direction of closed type functions by using kind declarations. For this
example, this would look something like:
datakind NatNum = Z | I NatNum | O NatNum
and
type family CanonicalNat (c :: NatNum) :: NatNum
Ideas for a new kind system for GHC is discussed on the wiki [4]. The
interaction with type classes section shows this example (for Z/Succ
represented Peano numbers):
data kind Nat = Zero | Succ Nat
class LessThanOrEqual (n1 :: Nat) (n2 :: Nat)
instance LessThanOrEqual Zero Zero
instance LessThanOrEqual n m => LessThanOrEqual n (Succ m)
Aside from the fact that I would imagine that there is a missing case here
instance LessThanOrEqual n m => LessThanOrEqual (Succ n) (Succ m)
which is non-trivial because of the overlap with the second instance
declaration, it is hard for me to see whether a checker with this new kind
system could decide that all types in Nat have corresponding instances in
LessThanOrEqual. It should be clear how this relates to my case described
above; can it now be decided that the result of CanonicalNat will always have
an instance of the Natural class?
Regards,
Philip
PS. A possible purpose of a function such as toIntegral is to have a length
implementation for fixed-length lists that is more lazy than the length
function on a list, but that can be used in exactly the same way.
[1] http://www.cs.princeton.edu/~rdockins/dist/typenats-0.2-source.tar.gz
[2]
http://content.ohsu.edu/cgi-bin/showfile.exe?CISOROOT=/etd&CISOPTR=227&filename=7031321462007_200705.diatchki.iavor.pdf
[3] http://research.microsoft.com/~simonpj/papers/assoc-types/ifl2007.pdf
[4] http://hackage.haskell.org/trac/ghc/wiki/KindSystem