Это, вероятно, немного излишним для вашей проблемы, но, возможно, это будет полезно Ф.О. кто-то другой, который наткнулся на этот вопрос.
Вы можете реализовать действительно универсальную функцию, которая работает с любым типом данных, который имеет один конструктор с двумя полями, используя 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
Этого типа класс вводит функцию gGetFirst
и gGetSecond
, которые не работать на самом типе пары, но его общего представление.Тип delcarations FirstT
и SecondT
являются так называемыми ассоциированными типами синонимов, которые являются частью расширения языка TypeFamilies. То, что мы заявляем здесь , состоит в том, что 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
Далее нам нужно сделать экземпляр для общего конструктора с двумя полями.
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
и 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 заключается в том, что он полностью безопасен по типу (в отличие от, например, API-интерфейсов отражения в Java). Это означает, что если вы попытаетесь использовать общие функции с несовместимыми типами, вы получите ошибку времени компиляции вместо исключение во время выполнения.
Например:
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