如何在 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.identifiers
与 readIdTypePair
一起使用的事实和 (2) return readRecordJSON
.
类型
从第一点编译器可以得出结论:
recBase :: { identifiers :: Array (Record (IdTypePair r)) | p }
一些未知的 r
和 p
。它(至少)有一个名为 identifiers
的字段这一事实来自点语法,该字段的类型来自 readIdTypePair
的参数以及 idents
是一个 Array
。但是除了identifiers
(用p
表示)之外还可以有更多的字段,identifiers
的每个元素都是一个部分记录(用r
表示)。
从第二点编译器可以得出结论:
recBase :: { identifiers :: a }
等等,什么?为什么是 a
而不是 Array Identifier
? Resource
的定义不是明确规定了identifiers :: Array Identifier
吗?
嗯,是的,确实如此,但诀窍在于:recBase
的类型不必是 Resource
。 readRecordJSON
的return类型是Resource
,但是readRecordJSON
的recBase
和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
}
如果您出于审美原因这样做,我没有异议。但以防万一你不知道 - 现在你知道了:-)
我写了一个实用程序类型和函数,旨在帮助解析某些行多态类型(特别是,在我的例子中,任何扩展 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.identifiers
与 readIdTypePair
一起使用的事实和 (2) return readRecordJSON
.
从第一点编译器可以得出结论:
recBase :: { identifiers :: Array (Record (IdTypePair r)) | p }
一些未知的 r
和 p
。它(至少)有一个名为 identifiers
的字段这一事实来自点语法,该字段的类型来自 readIdTypePair
的参数以及 idents
是一个 Array
。但是除了identifiers
(用p
表示)之外还可以有更多的字段,identifiers
的每个元素都是一个部分记录(用r
表示)。
从第二点编译器可以得出结论:
recBase :: { identifiers :: a }
等等,什么?为什么是 a
而不是 Array Identifier
? Resource
的定义不是明确规定了identifiers :: Array Identifier
吗?
嗯,是的,确实如此,但诀窍在于:recBase
的类型不必是 Resource
。 readRecordJSON
的return类型是Resource
,但是readRecordJSON
的recBase
和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
}
如果您出于审美原因这样做,我没有异议。但以防万一你不知道 - 现在你知道了:-)