我的程序中有两个函数:
getWidth :: Size -> GLint
getWidth (Size a b) = a
getXPos :: Position -> GLint
getXPos (Position a b) = a
我意识到这两个函数正在做同样的事情,唯一的区别是参数类型。问题是:我如何编写这样的通用函数:
getFirst :: ANYTHING -> a
getFirst (ANYTHING a b) -> a
答案 0 :(得分:7)
对于你的问题,这可能有点过分,但也许它会 对偶然发现这个问题的其他人有用。
您可以实现一个真正的通用函数,该函数适用于任何具有的数据类型 一个带有两个字段的构造函数,使用GHC's generic programming。
让我们先看一下类型签名。你想写一个像
这样的函数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
),我们可以访问该对的第一个字段
其类型为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
对类型本身进行操作但是它的通用表示。类型
delcarations FirstT
和SecondT
是所谓的关联类型同义词
是TypeFamilies语言扩展的一部分。我们在这里宣布什么
是FirstT
和SecondT
是某些现有的未知类型的同义词
这由类型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
,它在a上运行
单一领域。
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
答案 1 :(得分:5)
你需要一个type class(尽管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
如果您只想编写更少的代码,请使用record syntax:
data Size = Size { getWidth :: GLint, getHeight :: GLint }
data Position = Position { getXPos :: GLint, getYPos :: GLint }