10
votes

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.

2

2 Answers

11
votes

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
}
6
votes

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.