使用GHC.TypeLits和单例复制长度索引列表的函数

ill*_*out 1 haskell list dependent-type singleton-type

我正在尝试使用GHC.TypeLits,singletonsconstraints中的机制为长度索引列表编写复制函数.

Vect类型和签名replicateVec给出如下:

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
Run Code Online (Sandbox Code Playgroud)

你怎么写这个replicateVec功能?


我有一个replicateVec编译和类型检查的版本,但它似乎在运行时进入无限循环.代码如下.我添加了评论,试图使我使用的法律和证据更容易理解:

{-# 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
Run Code Online (Sandbox Code Playgroud)

但是,由于某些原因,replicateVec当我尝试运行它时,此函数会进入无限循环:

> replicateVec (sing @_ @3) "4"
["4","4","4","4","4","4","4","4","4","4","4","4",^CInterrupted.
Run Code Online (Sandbox Code Playgroud)

为什么会这样?如何replicateVec正确编写函数?

Li-*_*Xia 6

axiom :: Dict a是非常不安全的,因为a的运行时表示Dict a取决于约束a(它对应于Dict构造函数捕获的字典).

KnownNat约束对应于在运行时的整数值,所以这是不正确的构造DictKnownNat使用unsafeCoerce上的虚设字典(在cmpNatGT0KnownNatLaw).特别是,该整数用于replicateVec检查整数是否为0.

类型等式(~)是特殊的,因为它们没有有意义的运行时表示,因此axiom它们是正确的,如果它们是正确的,技术上不会导致错误的运行时行为,因为强制字典从未使用过,但强制转换Dict ()为a Dict (a ~ b)肯定不是支持的用法的unsafeCoerce.在平等之间强制执行可能更可靠.

为了解决KnownNat约束,约束内部的类型级操作关联到其任期级同行,看magicData.Constraints.Nat和重构KnownNat基于对GHC如何代表类型类隐性知识词典.


无论如何,对于感应式结构replicate,我们可以避免KnownNat,并使用反映其归纳性质的不同单体类型Nat.

data Sing n where
  Z :: Sing 0
  S :: Sing n -> Sing (1 + n)
Run Code Online (Sandbox Code Playgroud)

这个单身实际上很烦人,因为(+)它不是单射的.(\x -> (1 + x)技术上是单射的,但GHC不能说明这么多.)实际归纳定义会更容易Nat,但是,通过正确的约束条件,我们可以做一些事情.例如,单例反射(从类型级nSing n值的映射):

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')
Run Code Online (Sandbox Code Playgroud)

列表类型应该是类似的结构:

data List n a where
  Nil :: List 0 a
  Cons :: a -> List n a -> List (1 + n) a
Run Code Online (Sandbox Code Playgroud)

n这种方式设置类型索引的原因而不是Sing (n-1) -> Sing na -> List (n-1) a -> List n a禁止一些愚蠢的值:

oops :: Sing 0
oops = S undefined

ouch :: List 0 ()
ouch = Cons () undefined
Run Code Online (Sandbox Code Playgroud)

这将是一个问题,因为函数实际上需要处理那些毫无意义的情况.

replicate结果很简单,因为ListSing有很多共同的结构.

replicate :: Sing n -> a -> List n a
replicate Z _ = Nil
replicate (S n) a = Cons a (replicate n a)
Run Code Online (Sandbox Code Playgroud)

我们现在可以申请replicate如下:

replicate (singN @3) "x"
Run Code Online (Sandbox Code Playgroud)

  • 通过强制使用`Dict(a~a)`而不是'Dict()`创建`axiom`可能会稍微安全一些. (2认同)
  • @illabout,使用`Data.Type.Equality.:〜:`比`Dict`更常见的是等号.它也可能更有效率,但我现在没有时间解释原因. (2认同)