dep-t-0.6.2.0: lib/Dep/Has.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE TypeOperators #-}
-- | 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.
--
-- >>> :{
-- 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 ()
-- }
-- instance Dep Repository where
-- type DefaultFieldName Repository = "repository"
-- --
-- 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)
-- :}
--
-- '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
-- call log "I'm going to insert in the db!"
-- call select "select * from ..."
-- call insert [1, 2, 3, 4]
-- return "view"
-- :}
--
--
module Dep.Has (
-- * A general-purpose Has
Has (..)
, HasAll
-- * call helper
, asCall
-- * Component defaults
, Dep (..)
) where
import Data.Kind
import GHC.Records
import GHC.TypeLits
import Data.Coerce
-- 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
-- saying that the environment @e@ has the record @r_@ with effect monad @m@.
--
-- The constraint can be used on its own, or with "Control.Monad.Dep.Class".
type Has :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Constraint
class Has r_ (m :: Type -> Type) (env :: Type) | 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
-- 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 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
-- | When partially applied to a type-level list @rs_@ of parametrizable records-of-functions,
-- produces a 2-place constraint saying that the environment @e@ has all the
-- records @rs_@ with effect monad @m@.
type HasAll :: [(Type -> Type) -> Type] -> (Type -> Type) -> Type -> Constraint
type family HasAll rs_ m e where
HasAll '[] m e = ()
HasAll (r_ : rs_) m e = (Has r_ m e, HasAll rs_ m e)
-- | 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:
--
-- >>> :{
-- data SomeRecord m = SomeRecord { someSelector :: String -> m () }
-- data Env m = Env
-- { someRecord :: SomeRecord m
-- }
-- instance Has SomeRecord m (Env m) where
-- dep (Env{someRecord}) = someRecord
-- :}
--
-- In practice, this means that you can write @call someSelector@ instead of @someSelector (dep
-- env)@:
--
-- >>> :{
-- twoInvocations :: (IO (), IO ())
-- twoInvocations =
-- let env :: Env IO = Env { someRecord = SomeRecord { someSelector = putStrLn } }
-- call = asCall env
-- in (someSelector (dep env) "foo", call someSelector "foo")
-- :}
--
-- Using 'asCall' in a view pattern avoids having to name the
-- environment:
--
--
-- >>> :{
-- functionThatCalls :: Has SomeRecord m e => e -> m ()
-- functionThatCalls (asCall -> call) = call someSelector "foo"
-- :}
--
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.
--
-- This allows defining 'Has' instances with empty bodies, thanks to
-- @DefaultSignatures@.
type Dep :: ((Type -> Type) -> Type) -> Constraint
class Dep r_ where
-- The Char kind would be useful here, to lowercase the first letter of the
-- k type and use it as the default preferred field name.
type DefaultFieldName r_ :: Symbol
-- $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
-- >>> :set -XScopedTypeVariables
-- >>> import Data.Kind
-- >>> import Control.Monad.Dep
-- >>> import GHC.Generics (Generic)
--