これはおそらくあなたの問題には少しやり過ぎですが、この質問に出くわした他の誰かにとっては役立つかもしれません.
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 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
し、ペア型自体ではなく、そのジェネリック表現を操作します。型宣言FirstT
とは、 TypeFamilies言語拡張SecondT
の一部である関連型シノニムと呼ばれます。ここで宣言しているのは、とが type によって決定される既存の未知の型の同義語であることです。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
次に、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