How to combine rows of record types in PureScript? (Is there any alternative to the Union typeclass in PureScript 0.12.0?)
Asked Answered
R

2

12

Problem: I have different record types with many common fields. How could I "include" the common fields in the record type definitions?

Example:

newtype RecordType1 = RecordType1 { a :: Int, b :: Int, y :: String }
newtype RecordType2 = RecordType2 { a :: Int, b :: Int, z :: Boolean } 

How to write the equivalent in PureScript?

newtype RecordType1 = RecordType1 { CommonFields, y :: String }
newtype RecordType2 = RecordType2 { CommonFields, z :: Boolean }

The type class Union mentioned in An Overview of the PureScript Type System could be what I look for... but it seems to be out since PureScript 0.12.0.

Any recommendations? Is there anything I'm missing?

Thanks!

Roadhouse answered 23/8, 2018 at 13:25 Comment(0)
A
30

PureScript has a special syntax for combining records:

type Common = ( a :: Int, b :: Int )
type Record1 = { y :: String | Common }
type Record2 = { z :: Boolean | Common }
newtype RecordType3 = RecordType3 { w :: Number | Common }

Note that the definition of Common uses parentheses, not curly braces. That is because Common is a row, not a record. You can make a record out of it though:

type CommonRec = Record Common 
-- equivalent to:  CommonRec = { a :: Int, b :: Int }

In fact, the curly braces notation is just syntactic sugar for applying Record to a row. An expression { xyz } gets desugared to Record ( xyz ).

You can use the "pipe" syntax to extend rows as well:

type CommonPlusFoo = ( foo :: Bar | Common )
type RecWithFoo = { x :: Int | CommonPlusFoo }

You can also make your record types polymorphic by providing Common as a type parameter:

type Record1Poly r = { y :: String | r }
type Record1 = Record1Poly Common

This is very handy for writing functions that work with partial records, e.g.:

updateName :: forall r. { name :: String | r } -> { name :: String | r }
updateName x = x { name = "Mr. " <> x.name }

jones = { name: "Jones", occupation: "Plumber" }
mrJones = updateName jones  -- mrJones = { name: "Mr. Jones", occupation: "Plumber" }

In this example, the function can work with any record that has a name field, regardless of what else it might have.


Finally, to express an empty row, use empty parens:

type Record1Poly r = { y :: String | r }
type Record1 = Record1Poly Common
type OnlyY = Record1Poly ()

On a slightly unrelated topic, note that records in PureScript are not the same as records in Haskell. For example, above Record1 and Record2 are true PureScript ad-hoc extensible records (something that Haskell doesn't have), but RecordType3 is a newtype that has one constructor whose parameter is a record.

One important difference is that, unlike Haskell, this wouldn't work:

 x = RecordType3 { w: 42.0, a: 1, b: 2 }
 y = w x

The expression w x (or even expression x.w) doesn't compile, because RecordType3 is not itself a record, it's a newtype that wraps a record. In order to get w out of it you need to match on the constructor first:

 (RecordType3 k) = x
 y = k.w

Or wrap that as an accessor function:

 unRecordType3 (RecordType3 k) = k
 y = (unRecordType3 x).w

In practice this is really inconvenient if you're approaching records with a Haskell mindset. Instead, what you want to do in PureScript is prefer "naked" records (like Record1 and Record2 in my example above) and only resort to wrapping them in newtype when you really have to.

Abaddon answered 23/8, 2018 at 14:18 Comment(2)
Coming from Haskell, PureScript has mostly been a breeze and I barely had to read the documentation. But records and rows in the PureScript fashion are something new and interesting. I understand them better after reading your answer.Endplay
Glad I could helpAbaddon
N
13

Fyodor's answer is correct. However, there is another clean syntax for combining many row types if you need to.

normally, if you have many record types that you want to combine you would do this:

type Foo r = ( x :: String | r )
type Bar r = ( y :: Int | r )
type FooBar r = Foo (Bar r)

but this will get cumbersome if you have more than one to combine, or the names are too long:

type ThisIsAFoo r = ( x :: String | r )
type ThisIsABar r = ( y :: Int | r )
type ThisIsABaz r = ( z :: Number | r )
type ThisIsAFooBarBaz r = ThisIsAFoo (ThisIsABar (ThisIsABaz r))

so you can use a nice syntax for combining them in the Type module:

import Type.Row (type (+))
type ThisIsAFooBarBaz r = ThisIsAFoo + ThisIsABar + ThisIsABaz + r
Nolly answered 26/5, 2020 at 17:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.