我的程序中有两个功能:
getWidth :: Size -> GLint
getWidth (Size a b) = a
getXPos :: Position -> GLint
getXPos (Position a b) = a
我意识到这两个函数在做同样的事情,唯一的区别是参数类型。问题是:我如何编写这样一个通用函数:
getFirst :: ANYTHING -> a
getFirst (ANYTHING a b) -> a
我的程序中有两个功能:
getWidth :: Size -> GLint
getWidth (Size a b) = a
getXPos :: Position -> GLint
getXPos (Position a b) = a
我意识到这两个函数在做同样的事情,唯一的区别是参数类型。问题是:我如何编写这样一个通用函数:
getFirst :: ANYTHING -> a
getFirst (ANYTHING a b) -> a
对于您的问题,这可能有点矫枉过正,但也许对于偶然发现这个问题的其他人会很有用。
通过使用GHC 的泛型编程,您可以实现一个真正的泛型函数,该函数适用于具有单个构造函数和两个字段的任何数据类型。
我们先来看看类型签名。您想编写一个函数,例如
getFirst :: ANYTHING -> a
在 Haskell 中,可以是“任何东西”的类型用类型变量表示(就像结果类型一样a
),所以让我们写
getFirst :: t -> a
然而,拥有一个完全多态的类型不允许我们以任何方式操作该类型,因为我们无法对其内部结构做出任何假设。因此我们需要写一些关于类型的约束t
。
第二件事是多态返回类型(a
上述)意味着返回类型是根据调用站点推断的,本质上意味着调用者能够“请求”第一个字段的任何可能类型。这显然是不可能的,因为例如Size
唯一有效的返回类型是GLint
. 所以我们需要声明返回类型,使它依赖于类型t
。
getFirst :: (Generic t, GPair (Rep t)) => t -> FirstT (Rep t)
现在,这是一个相当复杂的类型签名,但本质是,对于任何t
泛型且具有Rep t
有效泛型对 ( GPair
) 的泛型表示的任何类型,我们都可以访问具有 type 的对的第一个字段FirstT (Rep t)
。
类型类GPair
可以这样定义
class GPair g where
type FirstT g -- type of the first field in the pair
type SecondT g -- type of the second field in the pair
gGetFirst :: g x -> FirstT g
gGetSecond :: g x -> SecondT g
这个类型类引入了函数gGetFirst
,gGetSecond
它不作用于对类型本身,而是作用于它的泛型表示。类型声明FirstT
和SecondT
所谓的关联类型同义词是TypeFamilies语言扩展的一部分。我们在这里声明的是,FirstT
并且SecondT
是由 type 确定的某些现有的、未知类型的同义词g
。
类型的通用表示包含在元数据描述中,其中包含数据类型名称、构造函数名称、记录字段名称等信息。在这种情况下,我们不需要任何这些信息,所以第一个实例GPair
简单剥离元数据层。
instance GPair f => GPair (M1 i c f) where
type FirstT (M1 i c f) = FirstT f
type SecondT (M1 i c f) = SecondT f
gGetFirst = gGetFirst . unM1
gGetSecond = gGetSecond . unM1
接下来,我们需要为具有两个字段的通用构造函数创建一个实例。
instance (GField l, GField r) => GPair (l :*: r) where
type FirstT (l :*: r) = FieldT l
type SecondT (l :*: r) = FieldT r
gGetFirst (l :*: _) = gGet l
gGetSecond (_ :*: r) = gGet r
然后我们定义了通用字段类型类GField
,它作用于该对的单个字段。
class GField g where
type FieldT g
gGet :: g x -> FieldT g
GField
我们像上面那样剥离元数据层
instance GField f => GField (M1 i c f) where
type FieldT (M1 i c f) = FieldT f
gGet = gGet . unM1
现在我们只需要为通用构造函数字段添加一个实例。
instance GField (K1 r t) where
type FieldT (K1 r t) = t
gGet (K1 x) = x
现在我们可以实现真正通用的访问器函数getFirst
和getSecond
.
getFirst :: (Generic t, GPair (Rep t)) => t -> FirstT (Rep t)
getFirst = gGetFirst . from
getSecond :: (Generic t, GPair (Rep t)) => t -> SecondT (Rep t)
getSecond = gGetSecond . from
该函数from
是其中的一部分,GHC.Generics
它将值转换为其通用形式。为此,需要实现数据类型Size
和类型类。Position
Generic
{-# LANGUAGE DeriveGeneric #-}
data Position = Position GLInt GLInt deriving Generic
data Size = Size GLInt GLInt deriving Generic
让我们测试一下:
> let sz = Size 1 2
> let pos = Position 4 6
> getFirst sz
1
> getSecond pos
6
这些函数还可以自动为适当的内置类型工作,例如元组:
> getSecond (1, "foo")
"foo"
现在,您可能会认为对于一个简单、通用的函数来说,这是非常多的代码,这是一个有效的关注点。但是,在实践中,一旦您熟悉了泛型表示类型的结构,泛型实例的编写就相当容易和快速。
此外,GHC 的泛型编程的优点在于它是完全类型安全的(例如,与 Java 中的反射 API 不同)。这意味着如果您尝试使用具有不兼容类型的泛型函数,您会得到编译时错误而不是运行时异常。
例如:
a = getFirst (1,2,3) -- compile error because value has more than two fields
data Foo = Foo Int Int | Bar Float Float deriving Generic
b = getFirst $ Foo 1 2 -- compile error because the type has multiple constuctors
这是尝试此操作的完整代码:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics
class GPair g where
type FirstT g
type SecondT g
gGetFirst :: g x -> FirstT g
gGetSecond :: g x -> SecondT g
instance GPair f => GPair (M1 i c f) where
type FirstT (M1 i c f) = FirstT f
type SecondT (M1 i c f) = SecondT f
gGetFirst = gGetFirst . unM1
gGetSecond = gGetSecond . unM1
instance (GField l, GField r) => GPair (l :*: r) where
type FirstT (l :*: r) = FieldT l
type SecondT (l :*: r) = FieldT r
gGetFirst (l :*: _) = gGet l
gGetSecond (_ :*: r) = gGet r
class GField g where
type FieldT g
gGet :: g x -> FieldT g
instance GField f => GField (M1 i c f) where
type FieldT (M1 i c f) = FieldT f
gGet = gGet . unM1
instance GField (K1 r t) where
type FieldT (K1 r t) = t
gGet (K1 x) = x
getFirst :: (Generic t, GPair (Rep t)) => t -> FirstT (Rep t)
getFirst = gGetFirst . from
getSecond :: (Generic t, GPair (Rep t)) => t -> SecondT (Rep t)
getSecond = gGetSecond . from
您需要一个类型类(尽管 IMO 概括这两个功能不是一个好主意):
class Dimension d where
getX :: d -> GLint
getY :: d -> GLint
instance Dimension Size where
getX (Size x y) = x
getY (Size x y) = y
instance Dimension Position where
getX (Position x y) = x
getY (Position x y) = y
如果您只想编写更少的代码,请使用记录语法:
data Size = Size { getWidth :: GLint, getHeight :: GLint }
data Position = Position { getXPos :: GLint, getYPos :: GLint }