我正在尝试编写一个函数
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
函数,如果Int
对Vector
的大小过大,编译器将拒绝代码?
我该怎么写这个函数?
答案 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
我们可以看到,对于任何n
,SNatural n
只有一个可能的值;它只反映了原始的Natural
定义。
我们可以通过多种方式进行矢量索引。
<
约束。在自然界定义<
非常简单:
{-# 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
这是依赖类型编程的标准解决方案,而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)
只有一个值,FZero
,Fin (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
- 索引,我们只需编写一个经过验证的转换函数,从Int
到Int
,然后像往常一样使用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
的一个实例,通过构造。