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.