For time ago Bas van Dijk asked a question on glasgow-haskell-users mailing list about proving the properties of type-level natural numbers obtained by user. That lead to an interesting discussion, that you can read in the list archives.

The problem was that we want to impose a constraint on some typelevel natural and then read value from user and guarantee that we have that constraint. For such constraint we took (<=255).

Here is a file that describe few approaches to solve this problem.

At first we need to use a HUGE list of pragmas.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

And relax compiler options a bit.

{-# OPTIONS_GHC -ftype-function-depth=1024 #-}
{-# OPTIONS_GHC -fcontext-stack=1024 #-}

import GHC.TypeLits
import GHC.Exts 
import Data.Proxy
import Data.Constraint
import Data.Type.Bool
import Data.Type.Equality
import System.Environment
import Unsafe.Coerce
import Data.Singletons
import Control.Monad

Attempt-1: Tagged value

We can keep a proof together with value, so we can introduce a new type:

data Proof n  (c :: Constraint) where Proof :: Proxy n -> Proof n c

In mailing list I provide following code:

fromSomeError :: SomeNat -> Maybe (Proof n (n <= 255))
fromSomeError (SomeNat p)
   | natVal p <= 255 = Just (Proof (Proxy :: Proxy n))
   | otherwise = Nothing

This is obviously a bug as we check natVal p, but return Proxy n. And correct version fails, as there is no way to simply inject a constraint.

{-
fromSome :: SomeNat -> Maybe (Proof n (n <= 255))
fromSome (SomeNat p)
  | natVal p <= 255 = Just (Proof p)
  | otherwise = Nothing
-}
 Proof.lhs:53:37:
    Could not deduce (n1 ~ n)
    from the context (KnownNat n1)
    bound by a pattern with constructor
        SomeNat :: forall (n :: Nat). KnownNat n => Proxy n -> SomeNat,
	in an equation for ‘fromSome’
	at Proof.lhs:52:13-21

Attempt-2: Type carrier

Now we want to have the following constraint on out Nat

data Proof2 :: (Nat -> Constraint) -> * where
  Proof2 :: c n => Proxy n -> Proof2 c

In order to have a ‘curried’ version of our constaint we can introduce a type family

type family LessThan255 n :: Constraint where
    LessThan255 f = (f <= 255)

Now lets try to create a proof from known natural:

{-
strange :: (LessThan255 n) => Proxy n -> Proof2 LessThan255
strange = Proof2
-}

~~~~~~~~~~~~~~~ Proof.lhs:89:13: Could not deduce (LessThan255 n) arising from a use of ‘Proof2’ from the context (LessThan255 n) bound by the type signature for strange :: (LessThan255 n) => Proxy n -> Proof2 LessThan255 ~~~~~~~~~~~~~~

strange :: (LessThan255 ~ c, c n) => Proxy n -> Proof2 c
strange = Proof2

Previous attempt to build function failed, but this one works. I don’t know if it’s related to non-injectivity or a bug.. I can report it :)

When I realized this problem, I have stopped.

Update. As Richard Eisenberg says:

By the way, the bug in the Proof2 version is a bug in GHC 7.8.3
(only in .3 -- not in .2 or in the soon-to-be .4) that allows you
to write unsaturated type families that don't work.
Saying `LessThan255` without a parameter should be a syntax error,
but that check was accidentally turned off for 7.8.3, leading to a bogus type error.

Attempt-3: carry a contraint in a datatype

Now let’s keep our ‘constraint’ in a datatype, here we have 2 proxy, one for datatype, and one for value, also a KnownNat constraint that we want to use later:

data Proof3 :: (Nat -> *) -> * where
  Proof3 :: KnownNat n => c n -> Proxy n -> Proof3 c

We can introduce a Show instance

instance Show (Proof3 c) where
  show (Proof3 _ k) = show $ natVal k

And now we can introduce a LessThen constraint as a datatype:

data LessThan255D (n::Nat) where LessThan255D :: (n <= 255) => LessThan255D n

We can convert type level constrant to data easily:

c2d :: LessThan255 n => Proxy n -> LessThan255D n
c2d _ = LessThan255D

But what about a proof, ideally we want to have following code:

{-
fromSome3 :: SomeNat -> Maybe (Proof3 LessThan255D)
fromSome3 (SomeNat p)
   | natVal p < natVal t255 = Just (Proof3 LessThan255D p)
   | otherwise = Nothing
   where t255 = Proxy :: Proxy 255
-}
Proof.lhs:138:46:
  Could not deduce ((n <=? 255) ~ 'True)
  from the context (KnownNat n)

It doesn’t work simply because value level check doesn’t guarantee typelevel properties.

One way to solve it is to use unsafeCoerce: the idea is to use a proof for the value we know, and then coerce a type of a proof to the type of the proof related to the users value:

fromSome3 :: SomeNat -> Maybe (Proof3 LessThan255D)
fromSome3 (SomeNat p)
   | natVal p < natVal t255 = Just (fake (c2d t255) p)
   | otherwise = Nothing
   where t255 = Proxy :: Proxy 255
         fake :: KnownNat n => c p -> Proxy n -> Proof3 c
         fake k p = Proof3 (unsafeCoerce k :: c n) p

It’s a nice solution, it’s unsafe as typechecker does not check that our predicate (natVal (p::Proxy n) < natVal (t255::Proxy 255)) implies the safety of a coerce from LessThan255 ~ 255 -> LessThan255 ~ p.

There is one more solution that is really typesafe, but has a big complexity, and require a bounded set of values:

We can try to check is equal to one specific value from the set.

guessProof :: (KnownNat n, n <= 255) => SomeNat -> Proxy n -> Maybe (Proof3 LessThan255D)
guessProof (SomeNat p) n = case sameNat p n of
    Just _  -> Just $ Proof3 LessThan255D n
    Nothing -> Nothing

Now we can build all set of values that are good:

type family Guesses (n::Nat) :: [Nat] where
   Guesses 0 = '[0]
   Guesses n = n ': Guesses (n-1)

And write a code that will check all possible values:

class GuessProof (n :: [Nat]) where
  proof :: SomeNat -> Proxy n -> Maybe (Proof3 LessThan255D)

instance GuessProof '[] where
  proof _ _ = Nothing

instance (KnownNat n, n <= 255, GuessProof ns) => GuessProof (n ': ns) where
  proof s p = guessProof s (inner p) `mplus` proof s (next p)
   where inner :: Proxy (n ': ns) -> Proxy (n::Nat)
         inner _ = Proxy
         next :: Proxy (n ': ns) -> Proxy (ns::[Nat])
         next _ = Proxy

It’s not very usable, have a bad complexity and require to change ghc options but it works and it’s safe.

Now let’s test our code:

f2 :: (c ~ (n <= 255)) => Proof3 LessThan255D -> IO ()
f2 (Proof3 n p) = print $ natVal p
test1 :: IO ()
test1 = do
    n <- readLn :: IO Integer

    case someNatVal n of
      Nothing -> error "Input is not a natural number!"
      Just sn -> case fromSome3 sn of
                   Just p -> f2 p
                   _ -> error "Input if larger than 255"
test2 :: IO ()
test2 = do
  n <- readLn :: IO Integer
  case someNatVal n of
      Nothing -> error "Input is not a natural number!"
      Just sn -> case proof sn (g (Proxy :: Proxy 255)) of
                   Just p -> f2 p
                   _ -> error "Input if larger than 255"
  where
    g :: Proxy n -> Proxy (Guesses n)
    g _ = Proxy

Attempt 5: Singletons

This is not the only solution, one more solution was provided by Richard Eisenberg I have not found it in mailing list archives so including it here.

The idea is that we may use less effective representation for typelevel naturals, i.e. unary naturals we can build a proof using singletons:

{-# LANGUAGE TemplateHaskell, DataKinds, PolyKinds, TypeFamilies,
             ScopedTypeVariables, TypeOperators, UndecidableInstances,
             GADTs, RankNTypes #-}
{-# OPTIONS_GHC -ftype-function-depth=300 -fcontext-stack=300 #-}

import Data.Singletons.TH
import GHC.TypeLits hiding ( Nat )

$(singletons [d|
  data Nat = Zero | Succ Nat

  leNat :: Nat -> Nat -> Bool
  leNat Zero     _        = True
  leNat (Succ _) Zero     = False
  leNat (Succ a) (Succ b) = a `leNat` b
  |])

-- | Singletons's 'withSomeSing' is what we want, but a bug in 7.8.3 doesn't
-- let it work without a specialized type for 'Nat's
withSomeNat :: Nat -> (forall (n :: Nat). Sing n -> r) -> r
withSomeNat = withSomeSing

-- | Conveniently generate unary naturals
type family U n where
  U 0 = Zero
  U n = Succ (U (n-1))

toNat :: Integer -> Maybe Nat
toNat n | n < 0      = Nothing
        | otherwise  = Just $ go n
  where
    go 0 = Zero
    go n = Succ (go (n-1))

type Bound = U 255

-- easier to test in GHCi than a proper 'main'
go :: Integer -> IO ()
go n =
   case toNat n of
     Nothing  -> putStrLn "Input is not a natural number!"
     Just nat -> putStrLn $ withSomeNat nat $ \ snat ->
       case snat `sLeNat` (sing :: Sing Bound) of
         STrue  -> f snat
         SFalse -> "Didn't work"

f :: forall proxy (n :: Nat). (n `LeNat` Bound) ~ True => proxy n -> String
f _ = "It worked!"

Attempt 6: Using Luquid haskell

Ranjit Jhala, provided a great solution for liquid haskell, that could be found either by url or here:



{-@ LIQUID "--no-termination" @-}

module Nat255 where

-- | Define a predicate for valid integers

{-@ predicate IsValid X = 0 <= X && X < 255 @-}

-- | Use the predicate to define a refinement type (subset) of valid integers

{-@ type Valid = {v:Int | IsValid v}        @-}

-- | A function that checks whether a given Int is indeed valid

{-@ isValid   :: n:Int -> {v:Bool | Prop v <=> IsValid n} @-}
isValid n     = 0 <= n && n < (255 :: Int)

-- | A function that can only be called with Valid Ints.

{-@ workWithValidNumber :: Valid -> IO () @-} 
workWithValidNumber n = putStrLn $ "This is a valid number" ++ show (n :: Int)

-- | This is fine...
ok    = workWithValidNumber 12

-- | ... But this is not.        
notOk = workWithValidNumber 257

-- | Finally the top level loop that inputs a number, tests it 
--   and calls `workWithValidNumber` if the number is valid.

loop = do putStrLn "Enter Number between 0 and 255"
          n <- readLn :: IO Int
          if isValid n
             then workWithValidNumber n
             else putStrLn "Humph, bad input, try again!" >> loop

I hope you have enjoyed reading this.




comments powered by Disqus