2

私のプログラムには2つの関数があります:

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

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

これら 2 つの関数は同じことを行っており、唯一の違いはパラメーターの型であることに気付きました。質問は次のとおりです。このような一般的な関数をどのように書くのですか:

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

2 に答える 2

7

これはおそらくあなたの問題には少しやり過ぎですが、この質問に出くわした他の誰かにとっては役立つかもしれません.

GHC のジェネリック プログラミングを使用して、2 つのフィールドを持つ単一のコンストラクタを持つ任意のデータ型で機能する、真にジェネリックな関数を実装できます。

最初に型シグネチャを見てみましょう。次のような関数を書きたいと思います

getFirst :: ANYTHING -> a

Haskellでは、「なんでも」になりうる型は(結果の型と同じようにa)型変数で表されるので、書きましょう。

getFirst :: t -> a

ただし、完全にポリモーフィックな型を使用しても、その内部構造について想定できないため、型を操作することはできません。したがって、 type に関するいくつかの制約を記述する必要がありますt

2 つ目は、ポリモーフィックな戻り値の型 (a上記) は、呼び出し元に基づいて戻り値の型が推測されることを意味し、基本的に、呼び出し元が最初のフィールドの可能な型を "要求" できることを意味します。たとえばSize、唯一の有効な戻り値の型は であるため、これは明らかに不可能ですGLint。したがって、型に依存するように戻り値の型を宣言する必要がありtます。

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

さて、これはかなり複雑な型シグネチャですが、本質は、ジェネリックであり、有効なジェネリック ペア ( ) でtあるジェネリック表現を持つ任意の型について、型を持つペアの最初のフィールドにアクセスできるということです。Rep tGPairFirstT (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とは、 TypeFamilies言語拡張SecondTの一部である関連型シノニムと呼ばれます。ここで宣言しているのは、とが type によって決定される既存の未知の型の同義語であることです。FirstTSecondTg

型の一般的な表現は、データ型名、コンストラクタ名、レコード フィールド名などの情報を含むメタデータ記述でラップされます。この場合、そのような情報は必要ないので、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

次に、2 つのフィールドを持つジェネリック コンストラクターのインスタンスを作成する必要があります。

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これで、真に汎用的なアクセサ関数andを実装できます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
于 2013-03-13T11:55:09.043 に答える
5

型クラスが必要です(IMO では、これら 2 つの関数を一般化することはお勧めできません)。

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 に答える