类型限制对于简单的代码来说变得庞大而且难以阅读

4
请注意以下代码:

-- A n-dimensional axis aligned bounding box.
data AABB v a = AABB {
    aabbMin :: !(v a), 
    aabbMax :: !(v a)
    } deriving (Show)

-- `v` is a container, representing the number of dimensions. Ex:
-- type Box2DFloat = AABB V2 Float
-- type Box4DFloat = AABB V4 Float 

-- A n-dimensional ray.
data Ray v a = Ray {
    rayPos :: !(v a),
    rayDir :: !(v a)
    } deriving (Show)

-- Traces a n-d ray through a n-d box, returns 
-- the intersection indexes (tmin, tmax).
intersectAABB 
    :: (Foldable f, 
        Metric f, 
        Ord a, 
        Num (f a), 
        Fractional (f a), 
        Floating a) 
    => Ray f a 
    -> AABB f a 
    -> [a]
intersectAABB (Ray rayPos rayDir) (AABB aabbMin aabbMax) 
    = [tmin, tmax] where
        t1   = (aabbMin - rayPos) / rayDir
        t2   = (aabbMax - rayPos) / rayDir
        tmin = foldr1 max $ liftI2 min t1 t2
        tmax = foldr1 min $ liftI2 max t1 t2

这是一个通常的Ray→AABB交集函数,它非常简单清晰,除了类型签名,几乎比函数本身还要大!有人建议我可以使用“封装我的需求”的种类限制来使其更简洁,但我找不到适当的种类限制来“封装我的需求”。在这种情况下,“我的需求”基本上是“该类型表现得像数字应该那样”。因此,在我的想法中,以下内容是有意义的:

class Real a where
    ... anything I want a real number to do ...

instance Real Float where
    ...

instance (Real a) => Real (V2 a) where
    ...

instance (Real a) => Real (V3 a) where
    ...

type AABB a = V2 a
type Ray a  = V2 a

type Box2DFloat = AABB (V2 Float)
type Box4DFloat = AABB (V4 Float)
type Ray2D a = Ray (V2 a)
type Ray3DRatio = Ray (V3 Ratio)
... etc ...

那么,我的签名将变得非常简单:

intersectAABB :: (Real r, Real s) => Ray r -> AABB r -> [s]

这看起来好多了。但是,如果没有使用Haskell的人费心定义这样一个类,那么一定存在某种原因。为什么没有"Real"类?如果定义这样一个类是个坏主意,那么解决我的问题的正确方法是什么?

2个回答

6

使用约束同义词:

{-# LANGUAGE ConstraintKinds #-}

type ConstraintSynonym f a = (
  Foldable f,
  Metric f,
  Ord a, 
  Num (f a), 
  Fractional (f a), 
  Floating a)

使用 ConstraintKinds,可以使用升级的元组来表示约束条件的连接(() 可以表示满足约束条件)。现在可以在注释中使用 ConstraintSynonym 代替一大堆约束条件的元组。


1
谢谢,我可以问一个小问题吗?这个库中有一些使用不同约束条件的函数,但都有些相似。所以,您建议我应该使用一个涵盖所有内容的单一同义词,还是每个函数一个,覆盖确切的函数需求?在习惯用语上应该如何称呼它? - MaiaVictor
我认为同义词应该以尽可能多的方式定义。单一使用的同义词是不好的。如果有多个略有不同的同义词,您可以尝试找到一些统一的模式或原则,并使用类型族来生成约束(类型族也可以返回“约束”)。 - András Kovács

2
您的问题可以通过使用^-^liftI2(/)来解决,而不是分别使用-/。这将减少约束条件到(Foldable f, Metric f, Ord a, Floating a)
使用ConstraintKinds,您可以创建常量别名以减少混乱:
type OrderedField a = (Ord a, Floating a)
type FoldableMetric f = (Foldable f, Metric f)

因此,约束条件变为(FoldableMetric f, OrderedField a)。您也可以通过创建虚拟类而无需使用ConstraintKinds来实现此操作:

class (Ord a, Floating a) => OrderedField a
instance (Ord a, Floating a) => OrderedField a

但是这需要使用 UndecidableInstances

向量并不是真正的数字。你不能为它们编写合理的 OrdReal 实例(甚至是否应该有一个 Num 实例也是有争议的)。我认为保持向量和数字类型分开是一件好事。


向量何时不像数字一样运作? - MaiaVictor
你如何为向量执行 toRational :: Real a => a -> Rational 操作?你可以对每个分量进行一些操作,但它们并不总是在向量的几何解释中有意义,比如乘法。通常当你“乘”两个向量时,是指点积或叉积。 - cchalmers
不太信服,我需要比那更详细的阐述,但当然这超出了问题的范围。谢谢! - MaiaVictor
注意:为了安全起见,将此问题转移到数学SO。请注意... - MaiaVictor
@Viclib 那复数怎么办?(由于某些原因,Complex 没有 Metric 实例,但你可以根据 Additive 编写函数。使用当前的定义是没有意义的,但如果你使用 liftI2 (/) 进行分量除法,则可以工作。我建议你坚持使用标准的“向量”符号(^+^ 等),它们存在的理由很好,而且你会得到更好的类型签名(Num (f a) 很丑)。 - cchalmers
@Viclib,向量不是数字的一种简单方式在于“乘法”的类型。向量具有缩放(标量向量->向量)和内积(向量向量->标量),但没有乘法(向量*向量->向量)。 - luqui

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接