packages feed

dep-t-advice 0.4.6.1 → 0.4.7.0

raw patch · 5 files changed

+371/−6 lines, 5 filesdep +aesondep +containersdep +textdep ~dep-tPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: aeson, containers, text

Dependency ranges changed: dep-t

API changes (from Hackage documentation)

- Control.Monad.Dep.Advice: instance Control.Monad.Dep.Advice.Gullible as e e_ m r curried => Control.Monad.Dep.Advice.Gullible (a : as) e e_ m r (a -> curried)
- Control.Monad.Dep.Advice: instance Control.Monad.Dep.Advice.Multicurryable as e_ m r curried => Control.Monad.Dep.Advice.Multicurryable (a : as) e_ m r (a -> curried)
+ Control.Monad.Dep.Advice: component :: forall e_ m record. (Applicative m, DistributiveRecord e_ m record) => (e_ (DepT e_ m) -> record (DepT e_ m)) -> record (DepT e_ m)
+ Control.Monad.Dep.Advice: distributeDepT :: forall e_ m record. DistributiveRecord e_ m record => DepT e_ m (record (DepT e_ m)) -> record (DepT e_ m)
+ Control.Monad.Dep.Advice: instance (GHC.Base.Functor m, Control.Monad.Dep.Advice.Gullible as e e_ m r curried) => Control.Monad.Dep.Advice.Gullible (a : as) e e_ m r (a -> curried)
+ Control.Monad.Dep.Advice: instance (GHC.Base.Functor m, Control.Monad.Dep.Advice.Multicurryable as e_ m r advised) => Control.Monad.Dep.Advice.DistributiveProduct e_ m (GHC.Generics.S1 ('GHC.Generics.MetaSel msymbol su ss ds) (GHC.Generics.Rec0 advised))
+ Control.Monad.Dep.Advice: instance (GHC.Base.Functor m, Control.Monad.Dep.Advice.Multicurryable as e_ m r curried) => Control.Monad.Dep.Advice.Multicurryable (a : as) e_ m r (a -> curried)
+ Control.Monad.Dep.Advice: instance (GHC.Generics.Generic (advised (Control.Monad.Dep.DepT e_ m)), GHC.Generics.Rep (advised (Control.Monad.Dep.DepT e_ m)) GHC.Types.~ GHC.Generics.D1 x (GHC.Generics.C1 y advised_), Control.Monad.Dep.Advice.DistributiveProduct e_ m advised_, GHC.Base.Functor m) => Control.Monad.Dep.Advice.DistributiveRecord e_ m advised
+ Control.Monad.Dep.Advice: instance forall k (e_ :: (* -> *) -> *) (m :: * -> *) (advised_left :: k -> *) (advised_right :: k -> *). (Control.Monad.Dep.Advice.DistributiveProduct e_ m advised_left, Control.Monad.Dep.Advice.DistributiveProduct e_ m advised_right, GHC.Base.Functor m) => Control.Monad.Dep.Advice.DistributiveProduct e_ m (advised_left GHC.Generics.:*: advised_right)

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for dep-t-advice
 
+## 0.4.7.0 
+
+* Added 'distributeDepT' and 'component'.
+
 ## 0.4.6.1 
 
 * Modified a signature to make it compile with GHC 9.0.1.
README.md view
@@ -118,6 +118,45 @@ works on functions that end on a `DepT` action. Also, advice might depend on
 the full gamut of functionality stored in the environment.
 
+## Historical aside
+
+According to Wikipedia, the term "advice" in the sense of aspect-oriented
+programming goes back to 1966. Quoting from [PILOT: A Step Toward Man-Computer
+Symbiosis](http://publications.csail.mit.edu/ai/browse/0200browse.shtml):
+
+> There are two ways a user can modify programs in this subjective model of
+> programming: he can modify the interface between procedures, or he can modify
+> the procedure itself. (Since procedures are themselves made up of procedures,
+> modifying a procedure at one level may correspond to modifying the interface
+> between procedures at a lower level.) Modifying the interface between
+> procedures is called advising. Modifying a procedure itself is editing.
+
+> Advising is the basic innovation in the model, and in the PILOT system.
+> Advising consists of inserting new procedures at any or all of the entry or
+> exit points to a particular procedure (or class of procedures). The
+> procedures inserted are called "advice procedures" or simply "advice".
+
+> Since each piece of advice is itself a procedure, it has its own entries and
+> exits. In particular, this means that the execution of advice can cause the
+> procedure that it modifies to be bypassed completely, e.g., by specifying as
+> an exit from the advice one of the exits from the original procedure; or the
+> advice may change essential variables and continue with the computation so
+> that the original procedure is executed, but with modified variables.
+> Finally, the advice may not alter the execution or affect the original
+> procedure at all, e.g., it may merely perform some additional computation
+> such as printing a message or recording history. Since advice can be
+> conditional, the decision as to what is to be done can depend on the results
+> of the computation up to that point.
+
+> The principal advantage of advising is that the user need not be concerned
+> about the details of the actual changes in his program, nor the internal
+> representation of advice. He can treat the procedure to be advised *as a
+> unit*, a single block, and make changes to it without concern for the
+> particulars of this block. This may be contrasted with editing in which the
+> programmer must be cognizant of the internal structure of the procedure.
+
+[tweet](https://twitter.com/DiazCarrete/status/1446783366678949891).
+
 ## Links
 
 - [Aspect Oriented Programming with
@@ -132,4 +171,14 @@   Patterns](https://www.goodreads.com/book/show/44416307-dependency-injection-principles-practices-and-patterns)
   This is a good book on the general princples of DI. 
 
+- [variadic-function](https://hackage.haskell.org/package/variadic-function) a
+  Hackage library which also deals with functions of any number of elements.
+  [Reddit
+  thread](https://www.reddit.com/r/haskell/comments/oeyaz2/ann_typeablemock_mocks_that_can_fit_into_any/).
+
+- [Inferring the arity of zipWith, with lots of type-level hackery](https://www.youtube.com/watch?v=iGSKqcebhfs&t=957s). YouTube video.
+
+- [Functional Pearl: The Decorator Pattern in Haskell](https://twitter.com/DiazCarrete/status/1403985337513394178)
+
+  > An arity-generic decorator needs to solve two problems: intercept recursive calls and handle functions of any arity uniformly
 
dep-t-advice.cabal view
@@ -1,7 +1,7 @@ cabal-version:       3.0
 
 name:                dep-t-advice
-version:             0.4.6.1
+version:             0.4.7.0
 synopsis:            Giving good advice to functions in a DepT environment.
 description:         Companion to the dep-t package. Easily add behaviour to functions living in a DepT environment, 
                      whatever the number of arguments they might have.
@@ -24,7 +24,7 @@   build-depends:       base >=4.10.0.0 && < 5,
                        sop-core ^>= 0.5.0.0,
                        transformers ^>= 0.5.0.0,
-                       dep-t ^>= 0.4.4.0,
+                       dep-t ^>= 0.4.6.0,
   default-language:    Haskell2010
 
 common common-tests
@@ -35,7 +35,7 @@                        transformers       ^>= 0.5.0.0,
                        mtl                ^>= 2.2,
                        template-haskell,
-
+                       text,
 
 common common-tasty
   import:              common-tests
@@ -61,6 +61,13 @@   main-is:             weird-advice-tests.hs
   build-depends:       
                        barbies            ^>= 2.0.2.0
+
+test-suite tests-has-conversion
+  import:              common-tasty
+  type:                exitcode-stdio-1.0
+  main-is:             tests-has-conversion.hs
+  build-depends:       aeson,
+                       containers
 
 -- VERY IMPORTANT for doctests to work: https://stackoverflow.com/a/58027909/1364288
 -- http://hackage.haskell.org/package/cabal-doctest
lib/Control/Monad/Dep/Advice.hs view
@@ -93,7 +93,7 @@     runFinalDepT,
     runFromEnv,
     runFromDep,
-
+    -- askFinalDepT,
     -- * Making functions see a different environment
     deceive,
 
@@ -101,6 +101,9 @@     -- $records
     adviseRecord,
     deceiveRecord,
+    -- * Plugging Has-based constructors
+    distributeDepT,
+    component,
 
     -- * "sop-core" re-exports
     -- $sop
@@ -116,6 +119,7 @@ 
 import Control.Monad.Dep
 import Control.Monad.Dep.Has
+import Control.Monad.Dep.Env
 import Control.Monad.Trans.Reader (ReaderT (..), withReaderT)
 import Data.Functor.Identity
 import Data.Kind
@@ -126,6 +130,7 @@ import Data.Typeable
 import GHC.Generics qualified as G
 import GHC.TypeLits
+import Data.Coerce
 
 -- $setup
 --
@@ -419,6 +424,7 @@   multiuncurry :: curried -> NP I as -> DepT e_ m r
   multicurry :: (NP I as -> DepT e_ m r) -> curried
   _runFromEnv :: m (e_ (DepT e_ m)) -> (e_ (DepT e_ m) -> curried) -> DownToBaseMonad as e_ m r curried
+  _askFinalDepT :: (e_ (DepT e_ m) -> m curried) -> curried
 
 instance Monad m => Multicurryable '[] e_ m r (DepT e_ m r) where
   type DownToBaseMonad '[] e_ m r (DepT e_ m r) = m r
@@ -427,12 +433,19 @@   _runFromEnv producer extractor = do
     e <- producer
     runDepT (extractor e) e
+  _askFinalDepT f = do
+    env <- ask
+    r <- lift (f env)
+    r
 
-instance Multicurryable as e_ m r curried => Multicurryable (a ': as) e_ m r (a -> curried) where
+instance (Functor m, Multicurryable as e_ m r curried) => Multicurryable (a ': as) e_ m r (a -> curried) where
   type DownToBaseMonad (a ': as) e_ m r (a -> curried) = a -> DownToBaseMonad as e_ m r curried
   multiuncurry f (I a :* as) = multiuncurry @as @e_ @m @r @curried (f a) as
   multicurry f a = multicurry @as @e_ @m @r @curried (f . (:*) (I a))
   _runFromEnv producer extractor a = _runFromEnv @as @e_ @m @r @curried producer (\f -> extractor f a)
+  _askFinalDepT f = 
+    let switcheroo action a = fmap ($ a) action
+     in _askFinalDepT @as @e_ @m @r . flip (fmap switcheroo f)
 
 -- | Given a base monad @m@ action that gets hold of the 'DepT' environment, run
 -- the 'DepT' transformer at the tip of a curried function.
@@ -454,6 +467,12 @@   DownToBaseMonad as e_ m r curried
 runFinalDepT producer extractor = _runFromEnv producer (const extractor)
 
+askFinalDepT ::
+  forall as e_ m r curried. 
+  Multicurryable as e_ m r curried =>
+  (e_ (DepT e_ m) -> m curried) -> curried
+askFinalDepT = _askFinalDepT @as @e_ @m @r
+
 -- | Given a base monad @m@ action that gets hold of the 'DepT' environment,
 -- and a function capable of extracting a curried function from the
 -- environment, run the 'DepT' transformer at the tip of the resulting curried
@@ -595,7 +614,7 @@   type NewtypedEnv '[] e e_ m r (DepT e_ m r) = ReaderT e m r
   _deceive f action = DepT (withReaderT f action)
 
-instance Gullible as e e_ m r curried => Gullible (a ': as) e e_ m r (a -> curried) where
+instance (Functor m, Gullible as e e_ m r curried) => Gullible (a ': as) e e_ m r (a -> curried) where
   type NewtypedEnv (a ': as) e e_ m r (a -> curried) = a -> NewtypedEnv as e e_ m r curried
   _deceive f g a = deceive @as @e @e_ @m @r f (g a)
 
@@ -769,6 +788,80 @@   -- | The deceived record.
   gullible (DepT e_ m)
 deceiveRecord = _deceiveRecord @e @e_ @m @gullible
+
+
+-- | Having a 'DepT' action that returns a record-of-functions with effects in
+-- 'DepT' is the same as having the record itself, because we can obtain the initial
+-- environment by 'ask'ing for it in each member function.
+distributeDepT 
+    :: forall e_ m record . DistributiveRecord e_ m record => 
+    -- | 'DepT' action that returns the component
+    DepT e_ m (record (DepT e_ m)) ->
+    -- | component whose methods get the environment by 'ask'ing.
+    record (DepT e_ m)
+distributeDepT (DepT (ReaderT action)) = _distribute @e_ @m @record action
+
+-- | Given a constructor that returns a record-of-functions with effects in 'DepT',
+-- produce a record in which the member functions 'ask' for the environment themselves.
+--
+-- You must have a sufficiently polymorphic constructor—both in the monad and
+-- in the environment—to invoke this function.
+--
+-- 'component' lets you plug simple component constructors 
+-- into a 'DepT'-based environment.
+--
+-- Compare with 'Control.Monad.Dep.Env.constructor' from "Control.Monad.Dep.Env", which 
+-- is intended to be used with 'Control.Monad.Dep.Env.fixEnv'-based environments.
+component 
+    :: forall e_ m record . (Applicative m, DistributiveRecord e_ m record) => 
+    -- | constructor which takes the environment as a positional parameter.
+    (e_ (DepT e_ m) -> record (DepT e_ m)) ->
+    -- | component whose methods get the environment by 'ask'ing.
+    record (DepT e_ m)
+component f = _distribute @e_ @m (pure . f)
+
+
+
+type DistributiveRecord :: ((Type -> Type) -> Type) -> (Type -> Type) -> ((Type -> Type) -> Type) -> Constraint
+class DistributiveRecord e_ m record where
+    _distribute :: (e_ (DepT e_ m) -> m (record (DepT e_ m))) -> record (DepT e_ m)
+
+type DistributiveProduct :: ((Type -> Type) -> Type) -> (Type -> Type) -> (k -> Type) -> Constraint
+class DistributiveProduct e_ m product where
+    _distributeProduct :: (e_ (DepT e_ m) -> m (product k)) -> product k
+
+instance
+  ( G.Generic (advised (DepT e_ m)),
+    G.Rep (advised (DepT e_ m)) ~ G.D1 x (G.C1 y advised_),
+    DistributiveProduct e_ m advised_,
+    Functor m
+  ) =>
+  DistributiveRecord e_ m advised
+  where
+  _distribute f =
+    let advised_ = _distributeProduct @_ @e_ @m (fmap (fmap (G.unM1 . G.unM1 . G.from)) f)
+     in G.to (G.M1 (G.M1 advised_))
+
+instance
+  ( DistributiveProduct e_ m advised_left,
+    DistributiveProduct e_ m advised_right,
+    Functor m
+  ) =>
+  DistributiveProduct e_ m (advised_left G.:*: advised_right)
+  where
+  _distributeProduct f  = 
+      _distributeProduct @_ @e_ @m (fmap (fmap (\(l G.:*: _) -> l)) f) 
+      G.:*: 
+      _distributeProduct @_ @e_ @m (fmap (fmap (\(_ G.:*: r) -> r)) f) 
+
+instance
+  ( 
+    Functor m, 
+    Multicurryable as e_ m r advised
+  ) =>
+  DistributiveProduct e_ m (G.S1 ( 'G.MetaSel msymbol su ss ds) (G.Rec0 advised))
+  where
+  _distributeProduct f = G.M1 . G.K1 $ askFinalDepT @as @e_ @m @r (fmap (fmap (G.unK1 . G.unM1))  f)
 
 -- advising *all* fields of a record
 --
+ test/tests-has-conversion.hs view
@@ -0,0 +1,212 @@+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE ImportQualifiedPost #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneKindSignatures #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE ViewPatterns #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+
+module Main (main) where
+
+import Control.Monad.Dep
+import Control.Monad.Dep.Has
+import Control.Monad.Dep.Env
+import Control.Monad.Dep.Advice
+import Control.Monad.Dep.Advice.Basic
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.RWS
+import Data.Kind
+import Data.List (intercalate,lookup)
+import Rank2 qualified
+import Rank2.TH qualified
+import Test.Tasty
+import Test.Tasty.HUnit
+import Prelude hiding (log)
+import Data.Proxy
+import System.IO
+import GHC.Generics (Generic)
+import Data.Functor.Identity
+import qualified GHC.Generics as G
+import System.IO
+import Control.Exception
+import Control.Arrow (Kleisli (..))
+import Data.Text qualified as Text
+import Data.Function ((&))
+import Data.Functor ((<&>), ($>))
+import Data.String
+import Data.Aeson
+import Data.Aeson.Types
+import Control.Monad.Trans.Cont
+import Data.Functor.Compose
+import Data.IORef
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+
+
+--
+--
+--
+
+type Logger :: (Type -> Type) -> Type
+newtype Logger d = Logger {
+    info :: String -> d ()
+  } deriving stock Generic
+
+data Repository d = Repository
+  { findById :: Int -> d (Maybe String)
+  , putById :: Int -> String -> d ()
+  , insert :: String -> d Int
+  } deriving stock Generic
+
+data Controller d = Controller 
+  { create :: d Int
+  , append :: Int -> String -> d Bool 
+  , inspect :: Int -> d (Maybe String)
+  } deriving stock Generic
+
+type MessagePrefix = Text.Text
+
+data LoggerConfiguration = LoggerConfiguration { 
+        messagePrefix :: MessagePrefix
+    } deriving stock (Show, Generic)
+      deriving anyclass FromJSON
+
+makeStdoutLogger :: MonadIO m => MessagePrefix -> env -> Logger m
+makeStdoutLogger prefix _ = Logger (\msg -> liftIO (putStrLn (Text.unpack prefix ++ msg)))
+
+makeInMemoryRepository 
+    :: (Has Logger m env, MonadIO m) 
+    => IORef (Map Int String) 
+    -> env 
+    -> Repository m
+makeInMemoryRepository ref (asCall -> call) = do
+    Repository {
+         findById = \key -> do
+            call info "I'm going to do a lookup in the map!"
+            theMap <- liftIO $ readIORef ref
+            pure (Map.lookup key theMap)
+       , putById = \key content -> do
+            theMap <- liftIO $ readIORef ref
+            liftIO $ writeIORef ref $ Map.insert key content theMap 
+       , insert = \content -> do 
+            call info "I'm going to insert in the map!"
+            theMap <- liftIO $ readIORef ref
+            let next = Map.size theMap
+            liftIO $ writeIORef ref $ Map.insert next content theMap 
+            pure next
+    }
+
+makeController :: forall m env . (Has Logger m env, Has Repository m env, Monad m) => env -> Controller m
+makeController (asCall -> call) = Controller {
+      create = do
+          call info "Creating a new empty resource."
+          key <- call insert ""
+          pure key
+    , append = \key extra -> do
+          call info "Appending to a resource"
+          mresource <- call findById key
+          case mresource of
+            Nothing -> do
+                pure False
+            Just resource -> do
+                call putById key (resource ++ extra) 
+                pure True
+    , inspect = \key -> do
+          call findById key 
+    }
+
+-- from purely Has-using to MonadDep-using
+-- this is very verbose, how to automate it?
+-- makeController'' :: forall e_ m . (Has Logger (DepT e_ m) (e_ (DepT e_ m)), Has Repository (DepT e_ m) (e_ (DepT e_ m)), Monad m) => Controller (DepT e_ m)
+-- makeController'' = Controller {
+--         create = askFinalDepT $ fmap create makeController
+--       , append = askFinalDepT $ fmap append makeController
+--       , inspect = askFinalDepT $ fmap inspect makeController
+--     }
+
+allocateMap :: ContT () IO (IORef (Map Int String))
+allocateMap = ContT $ bracket (newIORef Map.empty) pure
+
+makeController''' :: forall e_ m . (Has Logger (DepT e_ m) (e_ (DepT e_ m)), Has Repository (DepT e_ m) (e_ (DepT e_ m)), Monad m) => Controller (DepT e_ m)
+makeController''' = component makeController
+
+type EnvHKD :: (Type -> Type) -> (Type -> Type) -> Type
+data EnvHKD h m = EnvHKD
+  { logger :: h (Logger m),
+    repository :: h (Repository m),
+    controller :: h (Controller m)
+  } deriving stock Generic
+    deriving anyclass (Phased, DemotableFieldNames, FieldsFindableByType)
+
+deriving via Autowired (EnvHKD Identity m) instance Autowireable r_ m (EnvHKD Identity m) => Has r_ m (EnvHKD Identity m)
+
+parseConf :: FromJSON a => Configurator a
+parseConf = Kleisli parseJSON
+
+type Configurator = Kleisli Parser Value 
+
+type Allocator = ContT () IO
+
+type Phases = Configurator `Compose` Allocator `Compose` Identity
+
+env :: EnvHKD Phases (DepT (EnvHKD Identity) IO)
+env = EnvHKD {
+      logger = 
+        parseConf `bindPhase` \(LoggerConfiguration {messagePrefix}) -> 
+        skipPhase @Allocator $
+        pure $ component (makeStdoutLogger messagePrefix)
+    , repository = 
+        skipPhase @Configurator $
+        allocateMap `bindPhase` \ref -> 
+        pure $ component (makeInMemoryRepository ref)
+    , controller = 
+        skipPhase @Configurator $
+        skipPhase @Allocator $ 
+        pure $ component makeController
+}
+
+testEnvConstruction :: Assertion
+testEnvConstruction = do
+    let parseResult = eitherDecode' (fromString "{ \"logger\" : { \"messagePrefix\" : \"[foo]\" }, \"repository\" : null, \"controller\" : null }")
+    print parseResult 
+    let Right value = parseResult 
+        Kleisli (withObject "configuration" -> parser) = 
+              pullPhase @(Kleisli Parser Object) 
+            $ mapPhaseWithFieldNames 
+                (\fieldName (Kleisli f) -> Kleisli \o -> explicitParseField f o (fromString fieldName)) 
+            $ env
+        Right allocators = parseEither parser value 
+    runContT (pullPhase @Allocator allocators) \(pure -> deppie) -> do
+        resourceId <- runFromDep deppie create
+        runFromDep deppie append resourceId "foo"
+        runFromDep deppie append resourceId "bar"
+        Just result <- runFromDep deppie inspect resourceId
+        assertEqual "" "foobar" $ result
+
+tests :: TestTree
+tests =
+  testGroup
+    "All"
+    [
+     testCase "environmentConstruction" testEnvConstruction
+    ]
+
+main :: IO ()
+main = defaultMain tests