I wrote a utility type and function that is meant to aid in parsing certain row-polymorphic types (sepcifically, in my case, anything that extends 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}
When I try to use it, however, it causes the code to get this type error (in my larger code base, things were working fine before I implemented the readIdTypePair
function):
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
I have a live gist that demonstrates my issue.
But, here is the complete example as it stands, for posterity:
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 }
Your problem is that recBase
is not necessarily of type Resource
.
The compiler has two points of reference for determining the type of recBase
: (1) the fact that recBase.identifiers
is used with readIdTypePair
and (2) the return type of readRecordJSON
.
From the first point the compiler can conclude that:
recBase :: { identifiers :: Array (Record (IdTypePair r)) | p }
for some unknown r
and p
. The fact that it has (at least) a field named identifiers
comes from the dot-syntax, and the type of that field comes from readIdTypePair
's parameter combined with the fact that idents
is an Array
. But there could be more fields besides identifiers
(which is represented by p
), and every element of identifiers
is a partial record (which is represented by r
).
From the second point the compiler can conclude that:
recBase :: { identifiers :: a }
Wait, what? Why a
and not Array Identifier
? Doesn't the definition of Resource
clearly specify that identifiers :: Array Identifier
?
Well, yes, it does, but here's the trick: the type of recBase
doesn't have to be Resource
. The return type of readRecordJSON
is Resource
, but between recBase
and return type of readRecordJSON
stands a record update operation recBase { identifiers = idents }
, which can change the type of the field.
Yes, record updates in PureScript are plymorphic. Check this out:
> x = { a: 42 }
> y = x { a = "foo" }
> y
{ a: "foo" }
See how the type of x.a
changed? Here x :: { a :: Int }
, but y :: { a :: String }
And so it is in your code: recBase.identifiers :: Array (IdTypePairF r)
for some unknown r
, but (recBase { identifiers = idents }).identifiers :: Array Identifier
The return type of readRecordJSON
is satisfied, but the row r
is still unknown.
To fix, you have two options. Option 1 - make readIdTypePair
take a full record, not a partial one:
readIdTypePair :: Record (IdTypePairF ()) -> F Identifier
Option 2 - specify the type of recBase
explicitly:
recBase :: { identifiers :: Array (Record (IdTypePairF ())) } <- JSON.readJSON' jsStr
Separately, I feel the need to comment on your weird way of specifying records: you first declare a row and then make a record out of it. FYI it can be done directly with curly braces, for example:
type Resource = {
identifiers :: Array Identifier
}
In case you're doing it this way for aesthetic reasons, I have no objections. But in case you didn't know - now you know :-)