How to create a javascript object with a value that doesn't have a fixed type?

#1

I would like to generate an object like {backdrop: "static", keyboard: true}

keyboard is always Boolean, but backdrop can be String or Boolean

I looked in the foreign module, but i don’t see a way to set a property only read https://pursuit.purescript.org/packages/purescript-foreign/5.0.0/docs/Foreign.Index#v:readProp

I looked at argonaut to go through an intermediate json step https://pursuit.purescript.org/packages/purescript-argonaut-core/5.0.2/docs/Data.Argonaut.Core#v:fromObject problem here is that Object is from https://github.com/purescript/purescript-foreign-object which states Functions for working with homogeneous JavaScript objects from PureScript.. But my object is heterogeneous.

Is there a library for this? Or another way to use the libraries i just mentioned?

I need this to work with FFI, and prefer to construct the right javascript object from purescript instead of having to work with difficult (nested) javascript representation of purescript types.

#2

You could type this as { backdrop :: Json, keyboard :: Boolean }, and then argonaut-core provides functions Json -> Maybe String and Json -> Maybe Boolean which you can use to get the string/bool out (if it does turn out to be that type). You could also use Foreign, so { backdrop :: Foreign, keyboard :: Boolean } should work too.

1 Like
#3

Hi @hdgarrood what do you mean with “get the string/bool out” ? I want to put in values on the purescript side and get out values on the javascript side.

I like the type you proposed, i saw some functions like fromBoolean :: Boolean -> Json which i think should use to construct your type.

The foreign type with Foreign looks even better imo because i can avoid JSON. I understand now due to the type you wrote that i can probably use this function https://pursuit.purescript.org/packages/purescript-foreign/5.0.0/docs/Foreign#v:unsafeToForeign

Is it also possible to start of with an open type? So i can construct {} or {backdrop: true} or with more keys…

#4

Ah of course - I didn’t understand your original post properly, and I thought you might have wanted to read the values on the PureScript side too.

You can always use forall props. Record props, which means “a record with any fields which may have any type”. If you want to enforce things like "if this field is present then it should have the type Boolean", you can use the purescript-options library, or you can use Union constraints like react-basic does for its DOM elements.

#5

Does a function with signature forall props. Record props allow working with a record like it’s a monoid ? Starting out with an empty record and then adding key/value? Or how to create an object that allows adding properties to it ?

I don’t quite get what forall props. Record props would give …

#6

I have now this implementation with a record that is of predefined shape that type checks. Then for each property i give a default value. However i don’t like this because the implementation from the FFI code that i’m wrapping has already defaults set.

data Option
  = Backdrop OBackdrop
  | Keyboard Boolean
  | Focus Boolean
  | Show Boolean

data OBackdrop
  = BD_True
  | BD_False
  | BD_Static

jsoptions :: { backdrop :: Foreign, focus :: Boolean, keyboard :: Boolean, show :: Boolean }  
jsoptions = {backdrop: unsafeToForeign true, keyboard: true, focus: true, show: true}

optionsToOjbect opts =
  let c (Backdrop BD_True)   obj = obj { backdrop = (unsafeToForeign true) }
      c (Backdrop BD_False)  obj = obj { backdrop = (unsafeToForeign false) }
      c (Backdrop BD_Static) obj = obj { backdrop = (unsafeToForeign "static") }
      c (Keyboard v)         obj = obj { keyboard = v }
      c (Focus v)            obj = obj { focus    = v }
      c (Show v)             obj = obj { show     = v }
  in foldrDefault c jsoptions opts
#7

I think that your proposition is ok and will work nicely and also @hdgarrood purescript-options suggestion is a nice possibility as it is a generic and widely used pattern.
But if you have a lot of such kind of union types in your bindings and want to represent them directly in your PS as a first class values without additional “translation” (read “allocation” ;-)) step I can propose two another representations which I know of:

1. “Dirty” smart constructors

You can skip PureScript representation all together (because you don’t have to pattern match on it) and provide this kind of “really smart” constructors:

 foreign import data OBackdrop :: Type

static :: OBackdrop
static = unsafeCoerce "static"

