| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Cauldron
Description
This is a library for performing dependency injection. It's an alternative
to manually wiring your functions and passing all required parameters
explicitly. Instead of that, you throw your functions into a Cauldron, which wires
them for you, guiding itself by the types.
Wiring errors are detected at runtime, not at compile time.
This library should be used at the "composition root" of the application, and only there: the components we are wiring together need not be aware that the library exists.
These extensions, while not required, can play well with the library:
ApplicativeDoFor advanced fiddling in theArgsapplicative.OverloadedListsFor avoiding explicit calls tomconcatwhen building aCauldronfrom a list ofCauldrons, and for avoiding explicit calls tofromDecoList.
An example of using a Cauldron to wire the constructors of dummy A, B, C datatypes:
>>>:{data A = A deriving Show data B = B deriving Show data C = C deriving Show makeA :: A makeA = A makeB :: A -> B makeB = \_ -> B makeC :: A -> B -> IO C makeC = \_ _ -> pure C :}
The basic idea is to fill the Cauldron with recipes. Recipes are
built by wireing the arguments of a constructor function, and then using
functions like val_ or eff_ depending on whether the constructor is
effectful or not. More complex Recipes can also have decorators.
The we cook the Cauldron passing as a type argument the type of the bean
that we want to extract, along with a Fire argument that regulates what
dependency cycles are allowed (if allowed at all).
>>>:{do let cauldron :: Cauldron IO cauldron = mconcat [ recipe @A $ val_ $ wire makeA, recipe @B $ val_ $ wire makeB, recipe @C $ eff_ $ wire makeC -- we use eff because the constructor has IO effects ] action <- cauldron & cook @C forbidDepCycles & either throwIO pure action :} C
Note: It's better to avoid having beans whose types are functions or
tuples, because the library gives those types special treatment. See the docs
for wire, val, and eff.
Synopsis
- data Cauldron (m :: Type -> Type)
- empty :: forall (m :: Type -> Type). Cauldron m
- recipe :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) => recipelike m bean -> Cauldron m
- singleton :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) => recipelike m bean -> Cauldron m
- insert :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) => recipelike m bean -> Cauldron m -> Cauldron m
- adjust :: forall {m :: Type -> Type} bean. (Typeable bean, HasCallStack) => (Recipe m bean -> Recipe m bean) -> Cauldron m -> Cauldron m
- lookup :: forall {m :: Type -> Type} bean r. Typeable bean => (NonEmpty CallStack -> Recipe m bean -> r) -> Cauldron m -> Maybe r
- delete :: forall (m :: Type -> Type). TypeRep -> Cauldron m -> Cauldron m
- keysSet :: forall (m :: Type -> Type). Cauldron m -> Set TypeRep
- restrictKeys :: forall (m :: Type -> Type). Cauldron m -> Set TypeRep -> Cauldron m
- hoistCauldron :: (forall x. m x -> n x) -> Cauldron m -> Cauldron n
- hoistCauldron' :: (forall x. Typeable x => Args (m (Regs x)) -> Args (n (Regs x))) -> (forall x. Typeable x => Int -> Args (m (Regs x)) -> Args (n (Regs x))) -> Cauldron m -> Cauldron n
- data Recipe (m :: Type -> Type) bean = Recipe {
- bare :: Constructor m bean
- decos :: Seq (Constructor m bean)
- class ToRecipe (recipelike :: (Type -> Type) -> Type -> Type)
- fromDecoList :: forall (m :: Type -> Type) bean. [Constructor m bean] -> Seq (Constructor m bean)
- (|>) :: Seq a -> a -> Seq a
- (<|) :: a -> Seq a -> Seq a
- hoistRecipe :: (forall x. m x -> n x) -> Recipe m bean -> Recipe n bean
- hoistRecipe' :: forall m n bean. (Args (m (Regs bean)) -> Args (n (Regs bean))) -> (Int -> Args (m (Regs bean)) -> Args (n (Regs bean))) -> Recipe m bean -> Recipe n bean
- data Constructor (m :: Type -> Type) bean
- data Args a
- arg :: Typeable a => Args a
- wire :: Wireable curried tip => curried -> Args tip
- val_ :: forall bean (m :: Type -> Type). (Applicative m, HasCallStack) => Args bean -> Constructor m bean
- eff_ :: forall bean m. (Functor m, HasCallStack) => Args (m bean) -> Constructor m bean
- ioEff_ :: forall bean (m :: Type -> Type). (MonadIO m, HasCallStack) => Args (IO bean) -> Constructor m bean
- val :: forall {nested} bean (m :: Type -> Type). (Registrable nested bean, Applicative m, HasCallStack) => Args nested -> Constructor m bean
- val' :: forall bean (m :: Type -> Type). (Applicative m, HasCallStack) => Args (Regs bean) -> Constructor m bean
- eff :: (Registrable nested bean, Monad m, HasCallStack) => Args (m nested) -> Constructor m bean
- eff' :: forall bean m. HasCallStack => Args (m (Regs bean)) -> Constructor m bean
- ioEff :: forall {nested} bean (m :: Type -> Type). (Registrable nested bean, MonadIO m, HasCallStack) => Args (IO nested) -> Constructor m bean
- getConstructorArgs :: Constructor m bean -> Args (m (Regs bean))
- getConstructorCallStack :: forall (m :: Type -> Type) bean. Constructor m bean -> CallStack
- hoistConstructor :: (forall x. m x -> n x) -> Constructor m bean -> Constructor n bean
- hoistConstructor' :: forall m n bean. (Args (m (Regs bean)) -> Args (n (Regs bean))) -> Constructor m bean -> Constructor n bean
- cook :: (Monad m, Typeable bean) => Fire m -> Cauldron m -> Either CookingError (m bean)
- nest :: forall {m :: Type -> Type} bean. (Monad m, Typeable bean, HasCallStack) => Fire m -> Cauldron m -> Either CookingError (Constructor m bean)
- data Fire (m :: Type -> Type)
- forbidDepCycles :: forall (m :: Type -> Type). Monad m => Fire m
- allowSelfDeps :: forall (m :: Type -> Type). MonadFix m => Fire m
- allowDepCycles :: forall (m :: Type -> Type). MonadFix m => Fire m
- data CookingError
- data MissingDependencies = MissingDependencies CallStack TypeRep (Set TypeRep)
- data DoubleDutyBean = DoubleDutyBean TypeRep CallStack CallStack
- newtype DependencyCycle = DependencyCycle (NonEmpty (BeanConstructionStep, Maybe CallStack))
- prettyCookingError :: CookingError -> String
- prettyCookingErrorLines :: CookingError -> [String]
- getDependencyGraph :: forall (m :: Type -> Type). Cauldron m -> DependencyGraph
- data DependencyGraph
- writeAsDot :: Style BeanConstructionStep String -> FilePath -> DependencyGraph -> IO ()
- defaultStyle :: (Monoid s, IsString s) => Maybe CookingError -> Style BeanConstructionStep s
- setVertexName :: IsString s => (BeanConstructionStep -> s) -> Style BeanConstructionStep s -> Style BeanConstructionStep s
- data BeanConstructionStep
- toAdjacencyMap :: DependencyGraph -> AdjacencyMap BeanConstructionStep
- removeAggregates :: DependencyGraph -> DependencyGraph
- removeDecos :: DependencyGraph -> DependencyGraph
- collapseBeans :: DependencyGraph -> DependencyGraph
Filling the cauldron
data Cauldron (m :: Type -> Type) Source #
A map of bean recipes, indexed by the TypeRep of the bean each recipe
ultimately produces. Only one recipe is allowed for each bean type.
Parameterized by the monad m in which the recipe Constructors might have
effects.
Instances
| Monoid (Cauldron m) Source # | |
| Semigroup (Cauldron m) Source # | Union of two |
| IsList (Cauldron m) Source # | Somewhat unusual instance, in that the |
| type Item (Cauldron m) Source # | |
Arguments
| :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) | |
| => recipelike m bean | A |
| -> Cauldron m |
Create a Cauldron consisting of a single Recipe.
recipe and singleton are the same function.
For readability, the bean type is often passed as a type application, despite
not being strictly required:
>>>:{oneRecipe :: Cauldron IO oneRecipe = recipe @Bool $ val_ $ pure $ False :}
Typical usage involves putting singleton Cauldrons in a list and mconcatting them:
>>>:{twoRecipes :: Cauldron IO twoRecipes = mconcat [ recipe $ val_ $ pure $ False, recipe @Char $ val_ $ wire $ \(_ :: Bool) -> 'b' ] :}
Arguments
| :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) | |
| => recipelike m bean | A |
| -> Cauldron m |
Create a Cauldron consisting of a single Recipe.
recipe and singleton are the same function.
For readability, the bean type is often passed as a type application, despite
not being strictly required:
>>>:{oneRecipe :: Cauldron IO oneRecipe = recipe @Bool $ val_ $ pure $ False :}
Typical usage involves putting singleton Cauldrons in a list and mconcatting them:
>>>:{twoRecipes :: Cauldron IO twoRecipes = mconcat [ recipe $ val_ $ pure $ False, recipe @Char $ val_ $ wire $ \(_ :: Bool) -> 'b' ] :}
Arguments
| :: forall {recipelike} {m :: Type -> Type} bean. (Typeable bean, ToRecipe recipelike, HasCallStack) | |
| => recipelike m bean | A |
| -> Cauldron m | |
| -> Cauldron m |
adjust :: forall {m :: Type -> Type} bean. (Typeable bean, HasCallStack) => (Recipe m bean -> Recipe m bean) -> Cauldron m -> Cauldron m Source #
lookup :: forall {m :: Type -> Type} bean r. Typeable bean => (NonEmpty CallStack -> Recipe m bean -> r) -> Cauldron m -> Maybe r Source #
keysSet :: forall (m :: Type -> Type). Cauldron m -> Set TypeRep Source #
The set of all TypeRep keys of the map.
hoistCauldron :: (forall x. m x -> n x) -> Cauldron m -> Cauldron n Source #
Arguments
| :: (forall x. Typeable x => Args (m (Regs x)) -> Args (n (Regs x))) | Transformation to apply to the base constructor of each recipe. |
| -> (forall x. Typeable x => Int -> Args (m (Regs x)) -> Args (n (Regs x))) | Transformation to apply to each decorator. Takes the decorator index as parameter. |
| -> Cauldron m | |
| -> Cauldron n |
More general form of hoistCauldron that lets you modify the Args
inside all the Recipes in the Cauldron. See hoistRecipe'.
Recipes
data Recipe (m :: Type -> Type) bean Source #
Instructions for how to build a value of type bean while possibly
performing actions in the monad m.
Because the instructions aren't really run until the Cauldron is cooked,
they can be modified with functions like adjust, in order to change the
base bean Constructor or add or remove decorators.
Constructors
| Recipe | |
Fields
| |
class ToRecipe (recipelike :: (Type -> Type) -> Type -> Type) Source #
Convenience typeclass that allows passing either Recipes or Constructors
to the recipe and insert functions.
Minimal complete definition
toRecipe
Instances
| ToRecipe Constructor Source # |
|
| ToRecipe Recipe Source # | Simply identity. |
fromDecoList :: forall (m :: Type -> Type) bean. [Constructor m bean] -> Seq (Constructor m bean) Source #
(|>) :: Seq a -> a -> Seq a infixl 5 #
\( O(1) \). Add an element to the right end of a sequence. Mnemonic: a triangle with the single element at the pointy end.
(<|) :: a -> Seq a -> Seq a infixr 5 #
\( O(1) \). Add an element to the left end of a sequence. Mnemonic: a triangle with the single element at the pointy end.
hoistRecipe :: (forall x. m x -> n x) -> Recipe m bean -> Recipe n bean Source #
Change the monad used by the bean's main Constructor and its decos.
Arguments
| :: forall m n bean. (Args (m (Regs bean)) -> Args (n (Regs bean))) | Transformation to apply to the base constructor. |
| -> (Int -> Args (m (Regs bean)) -> Args (n (Regs bean))) | Transformation to apply to each decorator. Takes the decorator index as parameter. |
| -> Recipe m bean | |
| -> Recipe n bean |
More general form of hoistRecipe that enables precise control over the inner Args
of each constructor in the Recipe.
How decorators work
Decorators are Constructors which, instead of constructing the original
version of a bean, modify it in some way (but without changing its
type). Because they modify the bean, typically decorators will take the bean
as an argument.
Decorators can have other dependencies beyond the modified bean.
When the bean is a record-of-functions, decorators can be used to add behaviors like caching and logging to the functions.
The order of the decorators in the sequence is the order in which they modify
the underlying bean. First decorator wraps first, last decorator wraps last.
Think of it as there being an implicit & between the bean and the
subsequent decorators, and between the decorators themselves.
>>>:{newtype Foo = Foo { sayFoo :: IO () } makeFoo :: Foo makeFoo = Foo { sayFoo = putStrLn "foo" } makeFooDeco1 :: Foo -> Foo makeFooDeco1 Foo { sayFoo } = Foo { sayFoo = putStrLn "deco1 enter" >> sayFoo >> putStrLn "deco1 exit" } makeFooDeco2 :: Foo -> IO Foo makeFooDeco2 Foo { sayFoo } = putStrLn "deco2 init" >> pure Foo { sayFoo = putStrLn "deco2 enter" >> sayFoo >> putStrLn "deco2 exit" } :}
>>>:{do action <- mconcat [ recipe @Foo $ Recipe { bare = val $ wire makeFoo, decos = [ val $ wire makeFooDeco1, eff $ wire makeFooDeco2 ] } ] & cook @Foo forbidDepCycles & either throwIO pure Foo {sayFoo} <- action sayFoo :} deco2 init deco2 enter deco1 enter foo deco1 exit deco2 exit
Constructors
Bean-producing and bean-decorating functions need to be coaxed into
Constructors in order to be used in Cauldrons.
First we fill the arguments of the function in an Args context, either one
by one using args and Applicative operators, or all in a single swoop,
using wire.
Then, depending on whether the function produces the desired bean directly,
or through an effect, we use functions like val_, val, eff_ or eff on
the Args value.
data Constructor (m :: Type -> Type) bean Source #
A way of building value of type bean, potentially requiring some
dependencies, potentially returning some secondary aggregate beans
along the primary bean result, and also potentially requiring some
initialization effect in a monad m.
Note that only the type of the primary bean is reflected in the
Constructor type. Those of the dependencies and aggregate beans are not.
Constructor doesn't have a Functor instance. This is beause sometimes a
Constructor may depend on the same type of bean it produces, but the
Functor instance would only change the output type, leading to confusing
wiring errors.
A typical initialization monad will be IO, used for example to create
mutable references that the bean will use internally. Sometimes the
constructor will acquire resources with bracket-like operations, and in that
case a monad like Managed might be needed instead.
Instances
| ToRecipe Constructor Source # |
|
An Applicative that knows how to construct values by searching in a
Beans map, and keeps track of the types that will be searched in the
Beans map.
arg :: Typeable a => Args a Source #
Look for a type in the Beans map and return its corresponding value.
>>>:{fun1 :: Bool -> Int fun1 _ = 5 w1 :: Args Int w1 = fun1 <$> arg fun2 :: String -> Bool -> Int fun2 _ _ = 5 w2 :: Args Int w2 = fun2 <$> arg <*> arg :}
wire :: Wireable curried tip => curried -> Args tip Source #
Takes a curried function and reads all of its arguments by type using
arg, returning an Args for the final result value of the function.
This function assumes that the tip is not a function, in order to know
when to stop collecting arguments. If your intended tip is a function,
you might need to wrap it in a newtype in order to disambiguate.
>>>:{fun0 :: Int fun0 = 5 w0 :: Args Int w0 = wire fun0 fun1 :: Bool -> Int fun1 _ = 5 w1 :: Args Int w1 = wire fun1 fun2 :: String -> Bool -> Int fun2 _ _ = 5 w2 :: Args Int w2 = wire fun2 :}
val_ :: forall bean (m :: Type -> Type). (Applicative m, HasCallStack) => Args bean -> Constructor m bean Source #
Create a Constructor from an Args value that returns a bean.
Usually, the Args value will be created by wireing a constructor function.
>>>:{data A = A data B = B makeB :: A -> B makeB _ = B c :: Constructor IO B c = val_ $ wire $ makeB :}
eff_ :: forall bean m. (Functor m, HasCallStack) => Args (m bean) -> Constructor m bean Source #
Create a Constructor from an Args value that returns an initialization
effect that produces bean.
Usually, the Args value will be created by wireing an effectul constructor function.
>>>:{data A = A data B = B makeB :: A -> IO B makeB _ = pure B c :: Constructor IO B c = eff_ $ wire $ makeB :}
ioEff_ :: forall bean (m :: Type -> Type). (MonadIO m, HasCallStack) => Args (IO bean) -> Constructor m bean Source #
Registering aggregate beans
There is an exception to the Cauldron rule that each bean type can only
be produced by a single Recipe in the Cauldron.
Constructors can produce, besides their "primary" bean result,
secondary "aggregate" beans that are not reflected in the Constructor signature.
Multiple constructors across different Recipes can produce secondary beans of the
same type.
Aggregate beans are a bit special, in that:
- The value that is "seen" by a
Constructorthat depends on an aggregate bean is the aggregation of all values produced for that bean in theCauldron. Therefore, these beans must haveMonoidinstances. - When calculating build plan order for a
Cauldron,Constructors that depend on a aggregate bean come after all of theConstructors that produce that aggregate bean. - Aggregate beans can't be decorated.
- A bean type can't be primary and aggregate at the same time. See
DoubleDutyBeansError.
What are aggregate beans useful for?
- Exposing some uniform control or inspection interface for certain beans.
- Registering tasks or workers that must be run after application initialization.
The simplest way of registering aggregate beans is to pass an Args value returning a tuple
to the val (for pure constructors) or eff (for effectful constructors) functions. Components
of the tuple other than the rightmost component are considered aggregate beans:
>>>:{con :: Constructor Identity String con = val $ pure (Sum @Int, All False, "foo") effCon :: Constructor IO String effCon = eff $ pure $ pure @IO (Sum @Int, All False, "foo") :}
Example of how aggregate bean values are aggregated:
>>>:{data U = U deriving Show data V = V deriving Show makeU :: (Sum Int, U) makeU = (Sum 1, U) makeV :: U -> (Sum Int, V) makeV = \_ -> (Sum 7, V) newtype W = W (Sum Int) deriving Show -- depends on the aggregate bean :}
>>>:{do let cauldron :: Cauldron Identity cauldron = mconcat [ recipe @U $ val $ wire makeU, recipe @V $ val $ wire makeV, recipe @W $ val $ wire W ] Identity w <- cauldron & cook @W forbidDepCycles & either throwIO pure pure w :} W (Sum {getSum = 8})
Example of how aggregate beans can't also be primary beans:
>>>:{data X = X deriving Show makeX :: (Sum Int, X) makeX = (Sum 1, X) makeAgg :: Sum Int makeAgg = Sum 7 :}
>>>:{(mconcat [ recipe @X $ val $ wire makeX, recipe @(Sum Int) $ val $ wire makeAgg :: Cauldron IO ]) & cook @X forbidDepCycles & \case Left (DoubleDutyBeansError _) -> "Sum Int is aggregate and primary"; _ -> "oops" :} "Sum Int is aggregate and primary"
val :: forall {nested} bean (m :: Type -> Type). (Registrable nested bean, Applicative m, HasCallStack) => Args nested -> Constructor m bean Source #
Like val_, but examines the nested value returned by the Args looking
for (potentially nested) tuples. All tuple components except the
rightmost-innermost one are registered as aggregate beans (if they have
Monoid instances, otherwise val won't compile).
Because this function gives a special meaning to tuples, it shouldn't be used to wire beans that have themselves a tuple type. Better define a special-purpose bean datatype instead.
>>>:{data A = A data B = B makeB :: A -> (Sum Int, Any, B) makeB _ = (Sum 0, Any False, B) c :: Constructor IO B c = val $ wire $ makeB makeB' :: A -> (Sum Int, (Any, B)) makeB' _ = (Sum 0, (Any False, B)) c' :: Constructor IO B c' = val $ wire $ makeB :}
val' :: forall bean (m :: Type -> Type). (Applicative m, HasCallStack) => Args (Regs bean) -> Constructor m bean Source #
Like val, but uses an alternative form of registering secondary beans.
Less Registrable typeclass magic, but more verbose. Likely not what you want.
eff :: (Registrable nested bean, Monad m, HasCallStack) => Args (m nested) -> Constructor m bean Source #
Like eff_, but examines the nested value produced by the action
returned by the Args looking for (potentially nested) tuples. All tuple
components except the rightmost-innermost one are registered as aggregate
beans (if they have Monoid instances, otherwise eff won't compile).
Because this function gives a special meaning to tuples, it shouldn't be used to wire beans that have themselves a tuple type. Better define a special-purpose bean datatype instead.
>>>:{data A = A data B = B makeB :: A -> IO (Sum Int, Any, B) makeB _ = pure (Sum 0, Any False, B) c :: Constructor IO B c = eff $ wire $ makeB makeB' :: A -> IO (Sum Int, (Any, B)) makeB' _ = pure (Sum 0, (Any False, B)) c' :: Constructor IO B c' = eff $ wire $ makeB :}
eff' :: forall bean m. HasCallStack => Args (m (Regs bean)) -> Constructor m bean Source #
Like eff, but uses an alternative form of registering secondary beans.
Less Registrable typeclass magic, but more verbose. Likely not what you want.
ioEff :: forall {nested} bean (m :: Type -> Type). (Registrable nested bean, MonadIO m, HasCallStack) => Args (IO nested) -> Constructor m bean Source #
"with"-like constructors
Some effectful constructor functions, in order to manage the acquisition and
release of the bean they produce, use the common idiom or returning a
higher-order function that takes a callback. A typical example is
withFile.
These effecful constructor functions can be coaxed into Constructors that
have their effects in a monad like Managed.
We need to wrap the callback-accepting part in managed,
before we lift the function to Args using wire:
>>>:{-- We treat the 'IOMode' as if it were a bean dependency. handleBean :: Constructor Managed Handle handleBean = eff_ $ wire $ \mode -> managed $ withFile "/tmp/foo.txt" mode :}
Annoyingly, this forces us to be a bit more verbose and explicitly mention
the constructor parameters (in the example, mode) in order to reach the
part that we wrap in Managed.
Sundry constructor helpers
getConstructorArgs :: Constructor m bean -> Args (m (Regs bean)) Source #
Get the inner Args value for the Constructor, typically for inspecting
TypeReps of its arguments/registrations.
getConstructorCallStack :: forall (m :: Type -> Type) bean. Constructor m bean -> CallStack Source #
For debugging purposes, Constructors remember the CallStack
of when they were created.
hoistConstructor :: (forall x. m x -> n x) -> Constructor m bean -> Constructor n bean Source #
Change the monad in which the Constructor's effects take place.
hoistConstructor' :: forall m n bean. (Args (m (Regs bean)) -> Args (n (Regs bean))) -> Constructor m bean -> Constructor n bean Source #
More general form of hoistConstructor that enables precise control over the inner Args.
Cooking the beans
Arguments
| :: (Monad m, Typeable bean) | |
| => Fire m | The types of dependency cycles that are allowed between beans. |
| -> Cauldron m | |
| -> Either CookingError (m bean) |
Build the requested bean using the Recipes stored in Cauldron.
The Cauldrons must contain a Recipe for the requested bean, as well as
Recipes for producing all of its transitive dependencies.
>>>:{data A = A deriving Show :}
>>>:{(mempty :: Cauldron IO) & cook @A forbidDepCycles & \case Left (MissingResultBeanError _) -> "no recipe for requested bean"; _ -> "oops" :} "no recipe for requested bean"
>>>:{data A = A deriving Show data B = B A deriving Show :}
>>>:{(singleton $ val $ wire B :: Cauldron IO) & cook @B forbidDepCycles & \case Left (MissingDependenciesError _) -> "no recipe for A"; _ -> "oops" :} "no recipe for A"
Arguments
| :: forall {m :: Type -> Type} bean. (Monad m, Typeable bean, HasCallStack) | |
| => Fire m | The types of dependency cycles that are allowed between beans. |
| -> Cauldron m |
|
| -> Either CookingError (Constructor m bean) |
Takes a Cauldron and converts it into a Constructor where
any unfilled dependencies are taken as the arguments of the Constructor.
The Constructor can later be included in a bigger Cauldron, which will
provide the missing dependencies.
This function never fails with MissingDependenciesError.
This is an advanced function for when you want limited scopes for some beans.
Usually cook is enough.
Consider these example definitions:
>>>:{data A = A (IO ()) data B = B (IO ()) data C = C (IO ()) makeA :: A makeA = A (putStrLn "A constructor") makeA2 :: A makeA2 = A (putStrLn "A2 constructor") makeB :: A -> B makeB (A a) = B (a >> putStrLn "B constructor") makeC :: A -> B -> C makeC = \(A a) (B b) -> C (a >> b >> putStrLn "C constructor") :}
This is a wiring that uses nest to create an scope that gives a local
meaning to the bean A:
>>>:{do nested :: Constructor IO C <- nest @C forbidDepCycles (mconcat [ recipe @A $ val $ wire makeA2, -- this will be used by makeC recipe @C $ val $ wire makeC -- takes B from outside ]) & either throwIO pure action <- cook @C forbidDepCycles (mconcat [ recipe @A $ val $ wire makeA, recipe @B $ val $ wire makeB, recipe @C $ nested ]) & either throwIO pure C c <- action c :} A2 constructor A constructor B constructor C constructor
compare with this other wiring that uses a single Cauldron:
>>>:{do action <- cook @C forbidDepCycles (mconcat [ recipe @A $ val $ wire makeA, recipe @B $ val $ wire makeB, recipe @C $ val $ wire makeC ]) & either throwIO pure C c <- action c :} A constructor A constructor B constructor C constructor
How loopy can we get?
data Fire (m :: Type -> Type) Source #
Strategy for dealing with dependency cycles.
(The name is admittedly uninformative; the culinary metaphor was stretched too far.)
forbidDepCycles :: forall (m :: Type -> Type). Monad m => Fire m Source #
Forbid any kind of cyclic dependencies between beans. This is probably what you want.
>>>:{data A = A loopyA :: A -> A loopyA _ = A :}
>>>:{(recipe @A $ val $ wire loopyA :: Cauldron IO) & cook @A forbidDepCycles & \case Left (DependencyCycleError _) -> "self dep is forbidden"; _ -> "oops" :} "self dep is forbidden"
allowSelfDeps :: forall (m :: Type -> Type). MonadFix m => Fire m Source #
Allow direct self-dependencies.
A bean constructor might depend on itself. This can be useful for having decorated self-invocations, because the version of the bean received as argument comes "from the future" and is already decorated.
Note that a MonadFix instance is required of the initialization monad.
BEWARE: Pattern-matching too eagerly on a "bean from the future" during
construction will cause infinite loops or, if you are lucky, throw
FixIOExceptions.
>>>:{data A = A loopyA :: A -> A loopyA _ = A :}
>>>:{(recipe @A $ val $ wire loopyA :: Cauldron IO) & cook @A allowSelfDeps & \case Left (DependencyCycleError _) -> "oops"; _ -> "self dep is ok" :} "self dep is ok"
>>>:{data U = U data V = V loopyU :: V -> U loopyU _ = U loopyV :: U -> V loopyV _ = V :}
>>>:{mconcat [ recipe @U $ val $ wire loopyU, recipe @V $ val $ wire loopyV :: Cauldron IO ] & cook @U allowSelfDeps & \case Left (DependencyCycleError _) -> "cycle between 2 deps"; _ -> "oops" :} "cycle between 2 deps"
allowDepCycles :: forall (m :: Type -> Type). MonadFix m => Fire m Source #
Allow any kind of dependency cycles.
Usually comes in handy for creating serializers / deserializers for mutually dependent types.
Note that a MonadFix instance is required of the initialization monad.
BEWARE: Pattern-matching too eagerly on argument beans during
construction will cause infinite loops or, if you are lucky, throw
FixIOExceptions.
>>>:{data U = U data V = V loopyU :: V -> U loopyU _ = U loopyV :: U -> V loopyV _ = V :}
>>>:{mconcat [ recipe @U $ val $ wire loopyU, recipe @V $ val $ wire loopyV :: Cauldron IO ] & cook @U allowDepCycles & \case Left (DependencyCycleError _) -> "oops"; _ -> "cycles are ok" :} "cycles are ok"
When things go wrong
data CookingError Source #
Sometimes the cooking process goes wrong.
Constructors
| MissingResultBeanError TypeRep | The bean that was demanded from the |
| MissingDependenciesError (NonEmpty MissingDependencies) | A |
| DoubleDutyBeansError (NonEmpty DoubleDutyBean) | Beans that work both as primary beans and as secondary beans are disallowed. |
| DependencyCycleError DependencyCycle | Dependency cycles are disallowed by some |
Instances
| Exception CookingError Source # | |
Defined in Cauldron Methods toException :: CookingError -> SomeException # fromException :: SomeException -> Maybe CookingError # displayException :: CookingError -> String # | |
| Show CookingError Source # | |
Defined in Cauldron Methods showsPrec :: Int -> CookingError -> ShowS # show :: CookingError -> String # showList :: [CookingError] -> ShowS # | |
data MissingDependencies Source #
Missing depencencies for a Constructor.
Constructors
| MissingDependencies CallStack TypeRep (Set TypeRep) |
Instances
| Show MissingDependencies Source # | |
Defined in Cauldron Methods showsPrec :: Int -> MissingDependencies -> ShowS # show :: MissingDependencies -> String # showList :: [MissingDependencies] -> ShowS # | |
data DoubleDutyBean Source #
Constructors
| DoubleDutyBean TypeRep CallStack CallStack |
Instances
| Show DoubleDutyBean Source # | |
Defined in Cauldron Methods showsPrec :: Int -> DoubleDutyBean -> ShowS # show :: DoubleDutyBean -> String # showList :: [DoubleDutyBean] -> ShowS # | |
newtype DependencyCycle Source #
Constructors
| DependencyCycle (NonEmpty (BeanConstructionStep, Maybe CallStack)) |
Instances
| Show DependencyCycle Source # | |
Defined in Cauldron Methods showsPrec :: Int -> DependencyCycle -> ShowS # show :: DependencyCycle -> String # showList :: [DependencyCycle] -> ShowS # | |
prettyCookingErrorLines :: CookingError -> [String] Source #
Visualizing dependencies between beans.
getDependencyGraph :: forall (m :: Type -> Type). Cauldron m -> DependencyGraph Source #
Get a graph of dependencies between BeanConstructionSteps. The graph can
be obtained even if the mconcatted Cauldrons can't be cooked successfully.
data DependencyGraph Source #
An edge means that the source depends on the target.
The dependencies of each bean are given separatedly from its decorators.
Instances
writeAsDot :: Style BeanConstructionStep String -> FilePath -> DependencyGraph -> IO () Source #
See the DOT format.
defaultStyle :: (Monoid s, IsString s) => Maybe CookingError -> Style BeanConstructionStep s Source #
Default DOT rendering style to use with writeAsDot.
When a CookingError exists, is highlights the problematic BeanConstructionSteps.
setVertexName :: IsString s => (BeanConstructionStep -> s) -> Style BeanConstructionStep s -> Style BeanConstructionStep s Source #
Change the default way of how BeanConstructionSteps are rendered to text.
data BeanConstructionStep Source #
A step in the construction of a bean value.
Constructors
| BarePrimaryBean TypeRep | Undecorated bean. |
| PrimaryBeanDeco TypeRep Int | Apply the decorator with the given index. Comes after the |
| FinishedBean TypeRep | Final, fully decorated version of a bean. If there are no decorators, comes directly after |
| AggregateBean TypeRep | Beans that are secondary registrations of a |
Instances
| Show BeanConstructionStep Source # | |
Defined in Cauldron Methods showsPrec :: Int -> BeanConstructionStep -> ShowS # show :: BeanConstructionStep -> String # showList :: [BeanConstructionStep] -> ShowS # | |
| Eq BeanConstructionStep Source # | |
Defined in Cauldron Methods (==) :: BeanConstructionStep -> BeanConstructionStep -> Bool # (/=) :: BeanConstructionStep -> BeanConstructionStep -> Bool # | |
| Ord BeanConstructionStep Source # | |
Defined in Cauldron Methods compare :: BeanConstructionStep -> BeanConstructionStep -> Ordering # (<) :: BeanConstructionStep -> BeanConstructionStep -> Bool # (<=) :: BeanConstructionStep -> BeanConstructionStep -> Bool # (>) :: BeanConstructionStep -> BeanConstructionStep -> Bool # (>=) :: BeanConstructionStep -> BeanConstructionStep -> Bool # max :: BeanConstructionStep -> BeanConstructionStep -> BeanConstructionStep # min :: BeanConstructionStep -> BeanConstructionStep -> BeanConstructionStep # | |
toAdjacencyMap :: DependencyGraph -> AdjacencyMap BeanConstructionStep Source #
Conversion to a graph type for further processing.
Simplifying the dep graph
DependencyGraphs can get complex and difficult to intepret because they
include bean decorators and secondary beans, details in which we many not be
interested.
These functions help simplify DependencyGraphs before passing them to
writeAsDot. They can be composed between themselves.
removeAggregates :: DependencyGraph -> DependencyGraph Source #
Remove all vertices and edges related to secondary beans.
removeDecos :: DependencyGraph -> DependencyGraph Source #
Remove all vertices and edges related to bean decorators.
collapseBeans :: DependencyGraph -> DependencyGraph Source #
Unifies FinishedBeans with their respective BarePrimaryBeans, PrimaryBeanDecos and AggregateBeans.
Also removes any self-loops.