Получение экземпляров для данных более высокого класса

Этот вопрос основан на шаблоне данных с более высоким родом, описанном в этом блоге "Разумно-Полиморфный".

В следующем блоке кода я определяю семейство типов HKD и тип данных Personгде поля могут быть либо Maybe или же Identity,

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeFamilies  #-}

import           Data.Aeson

-- "Higher Kinded Data" (http://reasonablypolymorphic.com//blog/higher-kinded-data)
type family HKD f a where
    HKD Identity a = a
    HKD Maybe a = Maybe a

data Person f = Person
  { pName :: HKD f String
  , pAge  :: HKD f Int
  } deriving (Generic)

Затем я пытаюсь вывести ToJSON экземпляр для этого типа.

-- Already provided in imports:
-- instance ToJSON String
-- instance ToJSON Int
-- instance ToJSON a => ToJSON (Maybe a)

instance ToJSON (Person f) where
  toJSON = genericToJSON defaultOptions

К сожалению, я получаю следующую ошибку:

Нет экземпляра для (ToJSON (HKD f Int)) вытекающие из использования genericToJSON,

Учитывая, что у меня уже есть ToJSON Int а также ToJSON (Maybe Int)не должен GHC быть в состоянии получить экземпляр ToJSON (HKD f Int)? Насколько я понимаю, семейства типов действуют как псевдонимы типов в отношении экземпляров. Если это так, то я не могу определить свои собственные экземпляры для него, но он должен получать экземпляры из своих определений, в этом случае Int а также Maybe Int, К сожалению, ошибка, кажется, противоречит этому.

Как я могу определить ToJSON экземпляр для моего типа?

2 ответа

Тип семьи HKD необходимо применить к известному Identity или же Maybe уменьшить. Иначе, HKD f Int с неизвестным f просто застрял, и мы не можем решить HKD f a ограничения для всех типов полей aза исключением перечисления их в контексте, например, (ToJSON (HKD f Int), ToJSON (HKD f String)), что является одним из возможных решений, но не подходит для большого количества полей.

Решение 1. Получите контекст

Основная проблема заключается в утомительности написания и ведения списка ограничений полей, это решается путем того, чтобы отметить, что на самом деле это функция типа записи, и что мы можем определить ее в Haskell с использованием обобщений GHC.

type GToJSONFields a = GFields' ToJSON (Rep a)

-- Every field satisfies constraint c
type family GFields' (c :: * -> Constraint) (f :: * -> *) :: Constraint
type instance GFields' c (M1 i d f) = GFields' c f
type instance GFields' c (f :+: g) = (GFields' c f, GFields' c g)
type instance GFields' c (f :*: g) = (GFields' c f, GFields' c g)
type instance GFields' c U1 = ()
type instance GFields' c (K1 i a) = c a

instance (GToJSONFields (Person f)) => ToJSON (Person f) where
  toJSON = genericToJSON defaultOptions

Однако этот экземпляр является немодулярным и неэффективным, потому что он все еще предоставляет внутреннюю структуру записи (типы полей), и ограничения для каждого отдельного поля должны каждый раз пересматриваться. ToJSON (Person f) используется.

Суть решения 1

Решение 2: обобщить контекст

То, что мы действительно хотим написать в качестве примера, это

instance (forall a. ToJSON a => ToJSON (HKD f a)) => ToJSON (Person f) where
  -- ...

который использует количественное ограничение, новая функция, которая в настоящее время реализуется в GHC; надеюсь, синтаксис является информативным. Но поскольку он еще не выпущен, что мы можем сделать в это время?

Количественное ограничение в настоящее время кодируется с использованием класса типов.

class ToJSON_HKD f where
  toJSON_HKD :: ToJSON a => f a -> Value  -- AllowAmbiguousTypes, or wrap this in a newtype (which we will define next anyway)

instance ToJSON_HKD Identity where
  toJSON_HKD = toJSON

instance ToJSON_HKD Maybe where
  toJSON_HKD = toJSON

Но genericToJSON будет использовать ToJSON на полях, а не ToJSON_HKD, Мы можем обернуть поля в newtype что отправляет ToJSON ограничения с ToJSON_HKD ограничение.

newtype Apply f a = Apply (HKD f a)

instance ToJSON_HKD f => ToJSON (Apply f a) where
  toJSON (Apply x) = toJSON_HKD @f @a x

Поля Person можно только завернуть в HKD Identity или же HKD Maybe, Мы должны добавить еще один случай для HKD, Фактически, давайте сделаем это открытым и реорганизуем случай для конструкторов типов. Мы пишем HKD (Tc Maybe) a вместо HKD Maybe a; это дольше, но Tc тег можно использовать повторно для любого другого конструктора типа, например, HKD (Tc (Apply f)) a,

-- Redefining HKD
type family HKD f a
type instance HKD Identity a = a
type instance HKD (Tc f) a = f a

data Tc (f :: * -> *)  -- Type-level tag for type constructors

Эсон имеет ToJSON1 класс типа, роль которого очень похожа на ToJSON_HKDв качестве кодировки forall a. ToJSON a => ToJSON (f a), по счастливой случайности, Tc это просто правильный тип для подключения этих классов.

instance ToJSON1 f => ToJSON_HKD (Tc f) where
  toJSON1_HKD = toJSON1

Следующим шагом является сама обертка.

wrapApply :: Person f -> Person (Tc (Apply f))
wrapApply = gcoerce

Все, что мы делаем, это оборачиваем поля в newtype (от HKD f a в HKD (Tc (Apply f)) a, который равен Apply f a и представительно эквивалентно HKD f a). Так что это действительно принуждение. К несчастью, coerce не будет проверять здесь, как Person f имеет номинальный параметр типа (потому что он использует HKD, который совпадает с именем f уменьшить). Тем не мение, Person это Generic тип, а также общие представления ввода и ожидаемого вывода wrapApply на самом деле принудительно. Это приводит к следующему "общему принуждению", которое делает wrapApply излишний:

gcoerce :: forall a b
        .  (Generic a, Generic b, Coercible (Rep a ()) (Rep b ()))
        => a -> b
gcoerce = to . (coerce :: Rep a () -> Rep b ()) . from

Мы заключаем: обернуть поля в Applyи использовать genericToJSON,

instance ToJSON_HKD f => ToJSON (Person f) where
  toJSON = genericToJSON defaultOptions . gcoerce @_ @(Person (Tc (Apply f)))

Суть решения 2.

Примечание о сути: HKD был переименован в (@@)имя, заимствованное из синглетонов, и HKD Identity a переписан как HKD Id a, делая явное различие между конструктором типа Identityи нефункционализированный символ Id для функции идентичности. Это выглядит аккуратнее для меня.

Решение 3: без семейства типов

В блоге HKD сочетаются две идеи:

  1. Параметризация записей над конструктором типов f (также называется "функтор функтор паттерн");

  2. обобщающий f быть функцией типа, что возможно, даже несмотря на то, что Haskell не имеет первоклассных функций на уровне типов, благодаря технике дефункционализации.

Основная цель второй идеи - иметь возможность повторно использовать запись Person с развернутыми полями. Это выглядит как косметическая забота о количестве семейств типов сложности.

Если присмотреться поближе, можно утверждать, что на самом деле не так уж много дополнительной сложности. Стоит ли это того? У меня пока нет хорошего ответа.

Просто для справки, вот результат применения описанных выше методов к более простой записи без HKD Тип семьи.

data Person f = Person
  { name :: f String
  , age :: f Int
  }

Мы можем удалить два определения: ToJSON_HKD (ToJSON1 достаточно), и gcoerce (coerce достаточно). Мы заменяем Apply с этим другим подключением нового типа ToJSON а также ToJSON1:

newtype Apply' f a = Apply' (f a)  -- no HKD

instance (ToJSON1 f, ToJSON a) => ToJSON (Apply' f a) where
  toJSON (Apply' x) = toJSON1 x

И мы выводим ToJSON следующее:

instance ToJSON1 f => ToJSON (Person f) where
  toJSON = genericToJSON defaultOptions . coerce @_ @(Person (Apply' f))

Предостережение: специальные типы полей

Эсон имеет возможность сделать Maybe поля необязательные, поэтому они могут отсутствовать в соответствующем объекте JSON. Ну, эта опция не работает с методами, описанными выше. Это влияет только на поля, как известно, Maybe в определении экземпляра, так что сбой для решений 2 и 3 из-за новых типов вокруг всех полей.

Кроме того, для решения 1 это

instance {-# OVERLAPPING #-} ToJSON (Person Maybe) where
  toJSON = genericToJSON defaultOptions{omitNothingFields=True}

будет вести себя иначе, чем специализировать этот другой случай после того, как факт Person Maybe:

instance ... => ToJSON (Person f) where
  toJSON = genericToJSON defaultOptions{omitNothingFields=True}

Автор поста в блоге здесь. Вероятно, самое простое решение здесь - просто мономорфизировать f параметр:

instance ToJSON (Person Identity) where
  toJSON = genericToJSON defaultOptions

instance ToJSON (Person Maybe) where
  toJSON = genericToJSON defaultOptions

Это отчасти уродливо, но, безусловно, доставляется. В настоящее время я работаю в лаборатории, пытаясь найти более общие решения этой проблемы, и сообщу вам, если я что-нибудь придумаю.

Другие вопросы по тегам