简体   繁体   English

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

[英]Template data type in function definition

I have two functions in my program: 我的程序中有两个函数:

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

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

I realized that those two functions are doing the same thing and the only difference is parameter type. 我意识到这两个函数正在做同样的事情,唯一的区别是参数类型。 Question is: how do i write such a generic function: 问题是:我如何编写这样的通用函数:

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

This is probably a little bit overkill for your problem, but maybe it'll be useful for someone else that stumbles upon this question. 这可能对你的问题有点过分,但也许对于偶然发现这个问题的其他人来说它会有用。

You can implement a truly generic function that works on any datatype that has a single constructor with two fields by using GHC's generic programming . 您可以使用GHC的通用编程实现一个真正的通用函数,该函数适用于具有两个字段的单个构造函数的任何数据类型。

Let's look at the type signature first. 我们先来看一下类型签名。 You'd like to write a function such as 你想写一个像

getFirst :: ANYTHING -> a

In Haskell, a type that can be "anything" is signified with a type variable (just like the result type a ), so let's write 在Haskell中,可以是“任何东西”的类型用类型变量表示(就像结果类型a ),所以让我们写一下

getFirst :: t -> a

However, having a fully polymorphic type doesn't let us operate on the type in any way since we can't make any assumptions about its internal structure. 但是,具有完全多态类型不允许我们以任何方式对类型进行操作,因为我们无法对其内部结构做出任何假设。 Therefore we need to write in some constraints about the type t . 因此,我们需要写一些关于类型t约束。

The second thing is that a polymorphic return type (the a above) means that the return type is inferred based on the call site, essentially meaning that the caller is able to "request" any possible type for the first field. 第二件事是多态返回类型(上面的a )意味着返回类型是基于调用站点推断的,本质上意味着调用者能够“请求”第一个字段的任何可能类型。 This is clearly impossible, since for example for Size the only valid return type is GLint . 这显然是不可能的,因为例如对于Size ,唯一有效的返回类型是GLint So we need to declare the return type so that it depends on the type t . 所以我们需要声明返回类型,以便它取决于类型t

getFirst :: (Generic t, GPair (Rep t)) => t -> FirstT (Rep t)

Now, this is a rather complicated type signature, but the essence is that for any type t that is generic and has a generic representation Rep t that is a valid, generic pair ( GPair ), we can access the first field of the pair which has the type FirstT (Rep t) . 现在,这是一个相当复杂的类型签名,但实质是,任何类型的t是通用的,具有通用表示Rep t ,是一个有效的,通用对( GPair ),我们可以访问对的第一个字段,具有FirstT (Rep t)类型。

The type-class GPair can be defined like this 类型级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

This type-class introduces the function gGetFirst and gGetSecond that do not operate on the pair type itself but its generic representation. 此类型类引入了函数gGetFirstgGetSecond ,它们不对对类型本身进行操作,而是对其通用表示进行操作。 The type delcarations FirstT and SecondT are so called associated type synonyms that are part of the TypeFamilies language extension. 类型转换FirstTSecondT是所谓的关联类型同义词,它们是TypeFamilies语言扩展的一部分。 What we declare here is that FirstT and SecondT are a synonym for some existing, unknown type that is determined by the type g . 我们在这里声明的是, FirstTSecondT是由g类型确定的某些现有的未知类型的同义词。

The generic representations of types are wrapped in meta-data descriptions that contain information such as the data type name, constructor names, record field names etc. We are not going to need any of that information for this case, so the first instance of GPair simply strips out the meta-data layer. 类型的通用表示包含在元数据描述中,其中包含诸如数据类型名称,构造函数名称,记录字段名称等信息。对于这种情况,我们不需要任何这些信息,因此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

Next we need to make an instance for the generic constuctor with two fields. 接下来,我们需要为具有两个字段的通用构造函数创建一个实例。

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

And then we define the generic field type-class GField which operates on a single field of the pair. 然后我们定义通用字段类型类GField ,它对该对的单个字段进行操作。

class GField g where
    type FieldT g

    gGet :: g x -> FieldT g

We strip out the meta-data layer from GField as we did above 我们像上面那样从GField删除了元数据层

instance GField f => GField (M1 i c f) where
    type FieldT (M1 i c f) = FieldT f

    gGet = gGet . unM1

And now we just need to add an instance for generic constructor fields. 现在我们只需要为泛型构造函数字段添加一个实例。

instance GField (K1 r t) where
    type FieldT (K1 r t) = t

    gGet (K1 x) = x

Now we can implement the truly generic accessor functions getFirst and getSecond . 现在我们可以实现真正的通用访问器函数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

The function from is part of GHC.Generics and it converts a value to its generic form. 函数fromGHC.Generics一部分, GHC.Generics值转换为通用形式。 For this, the data types Size and Position need to implement the Generic type-class. 为此,数据类型SizePosition需要实现Generic类类。

{-# LANGUAGE DeriveGeneric #-}

data Position = Position GLInt GLInt deriving Generic
data Size     = Size GLInt GLInt deriving Generic

Let's test it out: 我们来测试一下:

> let sz = Size 1 2
> let pos = Position 4 6
> getFirst sz
1
> getSecond pos
6

The functions also work automatically for appropriate built-in types, such as tuples: 这些函数也可以自动适用于适当的内置类型,例如元组:

> getSecond (1, "foo")
"foo"

Now, you might think that this is an awful lot of code for a simple, generic function and that's a valid concern. 现在,您可能会认为这是一个简单的泛型函数的大量代码,这是一个有效的问题。 However, in practice the generic instances are rather easy and quick to write once you are familiar with how the generic representation types are structured. 但是,在实践中,一旦熟悉了泛型表示类型的结构,通用实例就可以轻松快速地编写。

Also, the great thing about GHC's generic programming is that it's completely type-safe (unlike, for example, the reflection APIs in Java). 此外,GHC通用编程的优点在于它完全是类型安全的(例如,与Java中的反射API不同)。 This means that if you try to use the generic functions with incompatible types, you get a compile time error instead of a run-time exception. 这意味着如果您尝试使用不兼容类型的泛型函数,则会出现编译时错误而不是运行时异常。

For example: 例如:

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

Here's the complete code for trying this out: 这是尝试这个的完整代码:

{-# 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

You need a type class (although IMO it isn't a good idea to generalize these two functions): 你需要一个类型类 (尽管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

If you just want to write less code, employ record syntax : 如果您只想编写更少的代码,请使用记录语法

data Size = Size { getWidth :: GLint, getHeight :: GLint }
data Position = Position { getXPos :: GLint, getYPos :: GLint }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM