Единични елементи, семейства от типове и екзистенциални типове за екземпляр на 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) containt

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