Given I have the folowing records in purescript:
let name = {name: "Jim"}
let age = {age: 37}
is it possible to combine those two records some how in a generic way? Something like:
name 'comb' age
such that I get the following record:
{name: "Jim", age: 37}
Somehow it seems to be possible with the Eff rowtype, but I'm curious if it would be possible with 'normal' records. I'm new to purescript and it's record syntax.
Thanks a lot.
EDIT:
It seems that currently the official package for handling record manipulations is purescript-record
- you can find Builder.purs there which provides merge
and build
functions:
> import Data.Record.Builder (build, merge)
> name = {name: "Jim"}
> age = {age: 37}
> :t (build (merge age) name)
{ name :: String
, age :: Int
}
API NOTE:
This API looks overcomplicated at first glance - especially when you compare it to simple unionMerge name age
call (unionMerge
is intoduced at the end of this answer). The reason behind Builder
existence (and so this API) is performance. I can assure you that this:
> build (merge name >>> merge age) {email: "[email protected]"}
creates only one new record. But this:
> unionMerge name (unionMerge age {email: "[email protected]"})
creates two records during execution.
What is even more interesting is how Builder
, build
and merge
are implemented - Builder
is newtype wrapper around a function (and its composition is just a function composition) and build
is just a function application on copied version of the record:
newtype Builder a b = Builder (a -> b)
build (Builder b) r1 = b (copyRecord r1)
In merge
there is unsafeMerge
performed:
merge r2 = Builder \r1 -> unsafeMerge r1 r2
So why are we gaining here anything?? Because we can be sure that intermediate results can't escape function scope and that every value is consumed exactly once in builder chain. Therefore we can perform all transformations "in place" in a mutable manner. In other words this intermediate
value:
> intermediate = unionMerge name {email: "[email protected]"}
> unionMerge age intermediate
can't be "extracted" from here:
> build (merge name >>> merge age) {email: "[email protected]"}
and it is only consumed once by the next builder, namely merge age
.
TYPESYSTEM COMMENT:
It seems that Purescript type system can handle this now thanks to the Union
type class from Prim
:
The Union type class is used to compute the union of two rows
of types (left-biased, including duplicates).
The third type argument represents the union of the first two.
Which has this "magic type" (source: slide 23):
Union r1 r2 r3 | r1 r2 -> r3, r1 r3 -> r2
OLD METHOD (still valid but not preferred):
There is purescript-records package which exposes unionMerge
which does exactly what you want (in new psci we don't have to use let
):
> import Data.Record (unionMerge)
> name = {name: "Jim"}
> age = {age: 37}
> :t (unionMerge age name)
{ name :: String
, age :: Int
}
note: When this answer was accepted, it was true, but now we do have the row constraints it mentions, and a library for manipulating records that includes merges/unions: https://github.com/purescript/purescript-record
It's not possible to do this at the moment, as we don't have a way of saying that a row lacks some label or other. It is possible to have an open record type:
something :: forall r. { name :: String | r } -> ...
But this only allows us to accept a record with name
and any other labels, it doesn't help us out if we want to combine, extend, or subtract from records as it stands.
The issue with combining arbitrary records is we'd have a type signature like this:
comb :: forall r1 r2. { | r1 } -> { | r2 } -> ???
We need some way to say the result (???
) is the union of r1
and r2
, but also we'd perhaps want to say that r1
's labels do not overlap with r2
's.
In the future this may be possible via row constraints.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With