如何在 PureScript 中使用 SimpleJSON 解析行多态记录?

How to parse row-polymorphic records with SimpleJSON in 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

我有一个 live gist 可以证明我的问题。

但是,这里是完整的示例,供后代参考:

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 }

你的问题是 recBase 不一定是 Resource 类型。

编译器有两个参考点来确定 recBase 的类型:(1) recBase.identifiersreadIdTypePair 一起使用的事实和 (2) return readRecordJSON.

类型

从第一点编译器可以得出结论:

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

一些未知的 rp。它(至少)有一个名为 identifiers 的字段这一事实来自点语法,该字段的类型来自 readIdTypePair 的参数以及 idents是一个 Array。但是除了identifiers(用p表示)之外还可以有更多的字段,identifiers的每个元素都是一个部分记录(用r表示)。

从第二点编译器可以得出结论:

recBase :: { identifiers :: a }

等等,什么?为什么是 a 而不是 Array IdentifierResource的定义不是明确规定了identifiers :: Array Identifier吗?

嗯,是的,确实如此,但诀窍在于:recBase 的类型不必是 ResourcereadRecordJSON的return类型是Resource,但是readRecordJSONrecBase和return类型之间有一个记录更新操作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 :: Array Identifier

满足readRecordJSON的return类型,但r行还未知


要修复,您有两种选择。选项 1 - 使 readIdTypePair 获取完整记录,而不是部分记录:

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

选项 2 - 明确指定 recBase 的类型:

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

另外,我觉得有必要评论一下您指定记录的怪异方式:首先声明一行,然后从中创建一条记录。仅供参考,可以直接用大括号完成,例如:

type Resource = {
  identifiers :: Array Identifier
}

如果您出于审美原因这样做,我没有异议。但以防万一你不知道 - 现在你知道了:-)