函数定义中的模板数据类型

时间:2013-03-13 08:31:09

标签: haskell

我的程序中有两个函数:

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

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

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

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

2 个答案:

答案 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

此类型类引入了没有的函数gGetFirstgGetSecond 对类型本身进行操作但是它的通用表示。类型 delcarations FirstTSecondT是所谓的关联类型同义词 是TypeFamilies语言扩展的一部分。我们在这里宣布什么 是FirstTSecondT是某些现有的未知类型的同义词 这由类型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

现在我们可以实现真正的通用访问器函数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

函数fromGHC.Generics的一部分,它将值转换为它 通用形式。为此,需要实现数据类型SizePosition 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 }