Одиночки, семейства типов и экзистенциальные типы для экземпляра FromJSON

Вероятно, проще сначала кратко изложить мою общую проблему, а затем показать, где я застрял.

Я хочу получить список JSON некоторого одноэлементного индексированного типа, где тип индексации также имеет связанное семейство типов. В коде:

data MyType = MyValue1 | MyValue2
type family MyFamily (mt :: MyType) where
    MyFamily MyValue1 = Int
    MyFamily MyValue2 = Double
data InputType (mt :: MyType) = InputNoFamily | InputWithFamily (MyFamily mt)
data OutputType (mt :: MyType) = OutputNoFamily | OutputWithFamily (MyFamily mt)

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

JSON -> [Some InputType] -> [Some OutputType] -> JSON

где Some из пакета exinst, но также переопределен ниже. Я могу разобрать JSON в том случае, если я не разбираю MyFamily mt, но я не могу придумать лучший способ включить его синтаксический анализ и из JSON.

То, что у меня есть до сих пор, ниже:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE RankNTypes #-}

module SO where

import Data.Aeson
import Data.Singletons.TH
import GHC.Generics

$(singletons [d|
  data MyType
    = MyValue1
    | MyValue2
    | MyValue3
    deriving (Show, Eq, Generic)
  |])
instance FromJSON MyType

type family MyFamily (mt :: MyType) :: * where
  MyFamily 'MyValue1 = Double
  MyFamily 'MyValue2 = Double
  MyFamily 'MyValue3 = Int

-- stolen from exinst package
data Some (f :: k -> *) =
    forall a. Some (Sing a) (f a)

some :: forall (f :: k -> *) a. SingI a => f a -> Some f
some = Some (sing :: Sing a)

withSome :: forall (f :: k -> *) (r :: *). Some f -> (forall a. SingI a => f a -> r) -> r
withSome (Some s x) g = withSingI s (g x)

data MyCompoundType (mt :: MyType)
    = CompoundNoIndex
    | CompoundWithIndex (MyFamily mt)

deriving instance (Show (SMyType mt), Show (MyFamily mt)) => Show (MyCompoundType mt)

