【问题标题】:How to parse row-polymorphic records with SimpleJSON in PureScript?如何在 PureScript 中使用 SimpleJSON 解析行多态记录?
【发布时间】:2020-11-18 19:56:46
【问题描述】:

我编写了一个实用程序类型和函数,旨在帮助解析某些行多态类型(具体而言,在我的例子中,任何扩展 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 应该是什么类型?为什么?
  • @FyodorSoikin recBase 应该有类型 Resource。在我更大的生产示例中,我在添加 readIdTypePair 功能之前验证了这种情况(编译工作)。

标签: json purescript row-polymorphism


【解决方案1】:

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

编译器有两个参考点来确定recBase 的类型:(1) recBase.identifiersreadIdTypePair 一起使用这一事实以及(2) readRecordJSON 的返回类型。

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

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

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

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

recBase :: { identifiers :: a }

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

嗯,是的,确实如此,但诀窍是:recBase 的类型不必是 ResourcereadRecordJSON 的返回类型是Resource,但是在recBasereadRecordJSON 的返回类型之间是一个记录更新操作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 的返回类型满足,但r 的行仍然未知。


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

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

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

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

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

type Resource = {
  identifiers :: Array Identifier
}

如果您出于审美原因这样做,我不反对。但如果你不知道 - 现在你知道了:-)

【讨论】:

  • 单独的选项对我更​​有吸引力,recBase :: Resource &lt;- JSON.readJSON' jsStr,但我认为这也会在现场演示中产生单独的类型错误(如果我遗漏了什么,请纠正我)。
  • 我认为第三个选项,如果在 PureScript 中有办法做到这一点,那就是类型应用程序(也许通过将 readIdTypePair 更改为使用 SProxy);我认为这仍然允许 readIdTypePair 是多态的。
  • 关于您直接使用 Record 语法的建议,我认为我的疯狂有一个方法。我发现当您需要重用行类型时,您似乎无法从 Record 类型中获取底层行类型,因此我将这些行类型与 purescript-option 库一起使用,这样我就可以拥有相同基础行类型的选项和记录。这可能属于不明显的合理默认值的范畴,并且提醒我很多给数据类型一个额外的类型构造函数来包装每个构造函数(忘记了这个技巧在例如 Haskell 中被称为什么)。
  • (这就是我所说的数据类型:youtube.com/watch?v=BHjIl81HgfE
  • 你是对的,选项2有一个错误:我一直在说recBase的类型不是Resource,然后就直接这样声明了?‍ ♀️。我已经更新了答案。
猜你喜欢
  • 1970-01-01
  • 2015-06-04
  • 2019-03-08
  • 1970-01-01
  • 2016-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多