Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kayhide/wakame

Haskell library of row polymorphic record manipulator
https://github.com/kayhide/wakame

hacktoberfest haskell

Last synced: 2 months ago
JSON representation

Haskell library of row polymorphic record manipulator

Awesome Lists containing this project

README

        

# Wakame

[![GitHub CI](https://github.com/kayhide/wakame/workflows/CI/badge.svg)](https://github.com/kayhide/wakame/actions)
[![Hackage](https://img.shields.io/hackage/v/wakame.svg)](https://hackage.haskell.org/package/wakame)
[![Stackage LTS](http://stackage.org/package/wakame/badge/lts)](http://stackage.org/lts/package/wakame)
[![Stackage Nightly](http://stackage.org/package/wakame/badge/nightly)](http://stackage.org/nightly/package/wakame)
![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)

`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:

```haskell
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:

```haskell
data UpdatingUser =
UpdatingUser
{ email :: Text
, username :: Text
}
deriving Generic
```

Then, write a function for doing the update:

```haskell
updateUser :: UpdatingUser -> User -> User
updateUser updating user = fromRec $ nub $ union (toRow updating) (toRow user)
```

Here is a working example of using this function:

```haskell
> 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:

```haskell
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:

```haskell
{ updated_at :: UTCTime }
```

And updating the user and the `updated_at` field can be done easily within the
same function:

```haskell
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:

```haskell
> 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 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.

```haskell
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](https://github.com/kayhide/wakame/blob/master/test/examples/Wakame/Examples/Usage.hs).

There are other examples available at
[Wakame.Examples.Functons](https://github.com/kayhide/wakame/blob/master/test/examples/Wakame/Examples/Functions.hs).

If you're interested in row polymorphism, the Wikipedia page may help:
[Row polymorphism](https://en.wikipedia.org/wiki/Row_polymorphism).

A direct translation of the functions described in the Wikipedia page is also available at
[Wakame.Examples.RowPolymorphism](https://github.com/kayhide/wakame/blob/master/test/examples/Wakame/Examples/RowPolymorphism.hs).

## 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](https://hackage.haskell.org/package/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](https://www.andres-loeh.de/TrueSumsOfProducts/).

### Why not `record-sop` ?

[records-sop](https://hackage.haskell.org/package/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](https://en.wikipedia.org/wiki/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!