dep-t-0.6.8.0: lib/Dep/Constructor.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE BlockArguments #-}
-- | This module provides a method for performing dependency injection in 'Phased'
-- environments by means of building fixpoints.
--
-- To ease the reader into the concept, here's how we can define the
-- [@factorial@](https://en.wikibooks.org/wiki/Haskell/Fix_and_recursion#Recursion)
-- function using only 'fix' from @base@:
--
-- >>> :{
-- type FactorialDeps = Int -> Int
-- makeFactorial :: FactorialDeps -> Int -> Int
-- makeFactorial deps n = if n == 0 then 1 else n * deps (n-1)
-- factorial :: Int -> Int
-- factorial = fix makeFactorial
-- :}
--
-- Looking at it, we can interpret it as a form of dependency injection. In the
-- example, @makeFactorial@ depends on another function of type @FactorialDeps@
-- for the @n > 1@ logic. So we build a fixpoint in which the resulting
-- \"closed\" @factorial@ is passed as the dependency to @makeFactorial@.
--
-- Very good, but what does this have to do with dependency injection in a
-- /real/ application? For real applications, we have a multitude of functions,
-- not only one. Each component has potentially many functions, and there may be
-- many components, with a complex directed acyclic graph of dependencies
-- between components!
--
-- This module provides the 'Constructor' applicative. A 'Phased' dependency
-- injection environment parameterized by 'Constructor' is like the set of all
-- component constructors taking part in dependency injection, each one still
-- \"open\" like @makeFactorial@, still waiting for its own dependencies.
--
-- And when we use the 'fixEnv' function on this \"open\" environment, we get
-- back a \"closed\" environment parameterized by 'Identity', were all the
-- dependencies have been resolved and the components are ready to be used.
module Dep.Constructor
( -- * Constructor phase
Constructor,
constructor,
lmapConstructor,
fixEnv,
-- * Constructor with accumulator
AccumConstructor,
accumConstructor,
accumConstructor_,
_accumConstructor,
_accumConstructor_,
lmapAccumConstructor,
fixEnvAccum,
-- * "Control.Arrow" re-exports
arr
)
where
import Control.Applicative
import Data.Bifunctor (second)
import Data.Coerce
import Data.Function (fix)
import Data.Kind
import Data.Typeable
import Dep.Phases
import Control.Category (Category)
import Control.Category qualified
import Control.Arrow
import Data.Functor.Identity
-- | A phase with the effect of \"constructing each component by reading its
-- dependencies from a completed environment\". It should be the final phase.
--
-- The @deps@ type parameter will typically be the \"closed\" form of the
-- dependency injection environment. That is, the type of environment produced
-- by 'fixEnv'.
newtype Constructor (deps :: Type) component
= Constructor (deps -> component)
deriving stock Functor
deriving newtype instance Category Constructor
-- | Mostly useful for 'arr', which builds a 'Constructor' out of a regular function.
deriving newtype instance Arrow Constructor
-- | 'pure' lifts a component that doesn't require any dependencies.
deriving newtype instance Applicative (Constructor deps)
-- | Turn an environment-consuming function into a 'Constructor' that can be slotted
-- into some field of a 'Phased' environment.
--
-- Equivalent to 'arr'.
constructor ::
forall deps component.
(deps -> component) ->
Constructor deps component
-- same order of type parameters as Has
constructor = Constructor
-- | A generalized 'Constructor' which produces, in addition to the result
-- value, an @accum@ value which is then aggregated across all components and fed
-- back along with the completed environment.
--
-- Like 'Constructor', 'AccumConstructor' should be the final phase.
newtype AccumConstructor (accum :: Type) (deps :: Type) component
= AccumConstructor ((accum, deps) -> (accum, component))
deriving stock Functor
-- | 'pure' lifts a component that doesn't require any dependencies.
-- The produced accumulator will be 'mempty'.
instance Monoid accum => Applicative (AccumConstructor accum deps) where
pure component = _accumConstructor_ \_ -> component
liftA2 f (AccumConstructor u) (AccumConstructor v) = AccumConstructor \accumdeps ->
let (acc1, component1) = u accumdeps
(acc2, component2) = v accumdeps
in (acc1 <> acc2, f component1 component2)
-- |
instance Monoid accum => Category (AccumConstructor accum) where
id = _accumConstructor_ id
(.) (AccumConstructor f) (AccumConstructor g) = AccumConstructor \(~(accum0,deps0)) ->
let (accum1, deps1) = g (accum0,deps0)
(accum2, deps2) = f (accum0,deps1)
in (accum1 <> accum2, deps2)
-- | Mostly useful for 'arr', which builds an 'AccumConstructor' out of a regular function. The produced accumulator will be 'mempty'.
instance Monoid accum => Arrow (AccumConstructor accum) where
arr = _accumConstructor_
first (AccumConstructor f) = AccumConstructor \(~(accum,(deps,extra))) ->
let (accum', component) = f (accum,deps)
in (accum', (component, extra))
-- | Turn an environment-consuming function into an 'AccumConstructor' that can
-- be slotted into some field of a 'Phased' environment. The function also
-- consumes and produces a monoidal accumulator.
accumConstructor ::
forall accum deps component.
(accum -> deps -> (accum, component)) ->
AccumConstructor accum deps component
accumConstructor f = AccumConstructor (\(~(accum, deps)) -> f accum deps)
accumConstructor_ ::
forall accum deps component.
Monoid accum =>
-- | Consumes the accumulator but doesn't produce it (returns the 'mempty' accumulator.)
(accum -> deps -> component) ->
AccumConstructor accum deps component
accumConstructor_ f = accumConstructor $ \accum deps -> (mempty, f accum deps)
_accumConstructor ::
forall accum deps component.
-- | Doesn't consume the accumulator but produces it.
(deps -> (accum, component)) ->
AccumConstructor accum deps component
_accumConstructor f = accumConstructor $ \_ deps -> f deps
-- | Equivalent to 'arr'.
_accumConstructor_ ::
forall accum deps component.
Monoid accum =>
-- | Neither consumes nor produces the accumulator, like a 'Constructor'.
(deps -> component) ->
AccumConstructor accum deps component
_accumConstructor_ f = accumConstructor $ \_ deps -> (mempty, f deps)
-- | This is a method of performing dependency injection by building fixpoints.
--
-- If we have a environment whose fields are functions that construct each
-- component by searching for its dependencies in a \"fully built\" version of
-- the environment, we can \"tie the knot\" to obtain the \"fully built\"
-- environment. This works as long as there aren't any circular dependencies
-- between components.
--
-- Think of it as a version of 'Data.Function.fix' that, instead of \"tying\" a single
-- function, ties a whole record of them.
--
-- We might have arrived as this \"ready-to-wire\" environment by peeling away
-- successive layers of applicative functor composition using 'pullPhase', until
-- only the wiring phase remains.
--
-- >>> :{
-- newtype Foo d = Foo {foo :: String -> d ()} deriving Generic
-- newtype Bar d = Bar {bar :: String -> d ()} deriving Generic
-- makeIOFoo :: MonadIO m => Foo m
-- makeIOFoo = Foo (liftIO . putStrLn)
-- makeBar :: Has Foo m env => env -> Bar m
-- makeBar (asCall -> call) = Bar (call foo)
-- type Deps_ = InductiveEnv [Bar,Foo]
-- type Deps = Deps_ Identity
-- deps_ :: Deps_ (Constructor (Deps IO)) IO
-- deps_ = EmptyEnv
-- & AddDep @Foo (constructor (\_ -> makeIOFoo))
-- & AddDep @Bar (constructor makeBar)
-- deps :: Deps IO
-- deps = fixEnv deps_
-- :}
--
-- >>> :{
-- bar (dep deps) "this is bar"
-- :}
-- this is bar
fixEnv ::
(Phased deps_, Typeable deps_, Typeable m) =>
-- | Environment where each field is wrapped in a 'Constructor'
deps_ (Constructor (deps_ Identity m)) m ->
-- | Fully constructed environment, ready for use.
deps_ Identity m
fixEnv env = fix (pullPhase (liftAH decompose env))
where
decompose (Constructor f) = coerce f
-- | A generalized 'fixEnv' which threads a monoidal accumulator
-- along with the environment.
--
-- Sometimes, we need constructors to produce a monoidal value along with the
-- component. Think for example about some kind of composable startup action for
-- the component.
--
-- And on the input side, some constructors need access to the monoidal value
-- accumulated across all components. Think for example about a component which
-- publishes diagnostics coming from all other components.
fixEnvAccum ::
(Phased deps_, Typeable deps_, Typeable m, Monoid accum, Typeable accum) =>
-- | Environment where each field is wrapped in an 'AccumConstructor'
deps_ (AccumConstructor accum (deps_ Identity m)) m ->
-- | Fully constructed accumulator and environment, ready for use.
(accum, deps_ Identity m)
fixEnvAccum env =
let f = pullPhase <$> pullPhase (liftAH decompose env)
in fix f
where
decompose (AccumConstructor f) = coerce f
-- | Change the dependency environment seen by the component.
lmapConstructor ::
forall deps deps' component.
Typeable component =>
-- | Modifies the environment, with access to the 'TypeRep' of the component.
(TypeRep -> deps -> deps') ->
Constructor deps' component ->
Constructor deps component
lmapConstructor tweak (Constructor f) =
let tyRep = typeRep (Proxy @component)
in Constructor $ f . tweak tyRep
-- | Change the dependency environment seen by the component.
--
-- The accumulator remains unchanged.
lmapAccumConstructor ::
forall accum deps deps' component.
Typeable component =>
-- | Modifies the environment, with access to the 'TypeRep' of the component.
(TypeRep -> deps -> deps') ->
AccumConstructor accum deps' component ->
AccumConstructor accum deps component
lmapAccumConstructor tweak (AccumConstructor f) =
let tyRep = typeRep (Proxy @component)
in AccumConstructor (\(~(accum, deps)) -> f (accum, tweak tyRep deps))
-- $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 -XDerivingStrategies
-- >>> :set -XDerivingVia
-- >>> :set -XDeriveAnyClass
-- >>> :set -XStandaloneDeriving
-- >>> :set -XUndecidableInstances
-- >>> :set -XTypeOperators
-- >>> :set -XScopedTypeVariables
-- >>> import Data.Kind
-- >>> import Data.Function ((&))
-- >>> import Control.Monad.IO.Class
-- >>> import Dep.Has
-- >>> import Dep.Env hiding (AccumConstructor, Constructor, accumConstructor, constructor, fixEnv, fixEnvAccum)
-- >>> import GHC.Generics (Generic)