2

我的程序中有两个功能:

getWidth :: Size -> GLint
getWidth (Size a b) = a

getXPos :: Position -> GLint
getXPos (Position a b) = a

我意识到这两个函数在做同样的事情,唯一的区别是参数类型。问题是:我如何编写这样一个通用函数:

getFirst :: ANYTHING -> a
getFirst (ANYTHING a b) -> a
4

2 回答 2

7

对于您的问题,这可能有点矫枉过正,但也许对于偶然发现这个问题的其他人会很有用。

通过使用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

这个类型类引入了函数gGetFirstgGetSecond它不作用于对类型本身,而是作用于它的泛型表示。类型声明FirstTSecondT所谓的关联类型同义词是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

现在我们可以实现真正通用的访问器函数getFirstgetSecond.

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和类型类。PositionGeneric

{-# 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
于 2013-03-13T11:55:09.043 回答
5

您需要一个类型类(尽管 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 }
于 2013-03-13T09:01:38.803 回答