Skip to content

Commit

Permalink
Semigroups, Monoids, Zips, and AsRecord (#13)
Browse files Browse the repository at this point in the history
* Monoids, Semigroups, and Zips

* AsRecord

* tests

* changelog, version bump

* stylin

* hmm
  • Loading branch information
parsonsmatt authored Apr 11, 2024
1 parent afd73d0 commit c4fd756
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 1 deletion.
8 changes: 8 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog for prairie

## 0.0.4.0

- [#13](https://github.com/parsonsmatt/prairie/pull/13)
- Introduce `Prairie.Semigroup`, allowing you to combine two records by semigroup-appending their fields together.
- Introduce `Prairie.Zip`, allowing you to combine two records by specifying how to combine their fields. This fuels `Prairie.Semigroup`.
- Introduce `Prairie.Monoid`, allowing you to make an `emptyRecord` with `mempty` at each field.
- Introduce `Prairie.AsRecord`, allowing you to derive instances for records based on their `Record` and `FieldDict` instances.

## 0.0.3.0

- [#8](https://github.com/parsonsmatt/prairie/pull/8)
Expand Down
6 changes: 5 additions & 1 deletion prairie.cabal
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cabal-version: 1.12

name: prairie
version: 0.0.3.0
version: 0.0.4.0
description: Please see the README on GitHub at <https://github.com/parsonsmatt/prairie#readme>
homepage: https://github.com/parsonsmatt/prairie#readme
bug-reports: https://github.com/parsonsmatt/prairie/issues
Expand All @@ -24,12 +24,16 @@ source-repository head
library
exposed-modules:
Prairie
Prairie.AsRecord
Prairie.Class
Prairie.Update
Prairie.Diff
Prairie.Fold
Prairie.Zip
Prairie.Traverse
Prairie.TH
Prairie.Semigroup
Prairie.Monoid

build-depends:
base >= 4.13 && < 5
Expand Down
6 changes: 6 additions & 0 deletions src/Prairie.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ module Prairie
, module Prairie.Diff
, module Prairie.Fold
, module Prairie.Traverse
, module Prairie.Zip
, module Prairie.TH
, module Prairie.Semigroup
, module Prairie.Monoid
) where

import Prairie.Class
Expand All @@ -16,3 +19,6 @@ import Prairie.Fold
import Prairie.TH
import Prairie.Traverse
import Prairie.Update
import Prairie.Zip
import Prairie.Semigroup
import Prairie.Monoid
51 changes: 51 additions & 0 deletions src/Prairie/AsRecord.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- | This module contains a newtype 'AsRecord' which is used to provide
-- a variety of default instances for types based on their 'Record'
-- instance.
module Prairie.AsRecord where

import Prairie.Class
import Prairie.Monoid
import Prairie.Semigroup

-- | This @newtype@ is intended for use with @DerivingVia@.
--
-- For an example use, let's consider this @User@ datatype:
--
-- > data Foo = Foo
-- > { ints :: [Int]
-- > , char :: First Char
-- > }
-- >
-- > mkRecord ''Foo
--
-- Let's say we want to define an instance of 'Semigroup' for this type, so
-- that we can combine two of them. Ordinarily, we'd need to write
-- a boilerplate-y instance:
--
-- > instance Semigroup Foo where
-- > f0 <> f1 = Foo
-- > { ints = f0.ints <> f1.ints
-- > , char = f0.char <> f1.char
-- > }
--
-- With @DerivingVia@, we can use 'AsRecord' to provide it easily:
--
-- @
-- deriving via AsRecord Foo instance Semigroup Foo
-- @
--
-- @since 0.0.4.0
newtype AsRecord rec = AsRecord { unAsRecord :: rec }

-- |
--
-- @since 0.0.4.0
instance (Record rec, FieldDict Semigroup rec) => Semigroup (AsRecord rec) where
AsRecord r0 <> AsRecord r1 =
AsRecord (appendRecord r0 r1)

-- |
--
-- @since 0.0.4.0
instance (Record rec, FieldDict Semigroup rec, FieldDict Monoid rec) => Monoid (AsRecord rec) where
mempty = AsRecord emptyRecord
10 changes: 10 additions & 0 deletions src/Prairie/Monoid.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Prairie.Monoid where

import Prairie.Class

-- | Create a 'mempty' 'Record' assuming that all of the fields of the
-- record have a 'Monoid' instance.
--
-- @since 0.0.4.0
emptyRecord :: (Record rec, FieldDict Monoid rec) => rec
emptyRecord = tabulateRecord (\field -> withFieldDict @Monoid field mempty)
17 changes: 17 additions & 0 deletions src/Prairie/Semigroup.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- | This module provides the ability to append two records using '(<>)',
-- provided that all of their fields have an instance of 'Semigroup'.
module Prairie.Semigroup where

import Prairie.Class
import Prairie.Zip

-- | Zip two records together using a 'Semigroup' append.
--
-- @since 0.0.4.0
appendRecord
:: forall rec. (Record rec, FieldDict Semigroup rec)
=> rec
-> rec
-> rec
appendRecord =
zipWithRecord (\a b field -> withFieldDict @Semigroup field (a <> b))
34 changes: 34 additions & 0 deletions src/Prairie/Zip.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Prairie.Zip where

import Prairie.Class
import Prairie.Fold

-- | Take two records and zip them together with the provided function.
--
-- The field is given for the final parameter in the function, allowing you to use @LambdaCase@.
--
-- @
-- zipWithRecord
-- (\a b ->
-- \case
-- UserName ->
-- a <> b
-- UserAge ->
-- a + b
-- )
-- @
--
-- @since 0.0.4.0
zipWithRecord
:: forall rec. (Record rec)
=> (forall ty. ty -> ty -> Field rec ty -> ty)
-> rec
-> rec
-> rec
zipWithRecord k r0 r1 =
foldRecord f r0 r0
where
f :: ty -> rec -> Field rec ty -> rec
f v0 rec field =
let v1 = getRecordField field r1
in setRecordField field (k v0 v1 field) rec
47 changes: 47 additions & 0 deletions test/Spec.hs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
Expand All @@ -24,6 +30,8 @@ import Control.Monad
import Data.Aeson
import Data.Kind (Type)
import Data.Monoid
import GHC.Records

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.8)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 8.10.7)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 8.8.4)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.0.2)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.2.5)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.6)