backdrop :: Boolean -> OBackdrop
backdrop = unsafeCoerce

We (currently @dtwhitney and @srghma and me :-)) are doing the same thing in our community project purescript-react-basic-mui on the prerelease branch - please check this module for example: https://github.com/purescript-react-basic-mui/purescript-react-basic-mui/blob/codegen-read-dts/src/MUI/Core/Grid.purs#L15

We are doing there also some “cheap namespacing” by grouping these kind of constructors into the records. But this is not really relevant…

Sorry for the code formatting of the above module but we are doing full, proper codegen from TS types ( we are using typescript compiler API - for more info please check https://github.com/lambdaterms/purescript-read-dts) and purty is not yet incorporated into the process (it is alredy implmeneted thanks to @srghma but not merged yet).

2. Full blown untagged unions

If you want to take an experimental path you can try @jvliwanag idea of untagged unions and literals representation. I think it was initially discussed under this thread and introduced in this comment: https://github.com/purescript-react-basic-mui/purescript-react-basic-mui/pull/13#issuecomment-553260049) .

There are two related libraries: https://github.com/jvliwanag/purescript-oneof and https://github.com/jvliwanag/purescript-literal and there is an API binding which @jvliwanag is currently buidling upon this ideas - I’m linking to an example field definition:
https://github.com/jvliwanag/purescript-antd-codegen/blob/master/generated/src/Antd/Reference/Table.purs#L207

Using the current state of these libs you could possibly write something like:

type Opts =
  { backdrop :: Undefined |+| StringLit "static" |+| Boolean
  , keyboard :: Boolean
  }

-- | You can skip optional field and use `coerce` from the lib
opts1 :: Opts
opts1 = coerce { keyboard: true }

-- | This is a plain string "static" already - no more transformations
-- | This value can be also reused in a different untagged union types
static = stringLit :: StringLit "static"

opts2 :: Opts
opts2 = { keyboard: true, backdrop: asOneOf static }

-- | You can pass `undefined` value directly
opts3 :: Opts
opts3 = { keyboard: true, backdrop: asOneOf undefined }

I want to repeat these few interesting points from the above example:

  • Undefined type makes a field optional.
  • undefined is a first class value so you can initialize your field with it (as in opts3) and of course pass it around to the constructor function etc. This contrasts with the approach like “rows Union jungling” which you can find in our purescript-react-basic-mui or in purescript-react-basic libs :slight_smile:
  • You can also skip optional field (as in opts1).
  • And turn a partial record into the full one by using coerce from the lib. coerce is a single and safe unsafeCoerce call under the hood.
  • StringLit "static" is a representation of a given string constant value on the type level. It is directly reflected into string “static” - no intermediate representation here (please check the compiled output here: https://github.com/paluh/purescript-untagged-union-example/blob/master/output/Main/index.js#L12).
  • asOneOf is also “cheap” - there are only two function calls under the hood. No typeclasses dicts involved.

You can find full a repo with the above example (I’ve also included compiled code for quick reference) here: https://github.com/paluh/purescript-untagged-union-example

Like I said - it is rather experimental because @jvliwanag said that he is still testing the API and doesn’t want to commit to any particular design decision regarding these libs at the moment.

To sum it up I want to say that we have a small plan to create some community project related to Typescript / JavaScript / PureScript bindings which will provide some tools and examples of a codegen, TS and PS interaction etc. I think I will open up a discussion in a separate thread on the topic I hope soon.

Sorry for this spam attack :slight_smile:

4 Likes
#8

That’s some really cool work underway @paluh I can definitely see the value of the union types approach. I’m just starting out with purescript though so i prefer method 1 with less experimentation.

To summarize so far the possibilities are:

  1. use Foreign type
  2. purescript-options
  3. unsafeCoerce with smart constructors
  4. Union types

I guess when i can use Undefined as first class value that i can change the initial values to

jsoptions = {backdrop: undefined, keyboard: undefined, focus: undefined, show: undefined}

This would help with not having to copy the implementation of the default values into purescript. By the way for reference the default values are documented here https://getbootstrap.com/docs/4.4/components/modal/#options

Imo it would be even nicer not having to specify keys at all. Instead of foldrDefault c jsoptions opts i could then write foldrDefault c {} opts, where {} is an empty record that can turn into a regular javascript object. This however i don’t get to typecheck because the c fold function keeps yielding differently typed records on each iteration.

#9

Imo it would be even nicer not having to specify keys at all. Instead of foldrDefault c jsoptions opts i could then write foldrDefault c {} opts, where {} is an empty record that can turn into a regular javascript object. This however i don’t get to typecheck because the c fold function keeps yielding differently typed records on each iteration.

I’m not sure if I fully understand but if you refer to the “untagged union” representation you can pass undefined for fields directly but you can also skip them and use coerce on the “not fully populated record”. coerce is provided by the jvliwanag/oneof library. I’ve done this when I was defining opts1 value above.

Regarding optional record fields in the context of the “smart constructors” approach let’s consider this snippet:

type OptionalFields r = ( opt1 ∷ Boolean, opt2 ∷ Int | r )

stringify
  ∷ ∀ given missing
  . Union given missing (OptionalFields + ())
  ⇒ { | given }
  → String
stringify r =
  unsafeStringify r

The above Union constraint states that our given row together with some missing row will add up to OptionalFields row. In other words by providing the above Union constraint we can restrict our argument record type build upon the given row (which could be expressed by { | given } or Record given signatures) so it can have zero, one or both defined fields present.
It is hard for me to provide any sensible function which is able to do something useful with such a record so I’m using dummy unsafeStringify placeholder here (to be honest I’m not even able to use “typing facts” implied by this kind of constraints in the function body - I’m writing on the topic few sections below).
In the real world we want to consume such a value specifically by javascript FFI which is able to inspect the record and handle this argument as it likes. In other words we can imagine that we have something like foreign import stringify :: Union .... => { | given } -> String instead of our current stringify implementation.
What is really important in this context is that such a FFI function has to provide a wrapper for a Union constraint placeholder. Please read my following post below to find full working example of a FFI function.

Going back to our optional fields - all this calls are correct:

logOptional ∷ Effect Unit
logOptional = do
  log $ stringify {}
  log $ stringify { opt1: true }
  log $ stringify { opt2: 2 }
  log $ stringify { opt1: false, opt2: 8 }

Now let’s try to define also required fields. We can express required fields by stating that all of them are a part of our { | given } record. Something like Union RequiredFields other given where RequiredFields is predefined row should work. By combining this approach with the previous constraint we get:


type RequiredFields r = ( req1 ∷ Number, req2 ∷ String | r )

stringify'
  ∷ ∀ given optionalGiven optionalMissing
  . Union (RequiredFields + ()) optionalGiven given
  ⇒ Union given optionalMissing (RequiredFields + OptionalFields + ())
  ⇒ { | given }
  → String
stringify' r = unsafeStringify r

Now we can be sure that at least req1 and req2 have to be present in the record argument. Of course we can provide any combination of optional fields in this record too.

What was quite surprising for a compiler user like me (I know only basics about HM inference, no knowledge about PS compiler internals etc.) is the fact that I’m not able to use required fields in the function body. What I mean is that I can’t do r.req1 or r.req2 (stringify' r = r.req2 won’t compile) because constraints don’t introduce such “facts” about the type to the inference algorithm in the compiler - please check this issue thread https://github.com/purescript/purescript/issues/3242 (@goodacre.liam comment is really informative I think).
I’ve also included related comment in the example repo: https://github.com/paluh/purescript-optional-and-required-fields-example/blob/master/src/Main.purs#L36.

Because of the above limitiation it is better to provide alternative signature which caputres RequiredFields directly in the type of the input record:

stringify''
  ∷ ∀ optionalGiven optionalMissing
  . Union optionalGiven optionalMissing (OptionalFields + ())
  ⇒ { | RequiredFields + optionalGiven }
  → String
stringify'' r = "req2:" <> r.req2 <> "; record: " <> unsafeStringify r

This formulation allows us to use any required field inside the body of a function.

From the fully polymorphic function perspective like unsafeStringify or from the perspective of a FFI JS function which is not type checked at all both signatures can be seen as equivalent.

logOptionalAndRequired = do
  log $ stringify' { req1: 8.0, req2: "test" }
  log $ stringify' { opt2: 2, req1: 8.0, req2: "test" }
  log $ stringify' { opt1: true, opt2: 2, req1: 8.0, req2: "test" }

  log $ stringify'' { req1: 8.0, req2: "test" }
  log $ stringify'' { opt2: 2, req1: 8.0, req2: "test" }
  log $ stringify'' { opt1: true, opt2: 2, req1: 8.0, req2: "test" }

Here you can find a full working example: https://github.com/paluh/purescript-optional-and-required-fields-example

The optional fields pattern is extensively used by purescript-react-basic library. In the next release of our MUI bindings we are going to handle distinction between required and optional fields (thanks to @srghma) and we are going to use the extended signature there.

#10

Hi @paluh thanks for your extensive answer. To be honest you totally lost me there. I have no clue what you are talking about. What mainly stands out from what you wrote is that you are using something called “stringify”. I don’t want to create a String, or a JSON.

I have the code which is in this post How to create a javascript object with a value that doesn't have a fixed type?

Instead of jsoptions i would like to supply an empty records, but when i do that i get

  Type of expression lacks required label backdrop.

while checking that expression {}
  has type { backdrop :: Foreign
           , focus :: Boolean   
           , keyboard :: Boolean
           , show :: Boolean    
           | t3                 
           }                    
while applying a function (foldrDefault (#dict Foldable t34)) c
  of type t0 -> t1 t2 -> t0
  to argument {}
in value declaration optionsToObject

This is because optionsToObject :: Set Option -> JsOptions has resulting type JsOptions. Perhaps i can coerce to Foreign.

#11

Ok i tried with Foreign, but problem remains

optionsToObject :: Set Option -> Foreign
optionsToObject opts =
  let c (Backdrop BD_True)   obj = obj { backdrop = (unsafeToForeign true) }
      c (Backdrop BD_False)  obj = obj { backdrop = (unsafeToForeign false) }
      c (Backdrop BD_Static) obj = obj { backdrop = (unsafeToForeign "static") }
      c (Keyboard v)         obj = obj { keyboard = v }
      c (Focus v)            obj = obj { focus    = v }
      c (Show v)             obj = obj { show     = v }
  in unsafeToForeign $ foldrDefault c {} opts
#12

Now i have this (which type checks) but i still have to investigate whether it’s actually sound and works as it’s supposed to

optionsToObject :: Set Option -> Foreign
optionsToObject opts =
  let c (Backdrop BD_True)   obj = unsafeToForeign $ (unsafeFromForeign obj) { backdrop = (unsafeToForeign true) }
      c (Backdrop BD_False)  obj = unsafeToForeign $ (unsafeFromForeign obj) { backdrop = (unsafeToForeign false) }
      c (Backdrop BD_Static) obj = unsafeToForeign $ (unsafeFromForeign obj) { backdrop = (unsafeToForeign "static") }
      c (Keyboard v)         obj = unsafeToForeign $ (unsafeFromForeign obj) { keyboard = v }
      c (Focus v)            obj = unsafeToForeign $ (unsafeFromForeign obj) { focus    = v }
      c (Show v)             obj = unsafeToForeign $ (unsafeFromForeign obj) { show     = v }
  in foldrDefault c (unsafeToForeign {}) opts
#13

Tried with “open records” suggested by @hdgarrood

optionsToObject :: forall a. Set Option -> Record a
optionsToObject opts =
  let c (Backdrop BD_True)   obj = obj { backdrop = (unsafeToForeign true) }
      c (Backdrop BD_False)  obj = obj { backdrop = (unsafeToForeign false) }
      c (Backdrop BD_Static) obj = obj { backdrop = (unsafeToForeign "static") }
      c (Keyboard v)         obj = obj { keyboard = v }
      c (Focus v)            obj = obj { focus    = v }
      c (Show v)             obj = obj { show     = v }
  in foldrDefault c {} opts

same type error here

#14

This is really sad. I would like to edit or complement the post above so it would be approachable for anyone reading it in the future. I don’t want to leave misleading or overcomplicated answers on this forum (in PS there is no magic so everything could be explained clearly I think :-)). Would you like to help me?

My intend was to present the opposite - to show an approach which avoids any conversions and is specifically designed to represent optional fields in a record in the PS type system. I know that react-basic uses it which makes it also quite popular :stuck_out_tongue:
stringify is just probably badly chosen name of an example function which takes such a record with optional fields. In other words conversion to String is completely irrelevant to the topic.

Let me try once again:

  • For the simplicity please let me use record type with three fields which all are Boolean and all are optional (focus, keyboard, show).

  • Let’s assume that we want to pass this record with optional fields the the consume function which is provided by the JS FFI side.

  • This consume function can possibly do whatever it wants with this value (like creating and JS object instance, send it to the server, store it somewhere etc.) but it should not mutate the given value.

  • Let’s assume that our consumer function just prints some info about a given value to the console.

Here is how one can possibly implement this scenario - I’m writing the whole modules here for completeness. Main.purs:

module Main where

import Prelude

import Effect (Effect)
import Prim.Row (class Union)

foreign import consume
  ∷ ∀ given missing
  . Union given missing ( focus ∷ Boolean, keyboard ∷ Boolean, show ∷ Boolean )
  ⇒ { | given }
  → Effect Unit

main ∷ Effect Unit
main = do
  consume {}
  consume { focus: true }
  consume { focus: false, show: true }

There is a minor twist related to the FFI implementation on the JS side. This function has a Union constraint in its type so PureScript compiler expects that we are accepting Union type class dict as a first argument (it is really missing due to the optimization) in our binding. In other words we have to add additional function() layer on top of our binding.

Additionally because consume is an side effecting “function” I have to wrap its body in additional function() {...} which represents Effect. Please ignore it as it is not really relevant here. Main.js:

// This first `function` wrapper consumes possible `Union` constraint.
// For every constraint PS compiler is going to pass its instance dictionary.
// In this case this dictionary should be empty because Union class doesn't
// contain any methods. The compiler optimizes this scenario and skips
// empty dict but still expects us to provide a wrapping function.
exports.consume = function() {
  function(r) {
    // This function wrapper represents `Effect`
    return function() {
      // This r value can __possibly__ contain  fields like: focus, keyboard, show
      // but they also can be missing:
      console.log("focus value or undefined: " + r.focus);
   
      if(r.keyboard === undefined) {
        console.log("keyboard attribute is missing"); 
      }
    };
  };
};

Please note that:

  • We can now pass directly our record values with any combination of fields to the consume function.

  • There is no casting recuired.

  • There is no intermediate representation.

Is this example better? Do you think that I should drop stringify from the previous example and replace it with this?

I could and would like also to extend this example with required fields in the next post if necessary.:

  • Do you think that I should drop section about Union to represent required fields? I wanted to present this non obvious behavior of “non propagating constraints” (Union in this case) but maybe I should remove it or move it to the end of the post.

  • Do you think that section about required fields with { | RequiredFields + optionalGiven } is clear? Do you think it is needed?

#15

This example is understandable for me. Now consume becomes the “magic” function. My situation is slightly different instead of purescript record directly into FFI. I have purescript Set (or HashSet in my current implementation) into “magic extendable record” (with a fold) into FFI. So the Union technology looks cool and useful, i can not use it like this in my situation.

I can not answer this. I understand your last post but i don’t understand the stringify part from before. I don’t know if it’s good or bad.

I don’t know what a Union is. I only know of such thing from programming in C.

Since you like some detailed information about what is clear and what is not. From this type signature i understand the following:

  • stands in for forall saying that this variable can be anything. So there are two “can be anything’s”, optionalGiven and optionalMissing
  • . Union optionalGiven optionalMissing (OptionalFields + ()). The dot . is unfamiliar, maybe it’s the same dot as in forall a., i never seen it been put on the next line though.
  • Union optionalGiven optionalMissing (OptionalFields + ()). So there is a typeclass Union with 3 params, two can be anything the third one is supplied a concrete type (OptionalFields + ())
  • (OptionalFields + ()). No idea what + means on the type level. Also don’t know the OptionalFields type. Don’t understand why unit type is there unit :: ().
  • ⇒ { | RequiredFields + optionalGiven } First argument to the function is a record which already has some fields. RequiredFields i think is a data constructor. + i don’t know what it is (i don’t think it’s the same + as before because now it’s on value level. optionalFields “can be anything” so i guess this says it doesn’t matter which fields the record has that you put in.

Function implementation to string i think is just for example and not the interesting part.

Would you like me to comment on some more specific parts of your post ??

#16

I talked to paluh. Here is the summary / conclusion of what was discussed.

The goal was to build a simple javascript object where each key is optional (we already discussed the situation where some keys can be mandatory). Of course each key in a object is unique, there are no two keys of the same string.

Method 1

I took an API specification and modeled into purescript types like Option (see above).

advantages

This can have it’s benefits when passing around these values in a purescript program.
Incrementally building the options.

disadvantages

boilerplate

But it has some disadvantages because when wanting to have the “unique key” property it’s not sufficient to make a Array Option with duplicate options. Therefor a Set or HashSet is needed. Set and HashSet need an instance of Eq on Option and Set also needs an instance of Ord, thus HashSet is a bit easier to implement (it needs Hashable though). Also the default derive implementation of Eq will try to unwrap all the types and compare them for equality too, which is a bit senseless because the comparison of Keyboard true and Keyboard false (for example) will never happen because (Hash)Set already guarantees there is only one Keyboard option. So a self written instance of Eq is needed which is a lot of boilerplate.

construction of javascript object

Another problem is that one can not simply build the javascript object. To build it inside of javascript one would have to unwrap very complicated purescript types (console.log(a :: HashSet Option)). To build a javascript object from purescript is easy, just use a record. The problem lies in it that a record can not be easily be build up. Some solutions that can or can not work:

purescript-record : can “build” records but the resulting type seems to be fixed
Foreign: this works, but requires a lot of casting to pass it around and actually do something with it, see How to create a javascript object with a value that doesn't have a fixed type?
Foreign.Object: a possibly much more elegant solution that would dissolve some disadvantages of just using Foreign
Starting out with a “full” record and copying the default values from the javascript implementation: you don’t really want to have this kind of leakage

method 2

A “trick” to pass incomplete records (have undefined values) directly into the FFI. This was suggested by paluh

advantages

Almost zero boilerplate.
No overhead of conversion functions, all difficult stuff is on the type level and causes no runtime overhead.
Compiler errors are just as good as well typed program (method 1)

disadvantages

We couldn’t build the record incrementally. Maybe with some more type level sorcery it would be possible at some point. This disadvantage can be somewhat mitigated by casting again to Foreign probably.

Not sure if defined keys with value “undefined” will be overwritten by the javascript library, though i suspect that this is nearly always the case. Not really a disadvantage but something just to be sure of.

Conclusion

The best way to build a javascript object with optional types (leaving the other keys to the javascript library defaults) depends on the way you want to construct these options.

If these are defined on compile time at once method 2 is definitely the way to go because it’s very clean, fast and in this scenario there are no downsides to it.

Easy for library implementer, easy for library user.

In case the javascript object needs to be built piece by piece possibly coming from different calculating functions or some user input, method 1 seems the most natural way to solve this problem. Possibly method 2 can be hacked somewhat through casting to Foreign again but you lose the nice abstraction which ADT’s give you and you have to work with additional functions that manipulate the data.

Difficult for library implementer due to boilerplate, easy for the library user.


I will possibly revise this when i know some more information in the feature. Thanks for all the help @paluh !

1 Like
#17

I’ve updated my previous answer to incorporate Union constraint dict into the FFI function. I’ve also found that it is possible that providing a foreign signature with constraints could be forbidden in the future (please check this issue under purescript/purescript repo for more details.

I think that in such a case I would provide (possibly edit this post) additional snippet which adds additional function layer to the binding with a custom type as a representation for a record with optional fields.

#18

This seems somewhat related https://qiita.com/kimagure/items/b0b7da07d8183cb51d58 i just wanted to drop in this link for reference.