opaleye-0.10.2.0: src/Opaleye/Internal/PackMap.hs
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TupleSections #-}
module Opaleye.Internal.PackMap where
import qualified Opaleye.Internal.Tag as T
import qualified Opaleye.Internal.HaskellDB.PrimQuery as HPQ
import Control.Applicative (liftA2)
import Control.Arrow (first, second)
import qualified Control.Monad.Trans.State as State
import Data.Profunctor (Profunctor, dimap, rmap)
import Data.Profunctor.Product (ProductProfunctor)
import qualified Data.Profunctor.Product as PP
import qualified Data.Functor.Identity as I
-- This is rather like a Control.Lens.Traversal with the type
-- parameters switched but I'm not sure if it should be required to
-- obey the same laws.
--
-- TODO: We could attempt to generalise this to
--
-- data LensLike f a b s t = LensLike ((a -> f b) -> s -> f t)
--
-- i.e. a wrapped, argument-flipped Control.Lens.LensLike
--
-- This would allow us to do the Profunctor and ProductProfunctor
-- instances (requiring just Functor f and Applicative f respectively)
-- and share them between many different restrictions of f. For
-- example, TableColumnMaker is like a Setter so we would restrict f
-- to the Distributive case.
-- | A 'PackMap' @a@ @b@ @s@ @t@ encodes how an @s@ contains an
-- updatable sequence of @a@ inside it. Each @a@ in the sequence can
-- be updated to a @b@ (and the @s@ changes to a @t@ to reflect this
-- change of type).
--
-- 'PackMap' is just like a @Traversal@ from the lens package.
-- 'PackMap' has a different order of arguments to @Traversal@ because
-- it typically needs to be made a 'Profunctor' (and indeed
-- 'ProductProfunctor') in @s@ and @t@. It is unclear at this point
-- whether we want the same @Traversal@ laws to hold or not. Our use
-- cases may be much more general.
newtype PackMap a b s t =
PackMap (forall f. Applicative f => (a -> f b) -> s -> f t)
-- | Replaces the targeted occurrences of @a@ in @s@ with @b@ (changing
-- the @s@ to a @t@ in the process). This can be done via an
-- 'Applicative' action.
--
-- 'traversePM' is just like @traverse@ from the @lens@ package.
-- 'traversePM' used to be called @packmap@.
traversePM :: Applicative f => PackMap a b s t -> (a -> f b) -> s -> f t
traversePM (PackMap f) = f
-- | Modify the targeted occurrences of @a@ in @s@ with @b@ (changing
-- the @s@ to a @t@ in the process).
--
-- 'overPM' is just like @over@ from the @lens@ package.
overPM :: PackMap a b s t -> (a -> b) -> s -> t
overPM p f = I.runIdentity . traversePM p (I.Identity . f)
-- {
-- | A helpful monad for writing columns in the AST
type PM a = State.State (a, Int)
new :: PM a String
new = do
(a, i) <- State.get
State.put (a, i + 1)
return (show i)
write :: a -> PM [a] ()
write a = do
(as, i) <- State.get
State.put (as ++ [a], i)
run :: PM [a] r -> (r, [a])
run m = (r, as)
where (r, (as, _)) = State.runState m ([], 0)
-- }
-- { General functions for writing columns in the AST
-- | Make a fresh name for an input value (the variable @primExpr@
-- type is typically actually a 'HPQ.PrimExpr') based on the supplied
-- function and the unique 'T.Tag' that is used as part of our
-- @QueryArr@.
--
-- Add the fresh name and the input value it refers to the list in
-- the state parameter.
extractAttrPE :: (primExpr -> String -> String)
-> T.Tag
-> primExpr
-> PM [(HPQ.Symbol, primExpr)] HPQ.PrimExpr
extractAttrPE mkName t pe = do
i <- new
let s = HPQ.Symbol (mkName pe i) t
write (s, pe)
return (HPQ.AttrExpr s)
-- | As 'extractAttrPE' but ignores the 'primExpr' when making the
-- fresh column name and just uses the supplied 'String' and 'T.Tag'.
extractAttr :: String
-> T.Tag
-> primExpr
-> PM [(HPQ.Symbol, primExpr)] HPQ.PrimExpr
extractAttr s = extractAttrPE (const (s ++))
isoState ::
Functor m =>
(s1 -> s2) ->
(s2 -> s1) ->
State.StateT s1 m a ->
State.StateT s2 m a
isoState to from =
State.StateT . ((fmap . second) to .) . (. from) . State.runStateT
extract :: String -> T.Tag -> PM [HPQ.Symbol] HPQ.PrimExpr
extract s t = isoState to from (extractAttr s t ())
where
to = (first . fmap) fst
from = (first . fmap) (\x -> (x, ()))
-- }
eitherFunction :: (PP.SumProfunctor p, Functor f)
=> p a (f b)
-> p a' (f b')
-> p (Either a a') (f (Either b b'))
eitherFunction f g = rmap (either (fmap Left) (fmap Right)) (f PP.+++! g)
-- | Like 'Control.Lens.Iso.iso'. In practice it won't actually be
-- used as an isomorphism, but it seems to be appropriate anyway.
iso :: (s -> a) -> (b -> t) -> PackMap a b s t
iso h g = PackMap (dimap h (fmap g))
-- {
-- Boilerplate instance definitions. There's no choice here apart
-- from the order in which the applicative is applied.
instance Functor (PackMap a b s) where
fmap f (PackMap g) = PackMap ((fmap . fmap . fmap) f g)
instance Applicative (PackMap a b s) where
pure x = PackMap (pure (pure (pure x)))
PackMap f <*> PackMap x = PackMap (liftA2 (liftA2 (<*>)) f x)
instance Profunctor (PackMap a b) where
dimap f g (PackMap q) = PackMap (fmap (dimap f (fmap g)) q)
instance ProductProfunctor (PackMap a b) where
purePP = pure
(****) = (<*>)
instance PP.SumProfunctor (PackMap a b) where
PackMap f +++! PackMap g = PackMap (\x -> eitherFunction (f x) (g x))
-- }