Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Patch dmap reset and patchable #2

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Revision history for patch

## Unreleased

* Add `PatchDMapWithReset`

* Add `Patchable`

## 0.0.0.1

* Remove unneeded dependencies
Expand Down
2 changes: 2 additions & 0 deletions patch.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ library
, Data.Patch.Class
, Data.Patch.DMap
, Data.Patch.DMapWithMove
, Data.Patch.DMapWithReset
, Data.Patch.IntMap
, Data.Patch.Map
, Data.Patch.MapWithMove
, Data.Patch.Patchable

ghc-options: -Wall -fwarn-redundant-constraints -fwarn-tabs

Expand Down
120 changes: 120 additions & 0 deletions src/Data/Patch/DMapWithReset.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

{-# OPTIONS_GHC -Wall #-}

-- | 'Patch'es on 'DMap' that consist only of insertions (or overwrites) and deletions.
module Data.Patch.DMapWithReset where

import Data.Patch.Class

import Data.Dependent.Map (DMap, GCompare (..))
import qualified Data.Dependent.Map as DMap
import Data.Semigroup (Semigroup (..))
import Data.Constraint.Extras

-- | A set of changes to a 'DMap'. Any element may be inserted/updated or deleted.
-- Insertions are represented as @'ComposeMaybe' (Just value)@,
-- while deletions are represented as @'ComposeMaybe' Nothing@.
newtype PatchDMapWithReset k p = PatchDMapWithReset { unPatchDMapWithReset :: DMap k (By p) }

-- | Holds the information about each key: where its new value should come from,
-- and where its old value should go to
data By p a
= By_Insert (PatchTarget (p a)) -- ^ Insert the given value here
| By_Delete -- ^ Delete the existing value, if any, from here
| By_Patch (p a) -- ^ Patch the value here with the given patch

instance (Semigroup (p a), Patch (p a)) => Semigroup (By p a) where
x@(By_Insert _) <> _ = x
By_Delete <> _ = By_Delete
By_Patch x <> By_Insert y = By_Insert (applyAlways x y)
By_Patch x <> By_Patch y = By_Patch (x <> y)
By_Patch _ <> By_Delete = By_Delete

instance (Monoid (p a), Patch (p a)) => Monoid (By p a) where
mappend = (<>)
mempty = By_Patch mempty

instance
( GCompare k
, Has' Semigroup k p
, Has' Patch k p
)
=> Semigroup (PatchDMapWithReset k p) where
PatchDMapWithReset xs <> PatchDMapWithReset ys = PatchDMapWithReset $ DMap.unionWithKey
(\k -> has' @Patch @p k
$ has' @Semigroup @p k
$ (<>)) xs ys

instance
( GCompare k
, Has' Semigroup k p
, Has' Patch k p
)
=> Monoid (PatchDMapWithReset k p) where
mappend = (<>)
mempty = PatchDMapWithReset DMap.empty

class (Patch (p a), PatchTarget (p a) ~ Patches1LocallyTarget p a) => Patches1Locally p a where
type Patches1LocallyTarget p :: k -> *

data These1 f g x
= This1 (f x)
| That1 (g x)
| These1 (f x) (g x)

mergeWithKey
:: forall k v1 v2 v.
(GCompare k)
=> (forall x. k x -> v1 x -> Maybe (v x))
-> (forall x. k x -> v2 x -> Maybe (v x))
-> (forall x. k x -> v1 x -> v2 x -> Maybe (v x))
-> DMap k v1 -> DMap k v2 -> DMap k v
mergeWithKey f g fg = \xs ys -> DMap.mapMaybeWithKey onlyThat $ DMap.unionWithKey doIt (DMap.map This1 xs) (DMap.map That1 ys)
where
doIt _ (This1 xs) (That1 ys) = These1 xs ys
doIt _ _ _ = error "mergeWithKey misalligned keys"

onlyThat :: forall x. k x -> These1 v1 v2 x -> Maybe (v x)
onlyThat k = \case
This1 xs -> f k xs
That1 ys -> g k ys
These1 xs ys -> fg k xs ys
{-# INLINE mergeWithKey #-}

-- | Apply the insertions or deletions to a given 'DMap'.
instance (GCompare k, Has (Patches1Locally p) k) => Patch (PatchDMapWithReset k p) where

type PatchTarget (PatchDMapWithReset k p) = DMap k (Patches1LocallyTarget p)

apply = go
where
go :: PatchDMapWithReset k p -> DMap k (Patches1LocallyTarget p) -> Maybe (DMap k (Patches1LocallyTarget p))
go (PatchDMapWithReset diff) old = Just $! mergeWithKey (\_ -> Just) inserts updates old diff
where
updates :: forall x. k x -> Patches1LocallyTarget p x -> By p x -> Maybe (Patches1LocallyTarget p x)
updates k ys = has @(Patches1Locally p) k $ \case
By_Insert x -> Just x
By_Delete -> Nothing
By_Patch x -> Just $ applyAlways x ys

inserts :: forall x. k x -> By p x -> Maybe (Patches1LocallyTarget p x)
inserts k = has @(Patches1Locally p) k $ \case
By_Insert x -> Just x
By_Delete -> Nothing
By_Patch _ -> Nothing

deriving instance (Patch (p a), Eq (p a), Eq (PatchTarget (p a))) => Eq (By p a)
deriving instance (Patch (p a), Show (p a), Show (PatchTarget (p a))) => Show (By p a)
deriving instance (Eq (DMap k (By p))) => Eq (PatchDMapWithReset k p)
deriving instance (Show (DMap k (By p))) => Show (PatchDMapWithReset k p)
45 changes: 45 additions & 0 deletions src/Data/Patch/Patchable.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}

-- The derived instances are undecidable in the case of a pathological instance like
-- instance Patch x where
-- type PatchTarget x = Patchable x
{-# LANGUAGE UndecidableInstances #-}

module Data.Patch.Patchable where

-- import Data.Aeson
import GHC.Generics

import Data.Patch

-- | Like SemiMap/PartialMap but for anything patchable
data Patchable p
= Patchable_Patch p
| Patchable_Complete (PatchTarget p)
deriving (Generic)

completePatchable :: Patchable p -> Maybe (PatchTarget p)
completePatchable = \case
Patchable_Complete t -> Just t
Patchable_Patch _ -> Nothing

deriving instance (Eq p, Eq (PatchTarget p)) => Eq (Patchable p)
deriving instance (Ord p, Ord (PatchTarget p)) => Ord (Patchable p)
deriving instance (Show p, Show (PatchTarget p)) => Show (Patchable p)
deriving instance (Read p, Read (PatchTarget p)) => Read (Patchable p)
-- instance (ToJSON p, ToJSON (PatchTarget p)) => ToJSON (Patchable p)
-- instance (FromJSON p, FromJSON (PatchTarget p)) => FromJSON (Patchable p)

instance (Monoid p, Patch p) => Monoid (Patchable p) where
mempty = Patchable_Patch mempty
mappend = (<>)

instance (Semigroup p, Patch p) => Semigroup (Patchable p) where
(<>) = curry $ \case
(Patchable_Patch a, Patchable_Patch b) -> Patchable_Patch $ a <> b
(Patchable_Patch a, Patchable_Complete b) -> Patchable_Complete $ applyAlways a b
(Patchable_Complete a, _) -> Patchable_Complete a