何时想要使用专门的存在类型与依赖对(也称为依赖和或sigma类型)?
这是一个例子。
以下是长度索引列表和依赖类型的复制函数。有关如何实施replicateVect
的信息,请参见this other question。以下是使用singletons
库:
data Vect :: Type -> Nat -> Type where
VNil :: Vect a 0
VCons :: a -> Vect a n -> Vect a (n + 1)
replicateVect :: forall n a. SNat n -> a -> Vect a n
有(至少)两种可能的方法来创建一个复制函数,该函数采用普通Natural
而非单SNat
。
一种方法是为Vect
创建专门的存在类型。我按照SomeVect
:
singletons
data SomeVect :: Type -> Type where
SomeVect :: forall a n. Vect a n -> SomeVect a
replicateExistentialVect :: forall a. Natural -> a -> SomeVect a
replicateExistentialVect nat a =
case toSing nat of
SomeSing sNat -> SomeVect $ replicateVect sNat a
另一种方法是使用依赖对。这使用singletons
中的Sigma
类型:
replicateSigmaVect :: forall n a. Natural -> a -> Sigma Nat (TyCon (Vect a))
replicateSigmaVect nat a =
case toSing nat of
SomeSing sNat -> sNat :&: replicateVect sNat a
这些功能看起来非常相似。使用replicateExistentialVect
和replicteSigmaVect
也非常相似:
testReplicateExistentialVect :: IO ()
testReplicateExistentialVect =
case replicateExistentialVect 3 "hello" of
SomeVect vect -> print vect
testReplicateSigmaVect :: IO ()
testReplicateSigmaVect =
case replicateSigmaVect 3 "hello" of
_ :&: vect -> print vect
可以找到完整的代码here。
这让我想到了我的问题。
SomeVect
)与从属对(如Sigma
)?答案 0 :(得分:5)
- 我应该何时使用专门的存在类型(如
醇>SomeVect
)与从属对(如Sigma
)?
回答这个问题有点棘手,因为:
Sigma
本身就是一种专门的存在主义类型。SomeVect
和Sigma
只是这种现象的两个例子。尽管如此,它确实感觉Sigma
与GHC中存在类型的其他编码方式略有不同。让我们试着找出究竟是什么让它与众不同。
首先,让我们全面地写出Sigma
的定义:
data Sigma (s :: Type) :: (s ~> Type) -> Type where
(:&:) :: forall s (t :: s ~> Type) (x :: s).
Sing x -> Apply t x -> Sigma s t
为了进行比较,我还定义了一个典型的"存在类型:
data Ex :: (s -> Type) -> Type where
MkEx :: forall s (t :: s -> Type) (x :: s).
t x -> Ex t
让我们回顾两者之间的差异:
Sigma s t
有两个类型参数,而Ex t
只有一个。这不是一个非常重要的区别,实际上,您只能使用一个参数来编写Sigma
:
data Sigma :: (s ~> Type) -> Type where
(:&:) :: forall s (t :: s ~> Type) (x :: s).
Sing x -> Apply t x -> Sigma t
或Ex
使用两个参数:
data Ex (s :: Type) :: (s -> Type) -> Type where
MkEx :: forall s (t :: s -> Type) (x :: s).
t x -> Ex s t
我选择在Sigma
中使用两个参数的唯一原因是更接近地匹配其他语言中的从属对的表示,例如在Idris's DPair
中。它也可能使Sigma s t
和∃ (x ∈ s). t(x)
之间的类比更加清晰。
更重要的是,Sigma
的最后一个参数s ~> Type
的种类与Ex
的参数类型不同, s -> Type
。特别是,差异在(~>)
和(->)
种类之间。后者(->)
是熟悉的函数箭头,而前者(~>)
是singletons
中的一种去功能化符号。
什么是defunctionalization符号,为什么他们需要自己的类型?他们在论文Promoting Functions to Type Families in Haskell的第4.3节中进行了解释,但我会尝试在此处提供精简版本。从本质上讲,我们希望能够编写类型系列,如:
type family Positive (n :: Nat) :: Type where
Positive Z = Void
Positive (S _) = ()
并且能够使用Sigma Nat Positive
类型。但这不会起作用,因为你不能部分应用像Positive
这样的类型系列。幸运的是,defunctionalization技巧让我们可以解决这个问题。使用以下定义:
data TyFun :: Type -> Type -> Type
type a ~> b = TyFun a b -> Type
infixr 0 ~>
type family Apply (f :: k1 ~> k2) (x :: k1) :: k2
我们可以为Positive
定义 defunctionalization symbol ,让我们可以部分应用它:
data PositiveSym0 :: Nat ~> Type
type instance Apply PositiveSym0 n = Positive n
现在,在Sigma Nat PositiveSym0
类型中,第二个字段的类型为Apply PositiveSym0 x
,或仅为Positive x
。因此,(~>)
在某种意义上比(->)
更通用,因为它允许我们使用更多类型而不是(->)
。
(如果有帮助,可以将(~>)
视为无法匹配的函数的类型,如Richard Eisenberg's thesis的第4.2.4节所述,而(->)
是 matchable 函数的种类。)
虽然MkEx
构造函数只有一个字段,但(:&:)
构造函数有一个附加字段(类型为Sing x
)。有两个原因。一个是根据定义,存储这个额外字段是使Sigma
作为一个从属对的部分原因,这使我们可以通过projSigma1
函数来检索它。另一个原因是,如果您取出Sing x
字段:
data Sigma (s :: Type) :: (s ~> Type) -> Type where
(:&:) :: forall s (t :: s ~> Type) (x :: s).
Apply t x -> Sigma s t
然后这个定义需要AllowAmbiguousTypes
,因为x
类型变量是不明确的。这可能很麻烦,因此有一个明确的Sing x
字段可以避免这种情况。
现在我已经完成了冗长的解释,让我试着回答你的问题:
- 我应该何时使用专门的存在类型(如
醇>SomeVect
)与从属对(如Sigma
)?
我认为这最终是个人品味的问题。 Sigma
很好,因为它非常简单,但您可能会发现定义专门的存在类型会使您的代码更容易理解。 (但也请看下面的警告。)
- 是否有任何只能用其中一种写入的功能?
醇>
我认为我之前的Sigma Nat PositiveSym0
示例会被视为您无法对Ex
执行的操作,因为它需要利用(~>)
类型。另一方面,您也可以定义:
data SomePositiveNat :: Type where
SPN :: Sing (n :: Nat) -> Positive n -> SomePositiveNat
因此,您在技术上并不需要(~>)
来执行此操作。
此外,我不知道为projSigma1
编写Ex
等效内容的方法,因为它没有存储足够的信息以便能够编写此内容。
另一方面,Sigma s t
要求Sing
种s
个实例,如果没有Sigma
,Sigma
可能是(~>)
不会工作。
- 是否有任何功能使用其中一个更容易编写?
醇>
当您迫切需要使用(->)
类型的内容时,您可以更轻松地使用Ex
,因为这是它闪耀的地方。如果你的类型只能使用s -> Type
类型,那么使用"典型的"可能会更方便。像s ~> Type
这样的存在类型,因为否则你必须以TyCon
的形式引入噪音,以便将某种类型Sigma
提升为Sing x
。
此外,如果能够方便地检索{{1}}类型的字段,您可能会发现{{1}}更容易使用。