尝试为长度索引向量实现类型安全的“at”时遇到困难

Difficulty in trying to implement a type-safe `at` for length-indexed vectors

提问人:Futarimiti 提问时间:5/20/2023 最后编辑:Futarimiti 更新时间:5/20/2023 访问量:88

问:

我刚刚了解了扩展,类型级文字,并发现可以使用 中提供的约束来比较类型级别的自然数,并使用 中提供的类型级操作进行操作。DataKindsData.Type.Ord(>)GHC.TypeNats(+)

考虑到这些,我萌生了实现长度索引向量的安全版本的想法。这个想法很简单:你传入一个作为索引,一个有长度和类型,然后你得到索引处的元素。编译器应该检测并大声喊叫任何非法 () 或 (其 )。!!ix :: Naturalvec :: Vec len alenaixixix < 0 || ix >= lenveclen == 0

因此,我首先定义了此类类型的签名(忽略扩展的导入和魔术注释):safeAt

infixr 5 :.
data Vec :: Nat -> * -> * where
  Nil :: Vec 0 a
  (:.) :: a -> Vec m a -> Vec (m + 1) a

safeAt :: forall (ix :: Nat) (len :: Nat) (proxy :: Nat -> *) a.
            (KnownNat ix, {- ix >= 0, -} len >= 1, ix < len) =>
              proxy ix -> Vec len a -> a
safeAt = undefined

按预期工作:

v :: Vec 10 Int
v = 1 :. 2 :. 3 :. 4 :. 5 :. 6 :. 7 :. 8 :. 9 :. 10 :. Nil

ok :: Int
ok = safeAt (Proxy :: Proxy 3) v

oops :: Int
oops = safeAt (Proxy :: Proxy 10) v  -- expected compilation error

在这里,我使用了一个代理来保存类型级别的 ,以便可以通过类型级别的约束来检查它。然而,当我尝试实现以下内容时,这成为一个问题:ixsafeAt

safeAt p (x:.xs) = case natVal p of
                     0 -> x
                     n -> safeAt (??? (n - 1)) xs

我需要晋升到 .我无法计算出这样的函数,甚至没有写出它的类型签名,并且开始怀疑这样的类型依赖函数是否存在于 Haskell 的类型系统中。我知道您可以使用 将 Nat 从类型级别拉到术语级别,但是有“反面”吗?也许那里有一个我不知道的神奇功能?此外,我相信此实现中还有其他问题,例如您如何安慰编译器,并且仍然给定代理不持有 .也许有一些替代方法可以以非递归方式实现?我有很多困惑。(n - 1)Proxy :: Proxy (n - 1)natVal(ix - 1)KnownNat(len - 1) >= 1p0safeAt

Haskell 矢量 型安全 VEC

评论

1赞 HTNW 5/20/2023
仅供参考:已弃用,取而代之的是 .*Data.Kind.Type
0赞 Futarimiti 5/20/2023
@HTNW 请原谅我。猜猜我正在看一些过时的教程。

答:

1赞 leftaroundabout 5/20/2023 #1

从那时起,代理几乎已经过时/遗留,并且已经可用。将这些扩展程序与它们一起使用,几乎可以为您提供代理可以做的所有事情。-XTypeApplications-XAllowAmbiguousTypes-XScopedTypeVariables

在许多情况下,包括你的,但这还不够:你需要比代理更强大的东西来传播约束。单例提供了这种能力。A for 将约束打包到一个值中,就像代理一样,您可以使用它进行计算。KnownNatSing nn :: NatKnownNat

抛出大型单例基础(有点矫枉过正),您可以从以下内容开始:

{-# LANGUAGE TypeApplications #-}

import Prelude.Singletons (SNum(..), sing)
import Data.Kind (Type)

safeAt :: forall (ix :: Nat) (len :: Nat) a.
              Sing ix -> Vec len a -> a
safeAt i (x:.xs) = case fromSing i of
                     0 -> x
                     _ -> safeAt (i %- sing @1) xs

当然,这还不是全部:我们如何表达这些尺寸限制?
这似乎很明显,但这是编译器倾向于不同意的琐碎事情。
ix < lenix-1 < len-1

欢迎来到依赖类型编程的乐趣!

...无论是在 GHC 本身还是在 singletons-presburger 插件中,肯定都有努力让这种事情顺利运行,但我不确定它的效果如何。

从实用的角度来说,你最好采用不同的方法:要么

  • 不要费心去做依赖类型的递归。相反,只需在下面使用不安全的实现,并在顶部添加类型约束。这是 IMO 针对此类问题的惯用 Haskell 解决方案。当然,它没有提供与完全依赖总版本相同的正确性验证级别,但如果您需要,则应使用 Agda 或 Idris 而不是 Haskell。
  • 用皮亚诺数字表示。对这些的归纳只是普通的模式匹配。
  • 将索引放入值级别(无论如何这可能更有用),但使用范围限制类型。Finite-TypeLits 提供了这一点。

在第一个版本中,您不需要任何代理或单例。这将执行以下操作:

{-# LANGUAGE ScopedTypeVariables, TypeApplications, AllowAmbiguousTypes #-}

unsafeAt :: Integer -> Vec len a -> a
unsafeAt = ...

safeAt :: ∀ ix len a . (KnownNat len, KnownNat ix, ix >= 0, ix < len)
  => Vec len a -> a
safeAt = unsafeAt (natVal @ix Proxy)

评论

0赞 Futarimiti 5/20/2023
那是。。。有很多东西要学!我能问一下是做什么的吗?为什么要在这里放一个空列表?natVal @ix []
0赞 leftaroundabout 5/20/2023
@Futarimiti哦,这只是我的一个肮脏的习惯,滥用列表语法来获取 TypeApplication 指定的任何类型的“代理”值。不过,在这种情况下它实际上不起作用,因为列表只接受 kind ,不接受 ...编辑。TypeNat
0赞 Futarimiti 5/21/2023
第三种方法真的是类型安全的吗?似乎与伊德里斯不同,在运行时之前不会捕获越界或负整数。到目前为止,我见过的唯一类型安全方法,再次使用代理。不过,我不确定我是否走在正确的道路上。FiniteFinnatToFinite
0赞 leftaroundabout 5/21/2023
Finite不包含任何负值或超出范围的值,从某种意义上说,尽管其内部实现允许存储此类值,但库的用户无法将此类值放入其中。With you 只会得到 ,它会引发错误,但不会生成非法值。packFiniteNothingfiniteFinite