Как анализировать полиморфные записи строк с помощью 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
}
Если вы делаете это из эстетических соображений, у меня нет возражений. Но если вы не знали - теперь вы знаете:-)