Beginner: array of (almost) arbitrary records

I’d like to have an array of (almost) arbitrary records. I’m trying something like this:

type Rec r = { type :: String | r }

rs :: forall r. Array (Rec r)
rs = [{ type: "rec1", field1: 1 }]

Here’s the error I get:

Could not match type

    r0

  with type

    ( field1 :: Int
    | t1
    )


while trying to match type r0
  with type ( field1 :: Int
            | t1
            )
while checking that expression { type: "rec1"
                               , field1: 1
                               }
  has type { type :: String
           | r0
           }
in value declaration rs

where r0 is a rigid type variable
        bound at (line 12, column 6 - line 12, column 35)
      t1 is an unknown type

I don’t understand: isn’t { type: "rec1", field1: 1 } of the type { type :: String | r }?

What’s the difference between my code, and the following one, taken from PS docs:

addProps :: forall r. { foo :: Int, bar :: Int | r } -> Int
addProps o = o.foo + o.bar + 1
1 Like

Hi, I was curious as well, I tried some things, but based on this reference I conclude that you have to extend first the record before you can use it, i.e., there is no way to have an arbitrary field inside a record ON ASSIGNMENT, otherwise it would not be strongly typed. EDIT: Would like to know if I’m wrong though, thanks!

#+begin_src purescript
import Prelude
import Data.Array

:pa
show 0

type Rec r = { type :: String | r }

type UpdatedRec = Rec (field1 :: Int)

abc :: Array UpdatedRec
abc = [{type: "asdf", field1: 123}]
abc
#+end_src

#+RESULTS:
#+begin_example
PSCi, version 0.14.0
Type :? for help

import Prelude

> > > > … … … … … … … … … … "0"

[{ field1: 123, type: "asdf" }]

> See ya!
()
#+end_example

dc582b498f, your code works, yet it doesn’t solve my problem.

I’d like to do something like this (the following code won’t compile):

type Rec r = { type :: String | r }

type UpdatedRec = Rec (field1 :: Int)

abc :: forall r. Array (Rec r)  -- <- here's the difference!
abc = [{type: "asdf", field1: 123}]

My target code would then be something like this (also won’t compile):

type Rec r = { type :: String | r }

type UpdatedRec1 = Rec (field1 :: Int)
type UpdatedRec2 = Rec (field2 :: String)

abc :: forall r. Array (Rec r)
abc = [{type: "I-am-rec-1", field1: 123}, {type: "I-am-rec-2", field2: "Hello!"}]

In other words, I’m trying to have an array of records with the same base fields, but with different additional fields - in PureScript.

I could do it without any problems with FFI (foreign types), yet so far I’m looking for some pure PS solution. (And no, I do not want to use ADTs - I want a clear and simple JavaScript data representation.)

Hello, and welcome. :slight_smile:

You can’t assume types for your type variables.
When you write a polymorphic function, you’re expecting it to be used with different types and you must respect what the variables represent.

For instance, take your first function abc. You’re saying that if I give it a type r, it’ll do something and give me back a value of type Array (Rec r).
So if my r is, say, (myField :: String), I expect it to return Array (Rec (myField :: String)) or, resolving Rec, { type :: String, myField :: String }.
But function abc actually returns the concrete type {type :: String, field1 :: Int}. So it doesn’t compile.

Also, note on your second example how you’re trying to mix different types in the same array. You can’t do that.

I’m not quite sure I understand what you’re trying to do, but I suspect the variant library might help you.

1 Like

Thanks for clarification, nava.

I think that the best conclusion of this topic are the words: you’re trying to mix different types in the same array. You can’t do that.

My original problem is quite simple: I have some JavaScript data, and I want to convert it to my (more sophisticated) PureScript types. I wanted to do the conversion in PureScript (or, at least, to do as much conversion in PureScript, as possible), yet for this to happen, I needed an array of different values (records, types). The only compromise I could afford was all the records to have some common fields (like type). Hence my original question.

(BTW: why I do not want to use ADTs, nor any other PureScript fancy datatypes, and/or libraries? Because of their runtime representation. I want something native to JavaScript, for users who don’t know and don’t use PureScript.)

Of course, it can be done with a little of FFI. I wanted to make sure, though, that there’s no other way in PureScript, before trying to get to FFI level.

1 Like

Maybe something like this could help here?

I think there are a bunch of approaches that could work depending on what your desired outcome is. From the sound of your problem, the first thing I’d be tempted to reach for is an Array Json and then you can construct it out of random elements by encodeJson on each, and you can convert back to PureScript by attempting to decodeJson each element into however many different types as you want to support. Maybe that’s what you mean by FFI though, not sure.

If you want just general purpose records of various shapes, you could use untagged-unions which promises “sane” runtime representations


though as I understand it, it uses a lot of magic and trickery under the covers.

There are probably some other solutions that could help in your situation too, though it’s hard to come up with the right suggestion without knowing more about what you’re specific use case would look like.

2 Likes

I mean, you can just slap purescript-exists on top to allow arbitrary records like you imagine with no runtime cost. Depending on what you want to do with the records afterwards you might run into issues down the line

