Skip to content

kayhide/wakame

Repository files navigation

Wakame

GitHub CI Hackage Stackage LTS Stackage Nightly BSD3 license

wakame is a Haskell library to manipulate record fields in a row-polymorphic way.

Overview

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 = "peter@amazing.com", 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 = "spider@man.com", username = "Spider Man"}
> updateUser updating user
User {id = ID 42, email = "spider@man.com", 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 = "spider@man.com", 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 unions 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 the toRow function
  • Adding, removing or replacing the fields over the Row with union and nub
  • Converting back to a record with fromRow

Row-polymorphic functions

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 the Of type family and a pair of toRow / fromRow functions.
    • wakame defines an instance of IsRow for all Haskell records with a Generic instance.
  • Lacks constrains a row to not have a field with the given label.
  • Merge is a combination of Union and Nub, 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.

Underlying data structure

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.

Why not record-sop ?

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.

What is wakame?

Wakame is a type of edible seaweed, popular in Japan.

The most important property of wakame is that, it changes its color when boiled.

Contributions

Feel free to open an issue or PR. Thanks!