-- instance with no parsing of `MyFamily`
instance
  forall (mt :: MyType).
  ( SingKind (KindOf mt)
  , FromJSON (DemoteRep (KindOf mt))
  ) => FromJSON (Some MyCompoundType) where
    parseJSON = withObject "MyCompoundType" $ \o -> do
      mt :: MyType <- o .: "myType"
      case toSing mt of
        SomeSing (smt :: SMyType mt') -> case smt of
          SMyValue1 -> return $ some (CompoundNoIndex :: MyCompoundType mt')
          SMyValue2 -> return $ some (CompoundNoIndex :: MyCompoundType mt')
          SMyValue3 -> return $ some (CompoundNoIndex :: MyCompoundType mt')

Очевидно, мне нужно добавить ограничение FromJSON (MarketIndex mt), но мне также нужно иметь возможность привязать его к Some CompoundType, для которого я создаю экземпляр.

Простое добавление константы FromJSON (MyFamily mt)

instance
  forall (mt :: MyType).
  ( SingKind (KindOf mt)
  , FromJSON (DemoteRep (KindOf mt))
  , FromJSON (MyFamily mt)
  ) => FromJSON (Some MyCompoundType) where
    parseJSON = undefined

выдает ошибки неоднозначного типа

Could not deduce (FromJSON (MyFamily mt0))
  arising from the ambiguity check for an instance declaration
from the context (SingKind (KindOf mt),
                  FromJSON (DemoteRep (KindOf mt)),
                  FromJSON (MyFamily mt))
  bound by an instance declaration:
             (SingKind (KindOf mt), FromJSON (DemoteRep (KindOf mt)),
              FromJSON (MyFamily mt)) =>
             FromJSON (Some MyCompoundType)
  at SO.hs:(57,3)-(61,39)
The type variable ‘mt0’ is ambiguous
In the ambiguity check for:
  forall (mt :: MyType).
  (SingKind (KindOf mt), FromJSON (DemoteRep (KindOf mt)),
   FromJSON (MyFamily mt)) =>
  FromJSON (Some MyCompoundType)
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
In the instance declaration for ‘FromJSON (Some (MyCompoundType))’

Я вижу, что проверка типов, говорящая о mt0, а не mt, является большой проблемой, но я не знаю, как уговорить ее ожидать тип mt в правой части ограничения.

(Я также понимаю, что не включил FromJSON (MyFamily mt) экземпляров, но если средство проверки типов не может определить mt ~ mt0, я не думаю, что в настоящее время это имеет значение).

Надеюсь, есть исправление?

Я потратил разумное количество времени, пробуя разные вещи, но происходит довольно много разных вещей (синглетоны, экзистенциалы и т. д.). Я медленно довожу себя до определенного уровня мастерства, но у меня просто недостаточно знаний или опыта, чтобы быть уверенным, как они (или нет) способствуют возникновению проблемы.


person dbeacham    schedule 08.10.2015    source источник


Ответы (2)


(Мой предыдущий ответ на ваш предыдущий вопрос в значительной степени применим здесь).

Вы можете анализировать любой тип, который хотите, вам просто нужно продемонстрировать, что конкретный тип имеет экземпляр FromJSON. В этом случае вам следует проанализировать конкретные типы результатов MyFamily, потому что все они имеют соответствующие экземпляры.

instance FromJSON (Some MyCompoundType) where
    parseJSON = withObject "MyCompoundType" $ \o -> do
      cons :: String <- o .: "constructor"
      mt :: MyType <- o .: "myType"
      case toSing mt of
        SomeSing smt ->
          case cons of
            "CompoundNoIndex" -> pure $ Some smt CompoundNoIndex
            "CompoundWithIndex" -> case smt of
              SMyValue1 -> Some SMyValue1 . CompoundWithIndex <$> o .: "field"
              SMyValue2 -> Some SMyValue2 . CompoundWithIndex <$> o .: "field"
              SMyValue3 -> Some SMyValue3 . CompoundWithIndex <$> o .: "field"

Здесь я предположил, что есть что-то, что указывает на закодированный конструктор. Конечно, существует множество альтернативных форматов кодирования и декодирования.

В качестве альтернативы мы можем собрать воедино аппроксимацию количественных ограничений и больше использовать одноэлементный тег, проанализированный из поля "myType":

import Data.Constraint -- from "constraints"
import Data.Proxy

data MyFamilySym :: TyFun MyType * -> *
type instance Apply MyFamilySym a = MyFamily a  

class ForallInst (f :: TyFun k * -> *) (c :: * -> Constraint) where
  allInst :: Proxy '(f, c) -> Sing x -> Dict (c (f @@ x))

instance ForallInst MyFamilySym FromJSON where
  allInst _ SMyValue1 = Dict
  allInst _ SMyValue2 = Dict
  allInst _ SMyValue3 = Dict  

instance FromJSON (Some MyCompoundType) where
    parseJSON = withObject "MyCompoundType" $ \o -> do
      cons :: String <- o .: "constructor"
      SomeSing smt <- toSing <$> o .: "myType"
      case cons of
        "CompoundNoIndex" -> pure (Some smt CompoundNoIndex)
        "CompoundWithIndex" ->
          case allInst (Proxy :: Proxy '(MyFamilySym, FromJSON)) smt of
            Dict -> Some smt . CompoundWithIndex <$> o .: "field" 

Ключевым моментом здесь является дефункционализация с помощью MyFamilySym и Apply. Это позволяет нам эффективно помещать MyFamily в заголовки экземпляров, что в противном случае было бы запрещено GHC. Подробнее о дефункционализации в singletons.

При использовании количественных экземпляров над семействами типов мы никогда не сможем избежать одной вещи: записи всех случаев семейства типов и демонстрации экземпляра для каждого случая. Решение ForallInst также делает это, но, по крайней мере, требует, чтобы мы выписали случаи только один раз.

person András Kovács    schedule 08.10.2015
comment
Спасибо - в прошлый раз я в основном пропустил материал по дефункционализации, так как мне нужно было быстро что-то заставить работать. Полученный код был немного уродливым, поэтому я пересматриваю свою реализацию. Тем не менее, сегодня я внимательно изучил сообщение о дефункционализации и думаю, что понимаю, что происходит. Но знаете ли вы о каких-либо других хороших ресурсах (библиотеки/сообщения в блогах/SO ответы/и т. д.), которые используют его? Я пытаюсь лучше понять синглтоны в целом, но без реальных примеров использования может быть немного сложно проверить мое понимание. - person dbeacham; 10.10.2015
comment
@dbeacham: я не знаю хороших ресурсов для этого. Должно быть, потому что я несколько раз писал очень перекрывающиеся ответы SO о синглтонах, и это становится немного утомительным. Единственный раз, когда я видел дефункционализацию в дикой природе, был в (ныне устаревшем) Vinyl 4.x. . Я лично подхожу к истории синглетонов в Haskell с точки зрения Agda/Coq, поэтому все, что я обычно делаю, это пытаюсь применить обычные шаблоны программирования с зависимой типизацией в условиях ограничений GHC. - person András Kovács; 13.10.2015

Я не очень хорошо знаком с синглтонами, но все же замечаю здесь возможное недоразумение:

В вашем текущем экземпляре часть

forall (mt :: MyType).
  ( SingKind (KindOf mt)
  , FromJSON (DemoteRep (KindOf mt))
  ) =>

вообще не используется. Файл компилируется так же хорошо, если вы его удалите.

Мне кажется, что вы пытаетесь установить ограничение, в котором говорится, что «для всех типов вида MyType эти экземпляры должны существовать». К сожалению, такая функция (иногда называемая «количественными ограничениями» или «ограничениями ранга n») в настоящее время не поддерживается GHC (и Саймон П.Дж., который был соавтором статьи, впервые предложившей ее, официально заявил, что понятия не имеет как реализовать вывод типа для него.)

Я предполагаю, что ваша модифицированная версия не работает, потому что там вам действительно делают нужны количественные ограничения для части FromJSON (MyFamily mt).

У меня есть предчувствие, которое, я надеюсь, может помочь. (К сожалению, я недостаточно понимаю в использовании синглетонов, чтобы написать реальную попытку решения.) Что, если вы замените некоторые из своих типов на GADT? например.:

data MyCompoundType (mt :: MyType) where
    CompoundNoIndex :: MyCompoundType mt
    CompoundWithIndex :: FromJSON (MyFamily mt) => MyCompoundType mt

Таким образом, MyCompoundType может сам носить с собой требуемый экземпляр.

person Ørjan Johansen    schedule 08.10.2015
comment
Спасибо, что заметили неиспользуемые ограничения — я упростил реализацию exinst для своих конкретных нужд, но не заметил, что сделал их излишними. - person dbeacham; 10.10.2015