Actually nevermind, purescript-exists isnt polymorphic over the kind of the type argument yet, but that should be pretty easy to pr, right?

Exists is not enough on it’s own anyway, but yeah, that’s the right idea. This would require existentials instead of universals (plain forall). PureScript doesn’t have support for existentials or subtyping, so the only way to encode this and keep the representation the same is to use an unsafe coercion to a foreign data type. You can encode the existential with higher-ranked foralls in plain PS, but then your representation is a closure instead of the record.

You would first need to run a Union constraint backwards on the record, and quantify over the rhs, which would need to be done through a foreign data type.

foreign import data URecord :: Row Type -> Row -> Type -> Type

fromRecord :: forall r1 r2 r3. Union r1 r2 r3 => Record r3 -> URecord r1 r2

And then you can hide r2 though an existential encoding. I would strongly recommend not going down this route, potentially by just removing all the fields you don’t need first, since you won’t be able to recover them in the type system anyway.

1 Like

So uhm, what about taking the code of Exists and modifying it to be polymorphic over the kind of the Type arg, wouldn’t that achive the same as Union but with better error messages and whatnot? (Not saying its good in the long run, due to the issue w retriving the fields)

Whoa, I haven’t expected so many answers, and such discussion.

I think I have to clarify the problem I’m trying to solve. (Right now I’m heading towards some simple FFI solution, but I think the problem is more general.)

#1 The actual problem is that I want to let people, who doesn’t know PureScript, but know JavaScript, to provide complex data structures to be used with the functions written in PureScript, provided by my application.

In other words: I have written some application, that consumes quite complex data. I’d like users to be able to provide data using JS only. (So far, my app requires those data in more sophisticated PureScript format, so PS compiler is needed.)

I decided to use only types with simple JS runtime representations: all the primitive types, plus records.

No ADTs, not even Maybe (it requires type constructors in JS).

ADTs can be used in my FFI functions, since they are not supposed to be exposed in my application’s API. It’s ok to use type constructors internally - it’s not ok to tell my users to use them (since they have no idea, that PureScript exists).

As for JSON… Well, it could be used. I’m not a big fan of this format, I prefer to use more native ones (like POJO). In the end, I think, I’ll use whatever is leaner.

#2 By the FFI solution, I mean to actually solve the problem in JavaScript, and call the function(s) from PureScript. As simple as that.

JavaScript has no problems with heterogenous collections (arrays), so it is quite easy to process. The more difficult part is data validation: it’s not difficult per se, but I chose PureScript since I strongly prefer strong and sound type system over this thing that is available is JS.

Yet, I also try to be reasonable. My goal is to solve my problem, and not to solve it all in PureScript.

My rule of thumb is: whenever I can solve problem in JavaScript with just a couple of lines, within minutes, while trying to solve the same problem in PureScript leads to hours of work, and the results are not so great, I simply use FFI.

Following this rule, my codebase is 99% PureScript, and 1% of really simple JavaScript.

BTW, I’m going to show you the application, and share my development experience, as soon as I’ll find some spare time to do it.

2 Likes

Variants have a very simple js representation

My original problem is quite simple: I have some JavaScript data, and I want to convert it to my (more sophisticated) PureScript types.

In that case I highly recommend forgetting about records and existentials and almost all of the solutions proposed so far, and using an Array Json instead (where Json comes from the argonaut library). The Json type doesn’t always have to be used for JSON specifically - it represents all of the things which could be returned by the JSON.parse() function. So arrays, objects, numbers, booleans, strings, null. Then, because the data could have any format, you’ll need some PureScript code to verify that the data has a format that you can use. If your PureScript function expects an Array Json, it’ll feel natural to call from JS because you’re free to use any data representation you’d like.

Note that even if you find a way of expressing “this record has exactly these fields but they might have any type” in PureScript (which is possible, but is likely to be very awkward), if you get passed bad data from the JS side, i.e. one of the fields has the wrong name, you won’t be able to catch that. If you write a function which consumes a { type :: Any, field1 :: Any } in PureScript, the PureScript compiler will assume that it’s not possible for the record it receives to contain anything other than those fields, so it won’t check. A solution that uses Json will be able to check that, and will give you the opportunity to handle it sensibly if you’re passed bad data.

3 Likes

I’ve also wanted to read in JSON records and I’ve found that the simplest and easiest thing to do is to parse them with the F monad.

If blob :: Foreign is a JSON object which you expect to be an array of records, each with a string field named "type", then you can parse it into PureScript with the F monad like this:

import Foreign (Foreign, readArray, readString, readProp)
import Control.Monad.Except (runExcept)

result :: Either MultipleErrors (Array {type :: String})
result = runExcept do
  xs <- readArray blob
  for xs \x -> do
    t <- readString =<< readProp "type" x
    pure {type:t}

Then the result will be either the array of records, or a list of errors explaining exactly how the JSON structure was not what you expected it to be.

Parse, don’t validate.

purescript-argonaut provides a way to both read and write JSON records in such a way that they consistently round-trip. But if you don’t need to write the JSON records, then parsing is the simplest approach.

1 Like

I personally prefer argonaut, even if I’m only parsing, but yeah, F works too.

3 Likes