Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type restriction in type declaration

There is famous example of the type level natural numbers:

data Zero
data Succ n

I have a question about desirable restriction when we applying type constructor Succ. For example, if we want to make such restriction in function definition, we can use the class and context, like in this code:

class Nat n where
  toInt :: n -> Int
instance Nat Zero where
  toInt _ = 0
instance (Nat n) => Nat (Succ n) where
  toInt _ = 1 + toInt (undefined :: n)

It's impossible to use toInt (undefined :: Succ Int), it's ok.

But how to realize similar restrictions on type level constructions (perhaps with some advanced type extensions)?

For instance, I'd like to permit using of type constructor Succ only with type Zero and with something, like this: Succ (Succ Zero), Succ (Succ (Succ Zero)) and so on. How to avoid such bad example on compile time:

type Test = Succ Int

(for now, there is no compilation error)

P.S.: It is more interesting for me how to create a restriction on the type declaration of the last sample

like image 306
Vladimir Avatar asked Nov 19 '25 23:11

Vladimir


1 Answers

Nowadays, we use the DataKinds extension:

{-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables #-}

-- N is a type, but is also a kind now
-- Zero, Succ Zero, ... are values, but are also type-level values of
-- kind N
data N = Zero | Succ N

-- (We could import Proxy the library, instead)
data Proxy (n :: N) = Proxy

-- Now n is restricted to kind N
class Nat (n :: N) where
  toInt :: proxy n -> Int

instance Nat Zero where
  toInt _ = 0
instance (Nat n) => Nat (Succ n) where
  toInt _ = 1 + toInt (undefined :: Proxy n)

We can then use toInt (Proxy :: Proxy (Succ Zero)). Instead, toInt (Proxy :: Proxy (Succ Int)) will raise a kind error, as wanted.

Personally, I would also replace proxies with more modern stuff like AllowAmbiguousTypes and TypeApplications so to remove the unused argument.

{-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables,
             AllowAmbiguousTypes, TypeApplications #-}

data N = Zero | Succ N

-- Now n is restricted to kind N
class Nat (n :: N) where
  toInt :: Int

instance Nat Zero where
  toInt = 0
instance (Nat n) => Nat (Succ n) where
  toInt = 1 + toInt @n

Use this as toInt @(Succ Zero). The toInt @n syntax chooses the n in the typeclass. It does not correspond to any value exchanged at runtime, only a type-level argument which exists at compile time.


Using

type Foo = Succ Int

also errors out as wanted:

    • Expected kind ‘N’, but ‘Int’ has kind ‘*’
    • In the first argument of ‘Succ’, namely ‘Int’
      In the type ‘Succ Int’
      In the type declaration for ‘Foo’
like image 176
chi Avatar answered Nov 22 '25 15:11

chi