replicate function for a length-indexed list using GHC.TypeLits and singletons

一个人想着一个人 提交于 2020-01-11 07:34:07

问题


I'm trying to write a replicate function for a length-indexed list using the machinery from GHC.TypeLits, singletons, and constraints.

The Vect type and signature for replicateVec are given below:

data Vect :: Nat -> Type -> Type where
  VNil  :: Vect 0 a
  VCons :: a -> Vect (n - 1) a -> Vect n a

replicateVec :: forall n a. SNat n -> a -> Vect n a

How can you write this replicateVec function?


I have a version of replicateVec that compiles and type checks, but it appears to go into an infinite loop when run. The code is below. I have added comments to try to make the laws and proofs I am using a little easier to understand:

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeInType #-}

module VectStuff where

import Data.Constraint ((:-)(Sub), Dict(Dict))
import Data.Kind (Type)
import Data.Singletons.Decide (Decision(Disproved, Proved), Refuted, (:~:)(Refl), (%~))
import Data.Singletons.Prelude (PNum((-)), sing)
import Data.Singletons.TypeLits (SNat, Sing(SNat))
import GHC.TypeLits (CmpNat, KnownNat, Nat)
import Unsafe.Coerce (unsafeCoerce)

data Vect :: Nat -> Type -> Type where
  VNil  :: Vect 0 a
  VCons :: forall n a. a -> Vect (n - 1) a -> Vect n a

deriving instance Show a => Show (Vect n a)

-- This is used to define the two laws below.
axiom :: Dict a
axiom = unsafeCoerce (Dict :: Dict ())

-- | This law says that if we know that @n@ is not 0, then it MUST be
-- greater than 0.
nGT0CmpNatLaw :: (Refuted (n :~: 0)) -> Dict (CmpNat n 0 ~ 'GT)
nGT0CmpNatLaw _ = axiom

-- | This law says that if we know that @n@ is greater than 0, then we know
-- that @n - 1@ is also a 'KnownNat'.
cmpNatGT0KnownNatLaw :: forall n. (CmpNat n 0 ~ 'GT) :- KnownNat (n - 1)
cmpNatGT0KnownNatLaw = Sub axiom

-- | This is a proof that if we have an @n@ that is greater than 0, then
-- we can get an @'SNat' (n - 1)@
sNatMinus1 :: forall n. (CmpNat n 0 ~ 'GT) => SNat n -> SNat (n - 1)
sNatMinus1 SNat =
  case cmpNatGT0KnownNatLaw @n of
    Sub Dict -> SNat

-- | This is basically a combination of the other proofs.  If we have a
-- @SNat n@ and we know that it is not 0, then we can get an @SNat (n -1)@
-- that we know is a 'KnownNat'.
nGT0Proof ::
     forall n.
     Refuted (n :~: 0)
  -> SNat n
  -> (SNat (n - 1), Dict (KnownNat (n - 1)))
nGT0Proof f snat =
  case nGT0CmpNatLaw f of
    Dict ->
      case cmpNatGT0KnownNatLaw @n of
        Sub d -> (sNatMinus1 snat, d)

replicateVec :: forall n a. SNat n -> a -> Vect n a
replicateVec snat a =
  -- First we check if @snat@ is 0.
  case snat %~ (sing @_ @0) of
    -- If we get a proof that @snat@ is 0, then we just return 'VNil'.
    Proved Refl -> VNil
    -- If we get a proof that @snat@ is not 0, then we use 'nGT0Proof'
    -- to get @n - 1@, and pass that to 'replicateVec' recursively.
    Disproved f ->
      case nGT0Proof f snat of
        (snat', Dict) -> VCons a $ replicateVec snat' a

However, for some reason this replicateVec function goes into an endless loop when I try to run it:

> replicateVec (sing @_ @3) "4"
["4","4","4","4","4","4","4","4","4","4","4","4",^CInterrupted.

Why is this happening? How can I write the replicateVec function correctly?


回答1:


axiom :: Dict a is very unsafe because the runtime representation of a Dict a depends on the constraint a (which corresponds to a dictionary that is captured by the Dict constructor).

A KnownNat constraint corresponds to an integer value at runtime, so it is not correct to construct a Dict of KnownNat using unsafeCoerce on a dummy dictionary (in cmpNatGT0KnownNatLaw). In particular, this integer is used in replicateVec to check whether the integer is 0.

Type equalities (~) are special in that they have no meaningful runtime representation, hence axiom-atizing equalities, if they are correct, technically does not lead to bad runtime behavior because the coerced dictionary is never used, but coercing from Dict () to a Dict (a ~ b) is certainly not a supported use of unsafeCoerce. Coercing between equalities might be more reliable.

To solve KnownNat constraints, constraints internally associates the type-level operations to their term-level counterparts, see magic in Data.Constraints.Nat and reconstructs the KnownNat dictionary based on implicit knowledge about how GHC represents type classes.


Anyway, for an inductive construction like replicate, we can avoid KnownNat, and use a different singleton type that reflects the inductive nature of Nat.

data Sing n where
  Z :: Sing 0
  S :: Sing n -> Sing (1 + n)

This singleton is actually annoying to use because (+) is not injective. (\x -> (1 + x) technically is injective, but GHC can't tell that much.) It would be easier with an actually inductively defined Nat, but still, with the right set of constraints, we can do some things. For example, singleton reflection (mapping from the type-level n to a Sing n value):

class SingN n where
  singN :: Sing n

instance {-# OVERLAPPING #-} SingN 0 where
  singN = Z

instance (n ~ (1 + n'), n' ~ (n - 1), SingN n') => SingN n where
  singN = S (singN @n')

The list type should be similarly structured:

data List n a where
  Nil :: List 0 a
  Cons :: a -> List n a -> List (1 + n) a

The reason to set up the type index n this way instead of Sing (n-1) -> Sing n and a -> List (n-1) a -> List n a is to forbid some silly values:

oops :: Sing 0
oops = S undefined

ouch :: List 0 ()
ouch = Cons () undefined

which would be a problem because functions would actually need to handle those cases which make no sense.

replicate turns out to be straightforward to implement because List and Sing have a lot of structure in common.

replicate :: Sing n -> a -> List n a
replicate Z _ = Nil
replicate (S n) a = Cons a (replicate n a)

We can now apply replicate as follows:

replicate (singN @3) "x"
  • Compilable gist


来源:https://stackoverflow.com/questions/49709113/replicate-function-for-a-length-indexed-list-using-ghc-typelits-and-singletons

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!