wakame
is a Haskell library to manipulate record fields in a row-polymorphic way.
Here is a quick overview of what wakame
provides.
Imagine a data type of:
data User =
User
{ id :: ID User
, email :: Text
, username :: Text
, created_at :: UTCTime
, updated_at :: UTCTime
}
deriving Generic
To update a subset of the User
record's fields, first define a data type containing the fields you want to update:
data UpdatingUser =
UpdatingUser
{ email :: Text
, username :: Text
}
deriving Generic
Then, write a function for doing the update:
updateUser :: UpdatingUser -> User -> User
updateUser updating user = fromRec $ nub $ union (toRow updating) (toRow user)
Here is a working example of using this function:
> user
User {id = ID 42, email = "[email protected]", username = "Peter Parker", created_at = 2020-06-16 11:22:11.991147596 UTC, updated_at = 2020-06-16 11:22:11.991147596 UTC}
> updating
UpdatingUser {email = "[email protected]", username = "Spider Man"}
> updateUser updating user
User {id = ID 42, email = "[email protected]", username = "Spider Man", created_at = 2020-06-16 11:22:11.991147596 UTC, updated_at = 2020-06-16 11:22:11.991147596 UTC}
Updating the updated_at
field in User
can be done in the same manner. But
this time, let's do it without defining a separate record type:
touchUser :: UTCTime -> User -> User
touchUser time user = fromRec $ nub $ union (toRow $ keyed @"updated_at" time) (toRow user)
toRow $ keyed @"update_at" time
creates a Row
object which has only one field:
{ updated_at :: UTCTime }
And updating the user and the updated_at
field can be done easily within the
same function:
updateAndTouchUser :: UpdatingUser -> UTCTime -> User -> User
updateAndTouchUser updating time user =
fromRec $ nub $ union (toRow $ updating) $ union (toRow $ keyed @"updated_at" time) (toRow user)
This function works as follows:
> updateAndTouchUser updating time user
User {id = ID 42, email = "[email protected]", username = "Spider Man", created_at = 2020-06-16 11:22:11.991147596 UTC, updated_at = 2020-06-16 11:31:35.170029827 UTC}
Note that using nub
once after a chain of union
s will be faster than using nub
after every individual union
.
Wrapping up, we have done the following:
- Converting a record into its corresponding
Row
representation with thetoRow
function - Adding, removing or replacing the fields over the
Row
withunion
andnub
- Converting back to a record with
fromRow
The following create
and update
functions are generalized in terms of row-polymorphism.
data ModelBase a =
ModelBase
{ id :: ID a
, created_at :: UTCTime
, updated_at :: UTCTime
}
deriving (Eq, Show, Generic)
create ::
forall a b.
( IsRow a
, IsRow b
, Lacks "id" (Of a)
, Merge (Of a) (Of (ModelBase b)) (Of b)
) => a -> IO b
create x = do
now <- getCurrentTime
id' <- pure $ ID @b 42 -- shall be `getNextID` or something in practice.
let y =
fromRow
$ merge (toRow x)
$ toRow $ ModelBase @b id' now now
pure y
type OfUpdatedAt = '[ '("updated_at", UTCTime) ]
update ::
( IsRow a
, IsRow b
, Union (Of a) (Of b) ab
, Merge OfUpdatedAt ab (Of b)
) => a -> b -> IO b
update updating x = do
now <- getCurrentTime
let y =
fromRow
$ merge (toRow $ keyed @"updated_at" now)
$ union (toRow updating)
$ toRow x
pure y
IsRow
is a constraint which defines theOf
type family and a pair oftoRow
/fromRow
functions.wakame
defines an instance ofIsRow
for all Haskell records with aGeneric
instance.
Lacks
constrains a row to not have a field with the given label.Merge
is a combination ofUnion
andNub
, which do appending and removing respectively.
With these constraints and functions, you can easily write row polymorphic functions in your application.
These examples are found at Wakame.Examples.Usage.
There are other examples available at Wakame.Examples.Functons.
If you're interested in row polymorphism, the Wikipedia page may help: Row polymorphism.
A direct translation of the functions described in the Wikipedia page is also available at Wakame.Examples.RowPolymorphism.
wakame
uses NP
(a.k.a. "N-ary Product") as the underlying representation of
Row
. NP
is a data type from the
sop-core library.
So if you need finer control of Row
, or if you need an advanced or
application-specific operation, you have the option of using the NP
data type
directly, which will allow you to take advantage of the rich set of functions
from the sop-core
library.
For more details, see the paper True Sums Of Products.
records-sop is a library
built on top of sop-core
. It focuses on the representation of a record data
type and provides a set of functions for doing conversions.
The difference is that records-sop
is relying on generics-sop
which is more
general and also covers non-record data types.
wakame
is specialized for only record data types.
Although the representation data types is virtually the same between
records-sop
and wakame
, how to convert between data types is different.
One of the benefits of wakame
is the ability to introduce special conversion
rules such as keyed @"label" value
to / from Row
.
wakame
gives you the ability to make a single keyed
value correspond to the
representation of a data type with one field, and any arbitrary tuple of
keyed
values to a data type with multiple fields.
In this way, you can use a tuple of keyed
values in place of an anonymous record.
Wakame is a type of edible seaweed, popular in Japan.
The most important property of wakame is that, it changes its color when boiled.
Feel free to open an issue or PR. Thanks!