delta-types-1.0.0.0: src/Data/Delta/Embedding.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-|
Copyright: © 2021-2023 IOHK, 2024 Cardano Foundation
License: Apache-2.0
Embeddings of delta types.
-}
module Data.Delta.Embedding (
-- $Embedding
Embedding
, Embedding' (..)
, mkEmbedding
, fromEmbedding
, pair
, liftUpdates
, replaceFromApply
-- * Internal
, inject
, project
) where
import Prelude
import Control.Exception
( SomeException
)
import Data.Delta.Core
( Delta (..)
, Replace (..)
)
import Data.Delta.Embedding.Internal
( Machine (..)
, fromState
, pairMachine
)
import Data.Either
( fromRight
)
import Data.Semigroupoid
( Semigroupoid (..)
)
{-------------------------------------------------------------------------------
Embedding
-------------------------------------------------------------------------------}
{- $Embedding
#doc:Embedding#
An 'Embedding'@ da db@ embeds one type and its delta type @da@
into another type and its delta type @db@.
For reasons of efficiency, 'Embedding' is an abstract type.
It is constructed using the 'Embedding'' type, which has
three components.
* 'write' embeds values from the type @a = 'Base' da@
into the type @b = 'Base' db@.
* 'load' attempts to retrieve the value of type @a@
from the type @b@, but does not necessarily succeed.
* 'update' maps a delta type @da@ to a delta type @db@.
For this mapping, both the value of type @a@ and a corresponding
value of type @b@ are provided;
the delta types @da@ and @db@ are relative to these values.
In the definition of 'update', we can assume that @Right a = load b@.
The embedding of one type into the other is characterized by the following
properties:
* The embedding is __not necessarily surjective__:
The type @b@ may contain many values that do not correspond to
a value of type @a@. Hence, 'load' has an 'Either' result.
(See Note [EitherSomeException] for the choice of exception type.)
However, retrieving a written value always succeeds, we have
prop> load . write = Right
* The embedding is __redundant__:
The type @b@ may contain multiple values that correspond to
one and the same @a@.
This is why the 'update' function expects the type @b@ as argument,
so that the right deltas can be computed.
Put differently, we often have
prop> write a ≠ b where Right a = load b
* The embedding of a delta __commutes with 'apply'__.
We have
> Right (apply da a) = load (apply (update a b da) b)
> where Right a = load b
However, since the embedding is redundant, we often have
prop> apply (update a (write a) da) (write a) ≠ write (apply da a)
-}
-- | Specification of an embedding of a type @a@ with delta types @da@
-- into the type @b@ with delta type @db@.
-- See [the discussion of @Embedding@](#doc:Embedding)
-- for a more detailed description.
data Embedding' da db where
Embedding'
:: (Delta da, Delta db, a ~ Base da, b ~ Base db) =>
{ load :: b -> Either SomeException a
, write :: a -> b
, update :: a -> b -> da -> db
} -> Embedding' da db
-- | 'Embedding' with efficient composition 'o'.
-- To construct an embedding, use 'mkEmbedding'.
data Embedding da db = Embedding
{ inject :: Base da -> Machine da db
, project :: Base db -> Either SomeException (Base da, Machine da db)
}
-- | Construct 'Embedding' with efficient composition
mkEmbedding :: Embedding' da db -> Embedding da db
mkEmbedding Embedding'{load,write,update} = Embedding
{ inject = start . write
, project = \b -> (, start b) <$> load b
}
where
start b = fromState step (b,())
step (a,da) (b,_) = (update a b da, ())
-- | Extract 'load', 'write', and 'update' functions
-- from an efficient 'Embedding'.
fromEmbedding :: (Delta da, Delta db) => Embedding da db -> Embedding' da db
fromEmbedding Embedding{inject,project} = Embedding'
{ load = fmap fst . project
, write = state_ . inject
, update = \a b da ->
let (_ ,mab) = from (project b)
(db,_ ) = step_ mab (a,da)
in db
}
where
from = fromRight (error "Embedding: 'load' violates expected laws")
-- | Efficient composition of 'Embedding'
instance Semigroupoid Embedding where
(Embedding inject2 project2) `o` (Embedding inject1 project1) =
Embedding{inject,project}
where
inject a =
let mab = inject1 a
mbc = inject2 (state_ mab)
in mbc `o` mab
project c = do
(b, mbc) <- project2 c
(a, mab) <- project1 b
pure (a, mbc `o` mab)
-- | A pair of 'Embedding's gives an embedding of pairs.
pair :: Embedding da1 db1 -> Embedding da2 db2 -> Embedding (da1,da2) (db1,db2)
pair (Embedding inject1 project1) (Embedding inject2 project2) =
Embedding{inject,project}
where
inject (a1,a2) = pairMachine (inject1 a1) (inject2 a2)
project (b1,b2) = do
(a1, m1) <- project1 b1
(a2, m2) <- project2 b2
pure ((a1,a2), pairMachine m1 m2)
-- | Lift a sequence of updates through an 'Embedding'.
--
-- > (b, dbs) = liftUpdates (mkEmbedding embedding') das a
-- > implies
-- > load embedding' b = Right (apply das a)
-- > b = apply dbs (write embedding' a)
liftUpdates
:: Delta da
=> Embedding da db
-> [da]
-- ^ List of deltas to apply.
-- The deltas are applied right-to-left; the 'head' is applied __last__.
-> Base da
-- ^ Base value to apply the deltas to.
-> (Base db, [db])
-- ^ (Final base value, updates that were applied ('head' is __last__)).
liftUpdates Embedding{inject} das0 a0 =
let (b,dbs) = go (inject a0) a0 (reverse das0) in (b, reverse dbs)
where
go machine1 _ [] = (state_ machine1, [])
go machine1 !a (da:das) = (b,db:dbs)
where
(b ,dbs) = go machine2 (apply da a) das
(db,machine2) = step_ machine1 (a,da)
-- | Having an 'apply' function is equivalent to the existence
-- of a canonical embedding into the trivial 'Replace' delta type.
replaceFromApply :: (Delta da, a ~ Base da) => Embedding' da (Replace a)
replaceFromApply = Embedding'
{ load = Right
, write = id
, update = \_ a da -> Replace (apply da a)
}
{-
-- | Use the 'update' function of an 'Embedding' to convert
-- one delta type to another.
--
-- This function assumes that the 'Embedding' argument satisfies
-- @load = Just@ and @write = id@.
applyWithEmbedding
:: (Delta db, a ~ Base db)
=> Embedding da db -> (da -> a -> a)
applyWithEmbedding e delta1 a = apply (update e a delta1) a
-}