The import of ‘GHC.Records’ is redundant

Check warning on line 33 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.4.4)

The import of ‘GHC.Records’ is redundant
import Prairie.AsRecord
import Test.Hspec

data User = User { name :: String, age :: Int }
Expand All @@ -36,6 +44,17 @@ deriving instance Show (Field User a)

exampleUser = User "Alice" 30

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.8)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 8.10.7)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 8.8.4)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.0.2)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.2.5)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.6)

Top-level binding with no type signature: exampleUser :: User

Check warning on line 45 in test/Spec.hs

View workflow job for this annotation

GitHub Actions / build (3.10, 9.4.4)

Top-level binding with no type signature: exampleUser :: User

data Foo = Foo
{ ints :: [Int]
, char :: First Char
}
deriving (Show, Eq)

mkRecord ''Foo

deriving via AsRecord Foo instance Semigroup Foo
deriving via AsRecord Foo instance Monoid Foo

data T a = T { x :: a, y :: Int }

instance Record (T a) where
Expand Down Expand Up @@ -197,3 +216,31 @@ main = hspec $ do
User { name = "", age = 30 }
`shouldBe`
Nothing

describe "Semigroup" do
it "can combine two records" do
let f0 = Foo [1] (First Nothing)
f1 = Foo [2,3] (First (Just 'a'))
f0 <> f1
`shouldBe`
Foo [1,2,3] (First (Just 'a'))

describe "Monoid" do
it "can produce an empty record" do
let obvious =
Foo mempty mempty
mempty `shouldBe` obvious

describe "Zip" do
it "can combine two records" do
let u0 = User "Matt" 35
u1 = User "ttaM" 53
zipWithRecord (\a b -> \case
UserName ->
a <> b
UserAge ->
a + b
) u0 u1
`shouldBe`
User "MattttaM" (35 + 53)

0 comments on commit c4fd756

Please sign in to comment.