您如何设计类似类型来执行特定规则?
假设您有一种幼稚的向量类型,例如
data Vector a
= V2 a a
| V3 a a a
我对如何以及如何应该 - 实现特定于该类型的构造函数的特定逻辑。我的意思是可以用一个例子来澄清。
假设我要实现:
instance Num a => Num (Vector a) where
(+) =
(*) = _
abs = _
signum = _
fromInteger = _
negate = _
由于模式匹配,我可以很容易地将v2
与v2
匹配。但是,如果我希望特定组合(例如v2
和v3
之间)该怎么办?
在这种情况下,是否需要为v2
和v3
制作单个类型?但是,我不会失去他们目前拥有的多态性特性,是相同类型的?例如,如果我开始构建消耗vector
的操作,我目前只需要一个功能。但是,如果我分为两种类型,则需要两个不同的功能。
如何通过类型的思考来解决这个问题?
Suppose you have a naive vector type, such as
data Vector a
= V2 a a
| V3 a a a
I am interested in how you can — and how you should — implement particular logic specific to the type's constructor. What I mean can be clarified with an example.
Suppose I want to implement:
instance Num a => Num (Vector a) where
(+) =
(*) = _
abs = _
signum = _
fromInteger = _
negate = _
Due to pattern matching, I can very easily match V2
with V2
. However, what if I want it to be invalid for a particular combination, such as between V2
and V3
?
In this case, is it required to make individual types for V2
and V3
? Then, however, do I not lose the polymorphic properties they currently have, being the same type? For example, if I begin building operations consuming my Vector
, I currently need only one function. However, if I split into two types, I need two distinct functions.
How can I solve this problem by thinking in types?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
因此,您希望有时您的ADT案例看起来像不同的类型,因此不会意外混合,而其他时候您想能够同质地对待它们?这就是GADT的目的(除其他事项)。
给您的类型一个幻影类型的参数,并强迫不同的构造函数不同。当您不在乎时,您只需制作该类型参数通用:
在
vectorAdd
编译器识别v2
和v3
永远无法匹配因为它们总是具有不同的len
参数,但您仍然可以编写适用于任何len
的vectorfst
。下一级别 - 不要创建特殊用途
len2
和len3
类型,请使用GHC支持的数字类型级文字:So you want your ADT cases to look like distinct types sometimes, so they can't be accidentally mixed, yet other times you want to be able to treat them homogeneously? That's what GADTs are for (among other things).
Give your type a phantom type parameter, and force it to be different for the different constructors. When you don't care, you can just make that type parameter generic:
In
vectorAdd
the compiler recognizes thatV2
andV3
can never be matched because they always have differentlen
parameters, yet you still can writevectorFst
that works for anylen
.Next level - don't create special-purpose
Len2
andLen3
types, use numeric type-level literals, which GHC supports:您需要两个不同的功能 - 但是它们不必有两个不同的名称!为所有类型的向量有意义的所有基本操作上课;然后,如果愿意,您可以根据它们构建高级功能。例如:
然后,您可以定义
计算两个向量的平均长度,它将享受在
v2
s或v3 S,即使它仅定义一次。
(实际上,我永远不会将
lerp
放在vector
class;而是我写类应用v => vector v其中...代码>定义
lifta2。
You need two distinct functions -- but they don't have to have two distinct names! Make a class for all the base operations that make sense on vectors of either type; then you can build higher-level functions in terms of them if you like. For example:
Then you could define, say,
to compute the length of the average of two vectors, and it will enjoy that nice polymorphism property of working on either
V2
s orV3
s even though it was only defined once.(Actually, I would never put
lerp
in theVector
class; instead, I'd writeclass Applicative v => Vector v where ...
and definelerp = liftA2 . scalarLerp
. The development here was just for expository purposes.)(因为您可能不想制作
vector
anum
,我将转到Monad操作。)Haskell的
[]
单一简单地浏览元素的所有可能组合来解决此问题。例如,[(+1),(*2)]<*> [3,6,5]
=[4,7,6,6,12,10]
。对于您的vector
实现仅支持两个或三个元素,这将行不通。将V2(+1)(*2)<*> v3 3 6 5
有两个或三个元素?因此,您可能想扩展您的类型以支持所有列表,这只会使您直接回到[]
。如果您想维护此“拉链”操作,但是……事情变得更加复杂。可能,但很复杂。基本思想是将向量的长度嵌入到该类型中,同时仍保持多态性。我们从本质上可以使用类型级的自然数量来做到这一点...
(
z
代表“ Zero”和s
“继任者”,例如s(s(s) s(sz))))
= 4)...我们可以使用数据类型和 gadts :
这里,
n
是向量的长度。例如,v2
将由vector(s(sz))a
和v3
byvector> vector(s(s(sz)) )))
。不过,值得注意的是,您可以制作长度多态性,例如vector(s n)a
是所有非空置矢量的类型(长度至少为一个)。不幸的是,(GHC的)类型推理在GADT的存在下分解,因此您可能需要范围的类型变量(以及许多显式类型的签名)来执行任何有用的事情。所有实际的数据定义都在说
nil
的长度为零,并且在向量(x:^ xs
)的元素中,长度比给出的向量多。例如,这是一个
functor
实例,定义fmap
类似于[
]类型签名乱扔垃圾。不幸的是,这几乎总是必要的,因为GHC不太热衷于在GADT存在下采用多态性。
forall
此处有助于范围a
和b
,以便可以在go go
的类型中使用它们。如果不在 ingo
将是完全独立的类型变量,只有类似名称。为了定义更有趣的功能,GHC需要知道我们要与列表的长度进行交互,因此我们定义了一种“完善”长度的Singleton类型...
(读取
p n
为“”n
的自然值的证明)...以及可以访问该类型的类:
从这里,我们可以将
replicatev
类似地定义为[] []
<代码>复制:从这里定义
vzip ::(a - &gt; b - &gt; c) - &gt;
zip&gt; vector n c 类似于 如何制作长度 - 长度 - 媒介物实施的“>”这个问题,以稍微不同的方式扩展了这些想法。)
(Because you probably don't want to make
Vector
aNum
, I'll be referring to monad operations instead.)Haskell's
[]
monad solves this problem by simply going through every possible combination of elements. For example,[(+1),(*2)] <*> [3,6,5]
=[4,7,6, 6,12,10]
. For yourVector
implementation which only supports two or three elements, though, this won't work. WouldV2 (+1) (*2) <*> V3 3 6 5
have two or three elements? For this reason, you probably want to expand your type to support all lengths of lists, which would just bring you right back to[]
.If you want to maintain this "zipping" operation, though... things get more complicated. Possible, but complicated. The basic idea is to embed the length of the vector into the type while still maintaining polymorphism. We can essentially do this with type-level natural numbers...
(
Z
stands for "zero" andS
for "successor", e.g.S (S (S (S Z)))
= 4)...and we can utilize this with data kinds and GADTs:
Here,
n
is the length of the vector. For example,V2
would be represented byVector (S (S Z)) a
andV3
byVector (S (S (S Z))) a
. Notably, though, you can make the length polymorphic, e.g.Vector (S n) a
is the type of all non-empty vectors (length is at least one). Unfortunately, (GHC's) type inference breaks down in the presence of GADTs, so you'll probably need scoped type variables (and a lot of explicit type signatures) to do anything useful.All the actual data definition is saying is that
Nil
has length zero, and an element prepended to a vector (x :^ xs
) has length one more than the vector given.As an example, here's a
Functor
instance, definingfmap
analogously to[]
'smap
:Notice all of the type signatures littered around. Unfortunately, this is almost always necessary because GHC isn't too keen to assume polymorphism in the presence of GADTs. The
forall
here helps scopea
andb
so that they can be used in the type forgo
. If it wasn't there, then thea
andb
infmap
and thea
andb
ingo
would be completely separate type variables, just with similar names.To define more interesting functions, GHC needs to know that we want to interact with the length of the list, so we define a singleton type which "refines" the length...
(read
P n
as "a proof ofn
's natural value)...and a class which gives access to that type:
From here, we can define
replicateV
analogously to[]
'sreplicate
:From here, you can define a
vzip :: (a -> b -> c) -> Vector n a -> Vector n b -> Vector n c
similar to[]
'szip
.(See also this question which expands on these ideas in a slightly different way.)