(类型)安全地检索向量的元素

时间:2014-05-30 05:40:02

标签: haskell

我正在尝试编写一个函数

getColumn :: Int -> Vector n e -> e

从大小为n的矢量中检索第i个项目。

这些向量定义如下:

data Natural where
  Zero :: Natural
  Succ :: Natural -> Natural

type One   = Succ Zero
type Two   = Succ One
type Three = Succ Two
type Four  = Succ Three

data Vector n e where
  Nil  :: Vector Zero e
  (:|) :: e -> Vector n e -> Vector (Succ n) e

infixr :|

是否有某种方法可以编写getColumn函数,如果IntVector的大小过大,编译器将拒绝代码?

我该怎么写这个函数?

1 个答案:

答案 0 :(得分:16)

首先,我们需要一个 singleton 类型的自然。单例是类型级数据的运行时表示,它们被称为单例类型,因为它们中的每一个都只有一个值。这很有用,因为它在值和表示的类型之间建立了一对一的对应关系;只知道类型或值让我们推断另一个。

这也让我们避开了Haskell类型不能依赖Haskell值的限制:我们的类型将取决于单例的类型索引,但该类型索引可以依次由<单身的em> value 。这种有点曲折的绕道并不存在于完全依赖的编程语言中,例如Agda或Idris,其中类型可以依赖于值。

data SNatural (n :: Natural) where
    SZero :: SNatural Zero
    SSucc :: SNatural n -> SNatural (Succ n) 

deriving instance Show (SNatural n) -- requires StandaloneDeriving

我们可以看到,对于任何nSNatural n只有一个可能的值;它只反映了原始的Natural定义。

我们可以通过多种方式进行矢量索引。

1。直接定义<约束。

在自然界定义<非常简单:

{-# LANGUAGE TypeOperators, TypeFamilies #-}

type family a < b where 
    Zero   < Succ b = True
    a      < Zero   = False
    Succ a < Succ b = a < b  

现在我们可以用类型相等约束来表达有界性:

index :: ((m < n) ~ True) => Vector n a -> SNatural m -> a
index (x :| xs) SZero     = x 
index (x :| xs) (SSucc i) = index xs i
index _         _         = error "impossible"

main = do
    print $ index (0 :| 1 :| Nil) SZero -- 0
    print $ index (0 :| 1 :| Nil) (SSucc (SSucc SZero)) -- type error 

2。使用具有烘焙边界的自然的替代表示。

这是依赖类型编程的标准解决方案,而IMO更简单,尽管起初它有点难以理解。我们称为有界自然类型Fin n(对于&#34;有限&#34;),其中n是表示上限的自然类型。诀窍是以一种方式索引我们的数字,使得值的大小不能大于索引。

data Fin (n :: Natural) where
    FZero :: Fin (Succ n)
    FSucc :: Fin n -> Fin (Succ n) 

很明显,Fin Zero没有任何价值。 Fin (Succ Zero)只有一个值,FZeroFin (Succ (Succ Zero))有两个值,因此Fin n总是有n个可能的值。我们可以直接使用它进行安全索引:

index :: Vector n a -> Fin n -> a
index (x :| xs) FZero     = x
index (x :| xs) (FSucc i) = index xs i
index _         _         = error "impossible" 

main = do
    print $ index (0 :| 1 :| Nil) (FSucc (FSucc FZero)) -- type error

附录:使用singletons库并按Int - s进行安全索引。

正如我们所见,在Haskell中进行依赖编程需要大量的样板。类型级数据及其单例或多或少相同,但仍需要单独的定义。在它们上运行的功能必须类似地重复。幸运的是,singletons包可以为我们生成样板:

{-# LANGUAGE 
    TypeFamilies, GADTs, DataKinds, PolyKinds,
    ScopedTypeVariables, TemplateHaskell #-}

import Data.Singletons.TH

-- We get the "SNat n" singleton generated too. 
$(singletons[d| data Nat = Z | S Nat |])

data Vector n e where
  Nil  :: Vector Z e
  (:|) :: e -> Vector n e -> Vector (S n) e
infixr :|

data Fin n where
    FZ :: Fin (S n)
    FS :: Fin n -> Fin (S n) 

index :: Vector n a -> Fin n -> a
index (x :| xs) FZ     = x
index (x :| xs) (FS i) = index xs i
index _         _      = error "impossible"

该软件包还包括根据需要从类型生成单例值的便捷方法:

foo :: SNat (S (S (S Z)))
foo = sing

sing是一个多态值,可以作为任何单例值的替身。有时可以从上下文中推断出正确的值,但有时我们必须注释其类型索引,通常使用ScopedTypeVariables扩展。

现在我们也可以安全地通过Int - s进行索引,而不会受到样板的过多困扰(不是灾难性量的样板文件;为{{1}实现sing手动需要一个类型类和一些实例)。

一般来说,经验证的编程不是关于验证数据编译时间(正如我们在上面的例子中所见),而是编写以可证明的正确方式运行的函数,即使是未知编译的数据 - 时间(如果验证函数是正确的话,你可以说我们验证数据时无关紧要)。我们的Nat可以被视为半验证函数,因为不可能实现错误抛出的版本(模数底部和分歧)。

要安全地按index - 索引,我们只需编写一个经过验证的转换函数,从IntInt,然后像往常一样使用Fin

index

同样,checkBound :: Int -> SNat n -> Maybe (Fin n) checkBound i _ | i < 0 = Nothing checkBound 0 (SS _) = Just FZ checkBound i SZ = Nothing checkBound i (SS n) = case checkBound (i - 1) n of Just n -> Just (FS n) Nothing -> Nothing 的神奇之处在于,无法编写返回违反给定边界的checkBound的定义。

Fin

这里我们需要使用一些indexInt :: forall n a . SingI n => Vector n a -> Int -> Maybe a indexInt v i = case checkBound i (sing :: SNat n) of Just i -> Just (index v i) Nothing -> Nothing 机制:singletons约束允许我们使用SingI来形成一个合适的单例值。这是一个无害的类约束,因为每个可能的sing都是n的一个实例,通过构造。