我最近了解了促销活动,并决定尝试写矢量。
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
module Vector where
data Nat = Next Nat | Zero
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
到目前为止,一切正常。但是在尝试制作Vector
的{{1}}实例时,我遇到了一个问题。
Applicative
我不知道怎么做instance Applicative (Vector n) where
a <*> b =
case a of
Construct f c ->
case b of
Construct x d -> Construct (f x) (c <*> d)
Empty -> Empty
pure x = _
。我试过这个:
pure
但第一行出现case n of
Next _ -> Construct x (pure x)
Zero -> Empty
错误,此表达式的第三行出现Variable not in scope: n :: Nat
。
所以,我使用了下面的黑客。
Couldn't match type n with 'Zero
它完成了工作,但它并不漂亮。它引入了一个无用的类class Applicative' n where
ap' :: Vector n (t -> u) -> Vector n t -> Vector n u
pure' :: t -> Vector n t
instance Applicative' n => Applicative' ('Next n) where
ap' (Construct f a) (Construct x b) = Construct (f x) (ap' a b)
pure' x = Construct x (pure' x)
instance Applicative' 'Zero where
ap' Empty Empty = Empty
pure' _ = Empty
instance Applicative' n => Applicative (Vector n) where
(<*>) = ap'
pure = pure'
。每当我想在任何函数中使用Applicative'
Applicative
时,我都必须提供额外的无用约束Vector
,它实际上适用于任何Applicative' n
。
这样做会有更好,更清洁的方法吗?
答案 0 :(得分:7)
您可以直接制作相同的内容:
instance Applicative (Vector Zero) where
a <*> b = Empty
pure x = Empty
instance Applicative (Vector n) => Applicative (Vector (Next n)) where
a <*> b =
case a of
Construct f c ->
case b of
Construct x d -> Construct (f x) (c <*> d)
pure x = Construct x (pure x)
正如我可以推断的那样:对于不同类型的类,代码应该是类型感知的。如果您有多个实例,不同的类型将获得不同的实现,并且很容易解决。但是,如果您尝试使用单个非递归实例来创建它,则基本上没有关于运行时类型的信息,并且始终相同的代码仍需要确定要处理的类型。当您有输入参数时,您可以利用GADT为您提供类型信息。但对于pure
,没有输入参数。因此,您必须为Applicative
实例提供一些上下文。
答案 1 :(得分:2)
这是一个利用singletons
包的(注释)替代方案。
非常粗略,Haskell不允许我们在上面的代码中对类型级值(如n
)进行模式匹配。使用singletons
,我们可以在此处和那里要求并提供一些SingI
的实例。
{-# LANGUAGE GADTs , KindSignatures, DataKinds, TemplateHaskell,
TypeFamilies, ScopedTypeVariables #-}
{-# OPTIONS -Wall #-}
import Data.Singletons.TH
-- Autogenerate singletons for this type
$(singletons [d|
data Nat = Next Nat | Zero
|])
-- as before
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
-- as before
instance Functor (Vector n) where
fmap _ Empty = Empty
fmap f (Construct x b) = Construct (f x) (fmap f b)
-- We now require n to carry its own SingI instance.
-- This allows us to pattern match on n.
instance SingI n => Applicative (Vector n) where
Empty <*> Empty = Empty
-- Here, we need to access the singleton on n, so that later on we
-- can provide the SingI (n-1) instance we need for the recursive call.
-- The withSingI allows us to use m :: SNat (n-1) to provide the instance.
(Construct f c) <*> (Construct x d) = case sing :: SNat n of
SNext m -> withSingI m $ Construct (f x) (c <*> d)
-- Here, we can finally pattern match on n.
-- As above, we need to provide the instance with withSingI
-- to the recursive call.
pure x = case sing :: SNat n of
SZero -> Empty
SNext m -> withSingI m $ Construct x (pure x)
使用它需要在每次使用时提供SingI n
实例,这有点不方便,但不是太多(IMO)。可悲的是<*>
并不真正需要SingI n
,因为原则上它可以从手边的两个向量重新计算出来。但是,pure
没有输入向量,因此它只能与提供的单例进行模式匹配。
作为另一种选择,与原始代码类似,可以编写
instance Applicative (Vector Zero) where
...
instance Applicative (Vector n) => Applicative (Vector (Next n)) where
...
这不是完全等效的,并且需要在Applicative (Vector n) =>
未知的所有函数中添加上下文n
,但这可能足以用于多种用途。
答案 2 :(得分:2)
考虑这是@ chi答案的附录,以提供对单身方法的额外解释......
如果您还没有这样做,我建议您阅读Hasochism paper。特别是,在该论文的第3.1节中,他们正好处理了这个问题,并将其用作隐式单例参数(@ chi的答案的SingI
和NATTY
类型类的激励示例在Hasochism论文中)是必要的,而不仅仅是方便的。
因为它适用于您的代码,主要问题是pure
需要运行时表示它应该生成的向量的长度,以及类型级变量n
不符合要求。解决方案是引入一个新的GADT,一个“单例”,提供直接对应于提升类型Next
和Zero
的运行时值:
data Natty (n :: Nat) where
ZeroTy :: Natty Zero
NextTy :: Natty n -> Natty (Next n)
我尝试使用与论文大致相同的命名约定:Natty
是相同的,ZeroTy
和NextTy
对应于论文的Zy
和{{1 }}
这个显式单例本身很有用。例如,请参阅论文中Sy
的定义。此外,我们可以轻松编写vchop
的变体,使用显式单例来完成其工作:
pure
我们还不能使用它来定义vcopies :: Natty n -> a -> Vector n a
vcopies ZeroTy _ = Empty
vcopies (NextTy n) x = Construct x (vcopies n x)
,因为pure
的签名是由pure
类型类确定的,我们无法将其显式化单身Applicative
在那里。
解决方案是引入隐式单例,这允许我们在需要时通过Natty n
函数在以下类型类的上下文中检索显式单例:
natty
现在,如果我们处于class NATTY n where
natty :: Natty n
instance NATTY Zero where
natty = ZeroTy
instance NATTY n => NATTY (Next n) where
natty = NextTy natty
上下文中,我们可以致电NATTY n
向vcopies natty
提供明确的vcopies
参数,这样我们就可以写:< / p>
natty
使用上面instance NATTY n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies natty
和vcopies
的定义,以及下面natty
的定义:
vapp
注意一个奇怪的地方。出于不明原因,我们需要引入这个vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
辅助函数。以下没有 vapp
的实例与基于NATTY
的定义和类型检查相匹配:
case
如果我们添加instance Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = Construct (f x) (c <*> d)
pure = error "Argh! No NATTY!"
约束来定义NATTY
:
pure
instance NATTY n => Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = Construct (f x) (c <*> d)
pure = vcopies natty
的定义不再进行类型检查。问题是第二个(<*>)
案例左侧的NATTY n
约束不会自动暗示右侧的(<*>)
约束(NATTY n1
}),所以GHC不希望我们在右侧调用Next n ~ n1
。在这种情况下,因为在第一次使用约束之后实际上不需要约束,所以没有(<*>)
约束的辅助函数,即NATTY
,可以解决问题。
@chi使用vapp
上的大小写匹配和辅助函数natty
作为替代解决方法。这里的等效代码将使用一个辅助函数将一个显式单例转换为隐式withSingI
上下文:
NATTY
允许我们写:
withNATTY :: Natty n -> (NATTY n => a) -> a
withNATTY ZeroTy a = a
withNATTY (NextTy n) a = withNATTY n a
这需要instance NATTY n => Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = case (natty :: Natty n) of
NextTy n -> withNATTY n $ Construct (f x) (c <*> d)
pure x = case (natty :: Natty n) of
ZeroTy -> Empty
NextTy n -> Construct x (withNATTY n $ pure x)
和ScopedTypeVariables
。
无论如何,坚持使用辅助函数,完整的程序如下所示:
RankNTypes
与{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
module Vector where
data Nat = Next Nat | Zero
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
data Natty (n :: Nat) where
ZeroTy :: Natty Zero
NextTy :: Natty n -> Natty (Next n)
class NATTY n where
natty :: Natty n
instance NATTY Zero where
natty = ZeroTy
instance NATTY n => NATTY (Next n) where
natty = NextTy natty
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
instance NATTY n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies natty
vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
vcopies :: Natty n -> a -> Vector n a
vcopies ZeroTy _ = Empty
vcopies (NextTy n) x = Construct x (vcopies n x)
库的对应关系是:
singletons
自动生成单例(包含构造函数$(singletons [d|
data Nat = Next Nat | Zero
|])
和SZero
,而不是SNat
和ZeroTy
;以及类型NatTy
而不是SNat
)和隐式单例类(称为Natty
而不是SingI
并使用函数NATTY
而不是sing
),给出完整的程序:
natty
有关{-# LANGUAGE DataKinds, GADTs, KindSignatures, TemplateHaskell, TypeFamilies #-}
module Vector where
import Data.Singletons
import Data.Singletons.TH
$(singletons [d|
data Nat = Next Nat | Zero
|])
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
instance SingI n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies sing
vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
vcopies :: SNat n -> a -> Vector n a
vcopies SZero _ = Empty
vcopies (SNext n) x = Construct x (vcopies n x)
库的功能及其构建方式的更多信息,我建议您阅读Introduction to Singletons。
答案 3 :(得分:1)
其他几个答案引入了Natty
或SNat
类型来实施pure
。实际上,拥有这种类型大大减少了对一次性类型的需求。然而,传统的Natty
/ SNat
GADT的潜在缺点是,即使在编译时已知Nat
,您的程序也会实际构建表示然后使用它。使用辅助类方法通常不会发生。你可以通过使用不同的表示来解决这个问题。
我将使用这些名称:
data Nat = Z | S Nat
假设我们定义了通常的
data Natty n where
Zy :: Natty 'Z
Sy :: Natty n -> Natty ('S n)
我们可以写出它的消除器(归纳原理):
natty :: p 'Z -> (forall k. p k -> p ('S k)) -> Natty n -> p n
natty z _ Zy = z
natty z s (Sy n) = s (natty z s n)
出于我们的目的,我们并不真正需要Natty
;我们只需要它的感应原理!所以让我们定义另一个版本。我想这个编码有一个正确的名称,但我不知道它可能是什么。
newtype NatC n = NatC
{ unNatC :: forall p.
p 'Z -- base case
-> (forall k. p k -> p ('S k)) -- inductive step
-> p n }
这与Natty
:
nattyToNatC :: Natty n -> NatC n
nattyToNatC n = NatC (\z s -> natty z s n)
natCToNatty :: NatC n -> Natty n
natCToNatty (NatC f) = f Zy Sy
现在我们可以为Nat
编写一个我们知道如何消除的类:
class KnownC n where
knownC :: NatC n
instance KnownC 'Z where
knownC = NatC $ \z _ -> z
instance KnownC n => KnownC ('S n) where
knownC = NatC $ \z s -> s $ unNatC knownC z s
现在这里是一个矢量类型(我已根据自己的喜好重新命名):
infixr 4 :<
data Vec :: Nat -> * -> * where
(:<) :: t -> Vec n t -> Vec ('S n) t
Nil :: Vec 'Z t
由于Vec
的长度参数不是最后一个,我们必须将其翻转以与NatC
一起使用:
newtype Flip f a n = {unFlip :: f n a}
induct2 :: f 'Z a
-> (forall k. f k a -> f ('S k) a)
-> NatC n -> f n a
induct2 z s n = unFlip $ unNatC n (Flip z) (\(Flip r) -> Flip (s r))
replC :: NatC n -> a -> Vec n a
replC n a = induct2 Nil (a :<) n
instance KnownC n => Applicative (Vec n) where
pure = replC knownC
(<*>) = ...
现在,如果在编译时知道向量长度,则pure
向量将直接构建,不需要中间结构。