Как анализировать полиморфные записи строк с помощью SimpleJSON в PureScript?

Я написал служебный тип и функцию, предназначенную для помощи в синтаксическом анализе определенных строково-полиморфных типов (в частности, в моем случае, все, что расширяет BaseIdRows:

type IdTypePairF r = (identifier :: Foreign, identifierType :: Foreign | r)


readIdTypePair :: forall r. Record (IdTypePairF r) -> F Identifier
readIdTypePair idPairF = do
  id <- readNEStringImpl idPairF.identifier
  idType <- readNEStringImpl idPairF.identifierType
  pure $ {identifier: id, identifierType: idType}

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

  No type class instance was found for

    Prim.RowList.RowToList ( identifier :: Foreign
                           , identifierType :: Foreign
                           | t3
                           )
                           t4

  The instance head contains unknown type variables. Consider adding a type annotation.

while applying a function readJSON'
  of type ReadForeign t2 => String -> ExceptT (NonEmptyList ForeignError) Identity t2
  to argument jsStr
while checking that expression readJSON' jsStr
  has type t0 t1
in value declaration readRecordJSON

where t0 is an unknown type
      t1 is an unknown type
      t2 is an unknown type
      t3 is an unknown type
      t4 is an unknown type

У меня есть живой суть, которая демонстрирует мою проблему.

Но вот полный пример для потомков:

module Main where

import Control.Monad.Except (except, runExcept)
import Data.Array.NonEmpty (NonEmptyArray, fromArray)
import Data.Either (Either(..))
import Data.HeytingAlgebra ((&&), (||))
import Data.Lazy (Lazy, force)
import Data.Maybe (Maybe(..))
import Data.Semigroup ((<>))
import Data.String.NonEmpty (NonEmptyString, fromString)
import Data.Traversable (traverse)
import Effect (Effect(..))
import Foreign (F, Foreign, isNull, isUndefined)
import Foreign as Foreign
import Prelude (Unit, bind, pure, ($), (>>=), unit)
import Simple.JSON as JSON

main :: Effect Unit
main = pure unit


type ResourceRows = (
  identifiers :: Array Identifier
)
type Resource = Record ResourceRows

type BaseIdRows r = (
  identifier :: NonEmptyString
, identifierType :: NonEmptyString
| r
)
type Identifier = Record (BaseIdRows())

-- Utility type for parsing
type IdTypePairF r = (identifier :: Foreign, identifierType :: Foreign | r)



readNEStringImpl :: Foreign -> F NonEmptyString
readNEStringImpl f = do
  str :: String <- JSON.readImpl f
  except $ case fromString str of
    Just nes -> Right nes
    Nothing -> Left $ pure $ Foreign.ForeignError
      "Nonempty string expected."

readIdTypePair :: forall r. Record (IdTypePairF r) -> F Identifier
readIdTypePair idPairF = do
  id <- readNEStringImpl idPairF.identifier
  idType <- readNEStringImpl idPairF.identifierType
  pure $ {identifier: id, identifierType: idType}

readRecordJSON :: String -> Either Foreign.MultipleErrors Resource
readRecordJSON jsStr = runExcept do
  recBase <- JSON.readJSON' jsStr
  --foo :: String <- recBase.identifiers -- Just comment to check inferred type
  idents :: Array Identifier <- traverse readIdTypePair recBase.identifiers
  pure $ recBase { identifiers = idents }

1 ответ

Решение

Ваша проблема в том, что recBase не обязательно типа Resource.

У компилятора есть две точки отсчета для определения типа recBase: (1) тот факт, что recBase.identifiers используется с readIdTypePair и (2) возвращаемый тип readRecordJSON.

С первого момента компилятор может сделать вывод, что:

recBase :: { identifiers :: Array (Record (IdTypePair r)) | p }

для некоторых неизвестных r и p. Тот факт, что он имеет (по крайней мере) поле с именем identifiers происходит из точечного синтаксиса, а тип этого поля определяется readIdTypePairпараметр в сочетании с тем фактом, что idents является Array. Но кроме полей могло быть и больше. identifiers (который представлен p), и каждый элемент identifiers является частичной записью (которая представлена r).

По второму пункту компилятор может сделать вывод, что:

recBase :: { identifiers :: a }

Чего ждать? Почему a и нет ArrayIdentifier? Разве определение Resource четко указать, что identifiers :: ArrayIdentifier?

Да, это так, но вот фокус: тип recBase не должно быть Resource. Тип возврата readRecordJSON является Resource, но между recBase и тип возврата readRecordJSON выдерживает операцию обновления записи recBase { identifiers = idents }, который может изменить тип поля.

Да, обновления записей в PureScript являются плиморфными. Проверь это:

> x = { a: 42 }
> y = x { a = "foo" }
> y
{ a: "foo" }

Посмотрите, как тип x.aизменилось? Вот x :: { a :: Int }, но y :: { a :: String }

И так в вашем коде: recBase.identifiers :: Array (IdTypePairF r) для некоторых неизвестных r, но (recBase { identifiers = idents }).identifiers :: ArrayIdentifier

Тип возврата readRecordJSON доволен, но строка r пока неизвестно.


Чтобы исправить это, у вас есть два варианта. Вариант 1 - сделать readIdTypePair возьмите полную запись, а не частичную:

readIdTypePair :: Record (IdTypePairF ()) -> F Identifier

Вариант 2 - укажите тип recBase явно:

recBase :: { identifiers :: Array (Record (IdTypePairF ())) } <- JSON.readJSON' jsStr

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

type Resource = {
  identifiers :: ArrayIdentifier
}

Если вы делаете это из эстетических соображений, у меня нет возражений. Но если вы не знали - теперь вы знаете:-)

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