packages feed

dep-t 0.4.4.0 → 0.4.5.0

raw patch · 6 files changed

+128/−82 lines, 6 filesdep ~doctestPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: doctest

API changes (from Hackage documentation)

+ Control.Monad.Dep.Has: asCall :: forall env m. env -> forall r_ x. Has r_ m env => (r_ m -> x) -> x
- Control.Monad.Dep.Has: class Has r_ d e | e -> d
+ Control.Monad.Dep.Has: class Has r_ m env | env -> m
- Control.Monad.Dep.Has: dep :: (Has r_ d e, Dep r_, HasField (DefaultFieldName r_) e u, Coercible u (r_ d)) => e -> r_ d
+ Control.Monad.Dep.Has: dep :: (Has r_ m env, Dep r_, HasField (DefaultFieldName r_) env u, Coercible u (r_ m)) => env -> r_ m

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for dep-t
 
+## 0.4.5.0
+
+* added "asCall" to Control.Monad.Dep.Has
+
 ## 0.4.4.0
 
 * added Control.Monad.Dep.Has, a generic "Has" typeclass which favors a style in which
README.md view
@@ -41,7 +41,7 @@     instance HasRepository IO EnvIO where
       repository = _repositoryIO
 
-Record-of-functions-in-IO is a simple technique which works well in many
+[Record-of-functions-in-IO](https://www.fpcomplete.com/blog/2017/06/readert-design-pattern/) is a simple technique which works well in many
 situations. There are even [specialized
 libraries](http://hackage.haskell.org/package/rio) that support it.
 
@@ -264,11 +264,26 @@   "call stack", or to implement something like Logback's [Mapped Diagnostic
   Context](http://logback.qos.ch/manual/mdc.html).
 
-- [RIO](http://hackage.haskell.org/package/rio) is a featureful ReaderT-like /
-  prelude replacement library which favors monomorphic environments.
+  So perhaps `DepT` will be overkill in a lot of cases, offering unneeded
+  flexibility. [This
+  gist](https://gist.github.com/danidiaz/358fdccdef51ad37bbec932631dcc4d2)
+  contains an example of `DepT`-less and `ReaderT`-less dependency injection.
+  It does use `Control.Monad.Dep.Has`, in combination with "open" and "closed"
+  versions of the environment record. Unlike in "Adventures..." the gist
+  doesn't use an extensible record for the environment but, to keep things
+  simple, a suitably parameterized conventional one.
 
 - Another exploration of dependency injection with `ReaderT`:
   [ReaderT-OpenProduct-Environment](https://github.com/keksnicoh/ReaderT-OpenProduct-Environment).
+
+- [The ReaderT design pattern](https://www.fpcomplete.com/blog/2017/06/readert-design-pattern/).
+
+> Your application code will, in general, live in ReaderT Env IO. Define it as type App = ReaderT Env IO if you wish, or use a newtype wrapper instead of ReaderT directly.
+
+> Optional: instead of directly using the App datatype, write your functions in terms of mtl-style typeclasses like MonadReader and MonadIO
+
+- [RIO](http://hackage.haskell.org/package/rio) is a featureful ReaderT-like /
+  prelude replacement library which favors monomorphic environments.
 
 - The [van Laarhoven Free Monad](http://r6.ca/blog/20140210T181244Z.html).
 
dep-t.cabal view
@@ -1,7 +1,7 @@ cabal-version:       3.0
 
 name:                dep-t
-version:             0.4.4.0
+version:             0.4.5.0
 synopsis:            Reader-like monad transformer for dependency injection.
 description:         Put all your functions in the environment record! Let all
                      your functions read from the environment record! No favorites!
@@ -81,5 +81,5 @@   build-depends:       
                        dep-t, 
                        rank2classes       ^>= 1.4.1,
-                       doctest            ^>= 0.17,
+                       doctest            ^>= 0.18,
 
lib/Control/Monad/Dep/Class.hs view
@@ -84,6 +84,7 @@ -- >>> :set -XFunctionalDependencies
 -- >>> :set -XFlexibleContexts
 -- >>> :set -XDataKinds
+-- >>> import Control.Monad.Reader
 -- >>> import Control.Monad.Dep
 -- >>> import Rank2 qualified
 -- >>> import Rank2.TH qualified
lib/Control/Monad/Dep/Has.hs view
@@ -11,97 +11,117 @@ {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE UndecidableInstances #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ViewPatterns #-}
 
--- | This module provides a generic \"Has\" class favoring a style in which the
--- components of the environment come wrapped in records or newtypes, instead
--- of being bare functions.
+-- | This module provides a general-purpose 'Has' class favoring a style in
+-- which the components of the environment, instead of being bare functions,
+-- are themselves records or newtypes containing functions.
 --
+-- In this style, the functions that are \"invoked\" from the environment are
+-- actually record field selectors. These selectors guide the 'Has' class to
+-- find the correct records in the environment.
+--
 -- >>> :{
 --  type Logger :: (Type -> Type) -> Type
---  newtype Logger d = Logger {log :: String -> d ()} deriving Generic
+--  newtype Logger d = Logger {log :: String -> d ()}
 --  instance Dep Logger where
 --    type DefaultFieldName Logger = "logger"
+--  --
 --  data Repository d = Repository
 --    { select :: String -> d [Int],
 --      insert :: [Int] -> d ()
---    } deriving Generic
+--    }
 --  instance Dep Repository where
 --    type DefaultFieldName Repository = "repository"
---  newtype Controller d = Controller {serve :: Int -> d String} deriving Generic
+--  --
+--  newtype Controller d = Controller {serve :: Int -> d String}
 --  instance Dep Controller where
 --    type DefaultFieldName Controller = "controller"
+--  --
 --  type Env :: (Type -> Type) -> Type
 --  data Env m = Env
 --    { logger :: Logger m,
 --      repository :: Repository m,
 --      controller :: Controller m
 --    }
---  instance Has Logger m (Env m)
---  instance Has Repository m (Env m)
---  instance Has Controller m (Env m)
---  mkController :: forall d e m. MonadDep [Has Logger, Has Repository] d e m => Controller m
+--  -- instance Has Logger m (Env m)
+--  -- instance Has Repository m (Env m)
+--  -- instance Has Controller m (Env m)
+--  :}
+--  
+-- 'Has' can be used in combination with 'MonadDep', like this:
+--
+-- >>> :{
+--  mkController :: MonadDep [Has Logger, Has Repository] d env m => Controller m
 --  mkController =
+--    Controller \url -> 
+--      useEnv \(asCall -> call) -> do
+--        call log "I'm going to insert in the db!"
+--        call select "select * from ..."
+--        call insert [1, 2, 3, 4]
+--        return "view"
+-- :}
+--
+-- 'Has' can also be used independently of 'MonadReader' or 'MonadDep'. Here
+-- for example the environment is passed as a plain function argument, and @m@
+-- doesn't have any constraint other than 'Monad':
+--
+-- >>> :{
+--  mkController' :: (Monad m, Has Logger m env, Has Repository m env) => env -> Controller m
+--  mkController' (asCall -> call) =
 --    Controller \url -> do
---      e <- ask
---      liftD $ log (dep e) "I'm going to insert in the db!"
---      liftD $ select (dep e) "select * from ..."
---      liftD $ insert (dep e) [1, 2, 3, 4]
+--      call log "I'm going to insert in the db!"
+--      call select "select * from ..."
+--      call insert [1, 2, 3, 4]
 --      return "view"
 -- :}
 --
--- The @adviseRecord@ and @deceiveRecord@ functions from the companion package
--- \"dep-t-advice\" can facilitate working with this style of components.
 --
 module Control.Monad.Dep.Has (
-        -- * A generic \"Has\"
-        Has (..), 
+        -- * A general-purpose Has
+        Has (..) 
+        -- * call helper
+    ,   asCall
         -- * Component defaults
-        Dep (..)
+    ,   Dep (..)
+--    ,   useCall
     ) where
 
 import Data.Kind
 import GHC.Records
 import GHC.TypeLits
 import Data.Coerce
-
--- $setup
---
--- >>> :set -XTypeApplications
--- >>> :set -XMultiParamTypeClasses
--- >>> :set -XImportQualifiedPost
--- >>> :set -XTemplateHaskell
--- >>> :set -XStandaloneKindSignatures
--- >>> :set -XNamedFieldPuns
--- >>> :set -XFunctionalDependencies
--- >>> :set -XFlexibleContexts
--- >>> :set -XDataKinds
--- >>> :set -XRankNTypes
--- >>> :set -XBlockArguments
--- >>> :set -XFlexibleInstances
--- >>> :set -XTypeFamilies
--- >>> :set -XDeriveGeneric
--- >>> import Control.Monad.Dep
--- >>> import Rank2 qualified
--- >>> import Rank2.TH qualified
--- >>> import GHC.Generics (Generic)
---
-
+-- import Control.Monad.Reader
+-- import Control.Monad.Dep.Class
 
 -- | A generic \"Has\" class. When partially applied to a parametrizable
--- record-of-functions @r_@, produces a 2-place constraint that can be later
--- used with "Control.Monad.Dep.Class".
+-- record-of-functions @r_@, produces a 2-place constraint that can used on its
+-- own, or with "Control.Monad.Dep.Class".
 type Has :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Constraint
-class Has r_ d e | e -> d where
-  -- |  Given an environment @e@, produce a record-of-functions parameterized by the environment's effect monad @d@.
+class Has r_ m env | env -> m where
+  -- |  Given an environment @e@, produce a record-of-functions parameterized by the environment's effect monad @m@.
   --
   -- The hope is that using a selector function on the resulting record will
-  -- determine its type without the need for type annotations.
+  -- fix the record's type without the need for type annotations.
   --
-  -- (This will likely not play well with RecordDotSyntax. See also <https://chrisdone.com/posts/import-aliases-field-names/ this trick>.)
-  dep :: e -> r_ d
-  default dep :: (Dep r_, HasField (DefaultFieldName r_) e u, Coercible u (r_ d)) => e -> r_ d
-  dep e = coerce . getField @(DefaultFieldName r_) $ e
+  -- (This will likely not play well with RecordDotSyntax. See also <https://chrisdone.com/posts/import-aliases-field-names/ this import alias trick for avoiding name collisions>.)
+  dep :: env -> r_ m
+  default dep :: (Dep r_, HasField (DefaultFieldName r_) env u, Coercible u (r_ m)) => env -> r_ m
+  dep env = coerce . getField @(DefaultFieldName r_) $ env
 
+-- | Transforms an environment with suitable 'Has' instances into a \"helper\"
+--   function that looks in the environment for the arguments of other functions.
+--   Typically, the \"helped\" functions will be record field selectors.
+--
+--   In practice, this means that you can write @call foo@ instead of @foo (dep
+--   env)@.
+--
+--   Using 'asCall' in a view pattern avoids having to name the
+--   environment.
+asCall :: forall env m . env -> forall r_ x. Has r_ m env => (r_ m -> x) -> x
+asCall env = \f -> f (dep env)
+
 -- | Parametrizable records-of-functions can be given an instance of this
 -- typeclass to specify the default field name 'Has' expects for the component
 -- in the environment record.
@@ -114,19 +134,25 @@   -- k type and use it as the default preferred field name.
   type DefaultFieldName r_ :: Symbol
 
--- -- Doesn't make much sense to have this, we already have Has!
--- type Sub :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> Constraint
--- class Sub sub super
--- type SubWrapper :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type
--- data SubWrapper sub super d e = SubWrapper e
-
--- type Nested :: ((Type -> Type) -> Type) -> ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type
--- data Nested sub super d e = Nested e
--- 
--- instance (Has sub d e, Has super d (sub d)) => Has super d (Nested sub super d e) where
---     dep (Nested e) = dep @super (dep @sub e)
-
--- Possible example
--- instance Has ReadRef IO (Env IO) via (Nested Ref ReadRef IO (Env IO))
+-- $setup
+--
+-- >>> :set -XTypeApplications
+-- >>> :set -XMultiParamTypeClasses
+-- >>> :set -XImportQualifiedPost
+-- >>> :set -XTemplateHaskell
+-- >>> :set -XStandaloneKindSignatures
+-- >>> :set -XNamedFieldPuns
+-- >>> :set -XFunctionalDependencies
+-- >>> :set -XFlexibleContexts
+-- >>> :set -XDataKinds
+-- >>> :set -XBlockArguments
+-- >>> :set -XFlexibleInstances
+-- >>> :set -XTypeFamilies
+-- >>> :set -XDeriveGeneric
+-- >>> :set -XViewPatterns
+-- >>> import Data.Kind
+-- >>> import Control.Monad.Dep
+-- >>> import GHC.Generics (Generic)
+--
 
 
test/tests_has.hs view
@@ -76,13 +76,12 @@ 
 mkController :: forall d e m. MonadDep [Has Logger, Has Repository] d e m => Controller m
 mkController =
-  Controller \url -> do
-    e <- ask
-    liftD $ log (dep e) "I'm going to insert in the db!"
-    -- liftD $ (dep e).log "I'm going to insert in the db!" -- Once RecordDotSyntax arrives...
-    liftD $ select (dep e) "select * from ..."
-    liftD $ insert (dep e) [1, 2, 3, 4]
-    return "view"
+  Controller \url -> 
+    useEnv \(asCall -> call) -> do
+      call log "I'm going to insert in the db!"
+      call select "select * from ..."
+      call insert [1, 2, 3, 4]
+      return "view"
 
 -- also toss in this helper function
 -- useEnv :: forall d e m r. (LiftDep d m, MonadReader e m) => (e -> d r) -> m r
@@ -95,9 +94,10 @@ mkController' =
   Controller \url ->
     useEnv \e -> do
-      log (dep e) "I'm going to insert in the db!"
-      select (dep e) "select * from ..."
-      insert (dep e) [5, 3, 43]
+      let (asCall -> call) = e
+      call log "I'm going to insert in the db!"
+      call select "select * from ..."
+      call insert [5, 3, 43]
       return "view"
 
 type EnvIO :: Type
@@ -123,8 +123,8 @@         liftD $ log (dep e) "I'm going to select an entity"
         return [],
       insert = \entity -> do
-        e <- ask
-        liftD $ log (dep e) "I'm going to write the entity!"
+        (asCall -> call) <- ask
+        liftD $ call log "I'm going to write the entity!"
         tell ([], entity)
     }