cauldron 0.6.1.0 → 0.7.0.0
raw patch · 17 files changed
+2394/−811 lines, 17 filesdep −algebraic-graphsdep −bytestringdep −textdep ~containersPVP ok
version bump matches the API change (PVP)
Dependencies removed: algebraic-graphs, bytestring, text
Dependency ranges changed: containers
API changes (from Hackage documentation)
- Cauldron: PrimaryBean :: TypeRep -> BeanConstructionStep
- Cauldron: SecondaryBean :: TypeRep -> BeanConstructionStep
- Cauldron: collapseToPrimaryBeans :: DependencyGraph -> DependencyGraph
- Cauldron: cookNonEmpty :: Monad m => NonEmpty (Fire m, Cauldron m) -> Either RecipeError (m (NonEmpty Beans))
- Cauldron: cookTree :: Monad m => Tree (Fire m, Cauldron m) -> Either RecipeError (m (Tree Beans))
- Cauldron: data Beans
- Cauldron: data RecipeError
- Cauldron: instance GHC.Internal.Base.Monoid (Cauldron.Cauldron m)
- Cauldron: instance GHC.Internal.Base.Monoid Cauldron.DependencyGraph
- Cauldron: instance GHC.Internal.Base.Semigroup (Cauldron.Cauldron m)
- Cauldron: instance GHC.Internal.Base.Semigroup Cauldron.DependencyGraph
- Cauldron: instance GHC.Internal.Exception.Type.Exception Cauldron.RecipeError
- Cauldron: instance GHC.Internal.IsList.IsList (Cauldron.Cauldron m)
- Cauldron: instance GHC.Internal.Show.Show Cauldron.BeanConstructionStep
- Cauldron: instance GHC.Internal.Show.Show Cauldron.DependencyCycle
- Cauldron: instance GHC.Internal.Show.Show Cauldron.DependencyGraph
- Cauldron: instance GHC.Internal.Show.Show Cauldron.DoubleDutyBeans
- Cauldron: instance GHC.Internal.Show.Show Cauldron.MissingDependencies
- Cauldron: instance GHC.Internal.Show.Show Cauldron.RecipeError
- Cauldron: prettyRecipeError :: RecipeError -> String
- Cauldron: prettyRecipeErrorLines :: RecipeError -> [String]
- Cauldron: removeSecondaryBeans :: DependencyGraph -> DependencyGraph
- Cauldron: taste :: Typeable bean => Beans -> Maybe bean
- Cauldron.Args: instance (GHC.Internal.Data.Typeable.Internal.Typeable b, Cauldron.Args.Wireable_ (Cauldron.Args.IsFunction rest) rest tip) => Cauldron.Args.Wireable_ 'Cauldron.Args.NotYetThere (b -> rest) tip
- Cauldron.Args: instance (GHC.Internal.Data.Typeable.Internal.Typeable b, GHC.Internal.Base.Monoid b, Cauldron.Args.Registrable_ (Cauldron.Args.IsReg rest) rest tip) => Cauldron.Args.Registrable_ 'Cauldron.Args.Tup2 (b, rest) tip
- Cauldron.Args: instance (GHC.Internal.Data.Typeable.Internal.Typeable b, GHC.Internal.Base.Monoid b, GHC.Internal.Data.Typeable.Internal.Typeable c, GHC.Internal.Base.Monoid c, Cauldron.Args.Registrable_ (Cauldron.Args.IsReg rest) rest tip) => Cauldron.Args.Registrable_ 'Cauldron.Args.Tup3 (b, c, rest) tip
- Cauldron.Args: instance (GHC.Internal.Data.Typeable.Internal.Typeable b, GHC.Internal.Base.Monoid b, GHC.Internal.Data.Typeable.Internal.Typeable c, GHC.Internal.Base.Monoid c, GHC.Internal.Data.Typeable.Internal.Typeable d, GHC.Internal.Base.Monoid d, Cauldron.Args.Registrable_ (Cauldron.Args.IsReg rest) rest tip) => Cauldron.Args.Registrable_ 'Cauldron.Args.Tup3 (b, c, d, rest) tip
- Cauldron.Args: instance Cauldron.Args.Registrable_ 'Cauldron.Args.Innermost a a
- Cauldron.Args: instance Cauldron.Args.Registrable_ (Cauldron.Args.IsReg nested) nested tip => Cauldron.Args.Registrable nested tip
- Cauldron.Args: instance Cauldron.Args.Wireable_ 'Cauldron.Args.AtTheTip a a
- Cauldron.Args: instance Cauldron.Args.Wireable_ (Cauldron.Args.IsFunction curried) curried tip => Cauldron.Args.Wireable curried tip
- Cauldron.Args: instance GHC.Internal.Base.Applicative Cauldron.Args.Args
- Cauldron.Args: instance GHC.Internal.Base.Applicative Cauldron.Args.Regs
- Cauldron.Args: instance GHC.Internal.Base.Functor Cauldron.Args.Args
- Cauldron.Args: instance GHC.Internal.Base.Functor Cauldron.Args.Regs
- Cauldron.Args: instance GHC.Internal.Base.Monad Cauldron.Args.Regs
- Cauldron.Args: instance GHC.Internal.Exception.Type.Exception Cauldron.Args.LazilyReadBeanMissing
- Cauldron.Args: instance GHC.Internal.Show.Show Cauldron.Args.LazilyReadBeanMissing
- Cauldron.Beans: instance GHC.Internal.Base.Monoid Cauldron.Beans.Beans
- Cauldron.Beans: instance GHC.Internal.Base.Semigroup Cauldron.Beans.Beans
- Cauldron.Beans: instance GHC.Internal.IsList.IsList Cauldron.Beans.Beans
- Cauldron.Beans: instance GHC.Internal.Show.Show Cauldron.Beans.Beans
- Cauldron.Beans: instance GHC.Internal.Show.Show Cauldron.Beans.SomeMonoidTypeRep
- Cauldron.Builder: instance GHC.Internal.Base.Applicative (Cauldron.Builder.Builder m)
- Cauldron.Builder: instance GHC.Internal.Base.Functor (Cauldron.Builder.Builder m)
- Cauldron.Builder: instance GHC.Internal.Base.Monad (Cauldron.Builder.Builder m)
- Cauldron.Builder: instance GHC.Internal.Base.Monad m => Cauldron.Builder.MonadWiring (Cauldron.Builder.Builder m)
- Cauldron.Builder: instance GHC.Internal.Control.Monad.Fix.MonadFix (Cauldron.Builder.Builder m)
- Cauldron.Builder: instance GHC.Internal.Exception.Type.Exception Cauldron.Builder.DuplicateBeans
- Cauldron.Builder: instance GHC.Internal.Show.Show Cauldron.Builder.DuplicateBeans
- Cauldron.Managed: instance GHC.Internal.Base.Applicative Cauldron.Managed.Managed
- Cauldron.Managed: instance GHC.Internal.Base.Functor Cauldron.Managed.Managed
- Cauldron.Managed: instance GHC.Internal.Base.Monad Cauldron.Managed.Managed
- Cauldron.Managed: instance GHC.Internal.Control.Monad.Fix.MonadFix Cauldron.Managed.Managed
+ Cauldron: AggregateBean :: TypeRep -> BeanConstructionStep
+ Cauldron: FinishedBean :: TypeRep -> BeanConstructionStep
+ Cauldron: MissingResultBeanError :: TypeRep -> CookingError
+ Cauldron: collapseBeans :: DependencyGraph -> DependencyGraph
+ Cauldron: data CookingError
+ Cauldron: instance GHC.Base.Monoid (Cauldron.Cauldron m)
+ Cauldron: instance GHC.Base.Monoid Cauldron.BeanGetter
+ Cauldron: instance GHC.Base.Monoid Cauldron.DependencyGraph
+ Cauldron: instance GHC.Base.Semigroup (Cauldron.Cauldron m)
+ Cauldron: instance GHC.Base.Semigroup Cauldron.BeanGetter
+ Cauldron: instance GHC.Base.Semigroup Cauldron.DependencyGraph
+ Cauldron: instance GHC.Exception.Type.Exception Cauldron.CookingError
+ Cauldron: instance GHC.IsList.IsList (Cauldron.Cauldron m)
+ Cauldron: instance GHC.Show.Show Cauldron.BeanConstructionStep
+ Cauldron: instance GHC.Show.Show Cauldron.CookingError
+ Cauldron: instance GHC.Show.Show Cauldron.DependencyCycle
+ Cauldron: instance GHC.Show.Show Cauldron.DependencyGraph
+ Cauldron: instance GHC.Show.Show Cauldron.DoubleDutyBeans
+ Cauldron: instance GHC.Show.Show Cauldron.MissingDependencies
+ Cauldron: nest :: forall {m :: Type -> Type} bean. (Monad m, Typeable bean, HasCallStack) => Fire m -> Cauldron m -> Either CookingError (Constructor m bean)
+ Cauldron: prettyCookingError :: CookingError -> String
+ Cauldron: prettyCookingErrorLines :: CookingError -> [String]
+ Cauldron: removeAggregates :: DependencyGraph -> DependencyGraph
+ Cauldron.Beans: instance GHC.Base.Monoid Cauldron.Beans.Beans
+ Cauldron.Beans: instance GHC.Base.Semigroup Cauldron.Beans.Beans
+ Cauldron.Beans: instance GHC.IsList.IsList Cauldron.Beans.Beans
+ Cauldron.Beans: instance GHC.Show.Show Cauldron.Beans.Beans
+ Cauldron.Beans: instance GHC.Show.Show Cauldron.Beans.SomeMonoidTypeRep
+ Cauldron.Builder: instance Control.Monad.Fix.MonadFix (Cauldron.Builder.Builder m)
+ Cauldron.Builder: instance GHC.Base.Applicative (Cauldron.Builder.Builder m)
+ Cauldron.Builder: instance GHC.Base.Functor (Cauldron.Builder.Builder m)
+ Cauldron.Builder: instance GHC.Base.Monad (Cauldron.Builder.Builder m)
+ Cauldron.Builder: instance GHC.Base.Monad m => Cauldron.Builder.MonadWiring (Cauldron.Builder.Builder m)
+ Cauldron.Builder: instance GHC.Exception.Type.Exception Cauldron.Builder.DuplicateBeans
+ Cauldron.Builder: instance GHC.Show.Show Cauldron.Builder.DuplicateBeans
+ Cauldron.Managed: instance Control.Monad.Fix.MonadFix Cauldron.Managed.Managed
+ Cauldron.Managed: instance GHC.Base.Applicative Cauldron.Managed.Managed
+ Cauldron.Managed: instance GHC.Base.Functor Cauldron.Managed.Managed
+ Cauldron.Managed: instance GHC.Base.Monad Cauldron.Managed.Managed
- Cauldron: DependencyCycleError :: DependencyCycle -> RecipeError
+ Cauldron: DependencyCycleError :: DependencyCycle -> CookingError
- Cauldron: DoubleDutyBeansError :: DoubleDutyBeans -> RecipeError
+ Cauldron: DoubleDutyBeansError :: DoubleDutyBeans -> CookingError
- Cauldron: MissingDependenciesError :: MissingDependencies -> RecipeError
+ Cauldron: MissingDependenciesError :: MissingDependencies -> CookingError
- Cauldron: cook :: Monad m => Fire m -> Cauldron m -> Either RecipeError (m Beans)
+ Cauldron: cook :: (Monad m, Typeable bean) => Fire m -> Cauldron m -> Either CookingError (m bean)
- Cauldron: defaultStyle :: Maybe RecipeError -> Style BeanConstructionStep Text
+ Cauldron: defaultStyle :: (Monoid s, IsString s) => Maybe CookingError -> Style BeanConstructionStep s
- Cauldron: setVertexName :: (BeanConstructionStep -> Text) -> Style BeanConstructionStep Text -> Style BeanConstructionStep Text
+ Cauldron: setVertexName :: IsString s => (BeanConstructionStep -> s) -> Style BeanConstructionStep s -> Style BeanConstructionStep s
- Cauldron: writeAsDot :: Style BeanConstructionStep Text -> FilePath -> DependencyGraph -> IO ()
+ Cauldron: writeAsDot :: Style BeanConstructionStep String -> FilePath -> DependencyGraph -> IO ()
Files
- CHANGELOG.md +15/−0
- README.md +15/−8
- app/Main.hs +34/−31
- cauldron.cabal +16/−6
- lib-graph/Cauldron/Graph.hs +921/−0
- lib-graph/Cauldron/Graph/Algorithm.hs +52/−0
- lib-graph/Cauldron/Graph/Export.hs +187/−0
- lib-graph/Cauldron/Graph/Export/Dot.hs +145/−0
- lib/Cauldron.hs +496/−266
- lib/Cauldron/Args.hs +1/−388
- lib/Cauldron/Args/Internal.hs +407/−0
- lib/Cauldron/Builder.hs +1/−1
- test/appTests.hs +4/−5
- test/argsTests.hs +2/−1
- test/codecTests.hs +10/−16
- test/managedTests.hs +3/−2
- test/tests.hs +85/−87
CHANGELOG.md view
@@ -1,5 +1,20 @@ # Revision history for cauldron +## 0.7.0.0++* Remove dependency on algebraic-graphs, copying those parts of the code that we used.+* Remove `cookTree` and `cookNonEmpty`. +* Added `nest`.+* `cook` is now "typed": we pass the type of the bean we want to extract.+* `RecipeError` -> `CookingError`.+* Renamed `PrimaryBean` to `FinishedBean`.+* Renamed `SecondaryBean` to `AggregateBean`.+* Now the `Constructor`s don't depend directly on `SecondaryBean`/`AggregateBean`.+ There is a `PrimaryBean`/`FinishedBean` that points to the `SecondaryBean`/`AggregateBean`,+ and `Constructor`s depend on that.+* Rename `collapseToPrimaryBeans` to `collapseBeans`.+* Rename `removeSecondaryBeans` to `removeAggregates`.+ ## 0.6.1.0 * `ioEff` added to `Cauldron`.
README.md view
@@ -67,10 +67,10 @@ wiring is *type-directed*, so there can't be any ambiguity about which bean constructor to use. -## Monoidally aggregated secondary beans+## Aggregate beans More complex constructors can return—besides a "primary" bean as seen in the-previous section—one or more "secondary" beans. For example:+previous section—one or more secondary "aggregate" beans. For example: ``` makeServer :: Logger -> Repository -> (Initializer, Inspector, Server)@@ -83,11 +83,12 @@ ``` These secondary outputs of a constructor, like `Initializer` and `Inspector`,-must have `Monoid` instances. Unlike with the "primary" bean the constructor produces, they+must have `Monoid` instances. Unlike with the "primary" bean the constructor+produces, they *can* be produced by more than one constructor. Their values will be aggregated across all the constructors that produce them. -Constructors can depend on the aggregated value of a secondary bean by taking+Constructors can depend on the final aggregated value of an aggregate bean by taking the bean as a regular argument. Here, `makeDebuggingServer` receives the `mappend`ed value of all the `Inspector`s produced by other constructors (or `mempty`, if no constructor produces them):@@ -106,8 +107,8 @@ makeServerDecorator :: Server -> Server ``` -Like normal constructors, decorators can have their own dependencies (other than the-decorated bean), perform effects, and register secondary beans:+Like normal constructors, decorators can have their own dependencies (besides the+decorated bean itself), perform effects, and register aggregate beans: ``` makeServerDecorator :: Logger -> Server -> IO (Initializer,Server)@@ -148,9 +149,9 @@ bean, or the in-construction result of applying the decorators that come earlier in the decorator sequence. -- [context hierachies](https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/hierarchies.html) correspond to distributing the constructors into various sets organized in parent-child relationships, so that constructors in a child can see the beans of the parent, but not vice-versa. +- [context hierachies](https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/hierarchies.html) correspond to taking an "incomplete" set of constructors where not all constructor dependencies can be satisfied inside the set, and turning it into a single constructor which takes the missing dependencies as arguments, and can be made part of a wider set of constructors. The missing dependencies will then be read from that wider set. -- [injecting all the beans that implement a certain interface as a list](https://twitter.com/NiestrojRobert/status/1746808940435042410) roughly corresponds to a constructor that takes a monoidally aggregated "secondary bean" as an argument. +- [injecting all the beans that implement a certain interface as a list](https://twitter.com/NiestrojRobert/status/1746808940435042410) roughly corresponds to a constructor that takes a aggregate bean as an argument. Some features I'm not yet sure how to mimic: @@ -161,3 +162,9 @@ # See also - [registry](https://hackage.haskell.org/package/registry) is a more mature and useable library for dependency injection in Haskell. See [this explanatory video](https://www.youtube.com/watch?v=fFCcvsbCrH8).++- [Do we need effects to get abstraction? (2019)](https://hachyderm.io/@DiazCarrete/114712223474781312)++# Acknowledgement++This package contains vendored code from Andrey Mokhov's [algebraic-graphs](https://hackage.haskell.org/package/algebraic-graphs) (most of the `cauldron:graph` library).
app/Main.hs view
@@ -10,7 +10,6 @@ module Main where import Cauldron-import Data.Maybe (fromJust) {- HERE ARE A BUNCH OF DATATYPES.@@ -74,7 +73,7 @@ data Z = Z deriving (Show) {-- These beans are a bit special: they are "secondary" beans which are optionally+ These beans are a bit special: they are secondary "aggregate" beans which are optionally produced by the constructors of other beans. They have Monoid instances. The values returned by all the constructors that@@ -96,10 +95,10 @@ makeA :: A makeA = A --- A bean with a monoidal registration.------ The registration could be some generic introspection mechanism, or perhaps--- some effectful action that sets up a worker thread.+-- A primary bean 'B' with an aggregate bean 'Inspector'.+-- +-- aggregate beans can be used to implement some generic introspection mechanism+-- for an app, or perhaps some effectful action that sets up worker threads. makeB :: (Inspector, B) makeB = (Inspector (pure ["B stuff"]), B) @@ -117,46 +116,50 @@ -- | A bean with a self-dependency! ----- We need this if we want self-invocations to be decorated.+-- We need these self-dependencies in order for self-invocations to be decorated. ----- Dependency cycles of more than one bean are forbidden, however.+-- The 'Fire' we use to 'cook' the 'Cauldron' might allow or disallow self-dependencies. makeG :: E -> F -> G -> G makeG _ _ _ = G -- | A decorator. ----- Decorators are basically normal constructors, only that they return--- a Endo that knows how to tweak the value of a bean.+-- Decorators are basically normal constructors, only that they take+-- the bean they return as a parameter. --+-- This is not the same as a bean self-dependency! These receive the completed+-- bean from the future, while decorators work with the in-construction version+-- of the bean.+-- -- Because they are normal constructors, they can be effectful, and they -- might have dependencies of their own. makeGDeco1 :: A -> G -> G makeGDeco1 _ g = g --- | A bean with two monoidal registrations.+-- | A primary bean 'H' with two aggregate beans 'Initializer' and 'Inspector'. makeH :: A -> D -> G -> (Initializer, Inspector, H) makeH _ _ _ = (Initializer (putStrLn "H init"), Inspector (pure ["H stuff"]), H) --- | Notice that this bean has "Inspector" as a dependency. Inspector is a--- monoidal bean which is aggregated across all the constructor that register--- it. This is OK as long as there are no dependency cycles.+-- | Notice that this bean has "Inspector" as a dependency. Inspector is an+-- aggregate bean; its value is aggregated across all the constructor that+-- produce it. ----- Why would a bean depend on such a aggregated bean? Well, for example, a--- server bean might want to publish diagnostic information collected from beans--- that register it.+-- Why would a bean depend on an aggregate bean? Well, for example, a server+-- bean might want to publish diagnostic information (exposed using an uniform+-- interface) that is collected from the constructors that register it. makeZ :: Inspector -> D -> H -> Z makeZ _ _ _ = Z makeZDeco1 :: B -> E -> Z -> Z makeZDeco1 _ _ z = z --- | A decorator with a monoidal registration.+-- | A decorator for 'Z' which produces an aggregate bean 'Initializer'. makeZDeco2 :: (F -> Z -> (Initializer, Z)) makeZDeco2 = \_ z -> (Initializer (putStrLn "Z deco init"), z) -data Entrypoint = Entrypoint Initializer Inspector Z+data Result = Result Initializer Inspector Z -boringWiring :: IO Entrypoint+boringWiring :: IO Result boringWiring = do let -- We have to remember to collect the monoidal registrations. initializer = init1 <> init2@@ -179,15 +182,15 @@ z1 = makeZDeco1 b e z0 (init2, z2) = makeZDeco2 f z1 z = z2- pure $ Entrypoint initializer inspector z+ pure $ Result initializer inspector z -- | Here we don't have to worry about positional parameters. We throw all the -- constructors into the 'Cauldron' and taste the bean values at the end, plus a -- graph we may want to draw. -- -- Note that we detect wiring errors *before* running the effectful constructors.-coolWiring :: Either RecipeError (IO Entrypoint)-coolWiring = fmap (fmap (fromJust . taste @Entrypoint)) $ cook allowSelfDeps cauldron+coolWiring :: Either CookingError (IO Result)+coolWiring = cook allowSelfDeps cauldron cauldron :: Cauldron IO cauldron :: Cauldron IO =@@ -213,14 +216,14 @@ val $ wire makeZDeco2 ] },- recipe @Entrypoint $ val $ wire Entrypoint+ recipe @Result $ val $ wire Result ] main :: IO () main = do -- "manual" wiring do- Entrypoint (Initializer {runInitializer}) (Inspector {inspect}) z <- boringWiring+ Result (Initializer {runInitializer}) (Inspector {inspect}) z <- boringWiring inspection <- inspect print inspection print z@@ -228,10 +231,10 @@ -- wiring with Cauldron merr <- case coolWiring of Left badBeans -> do- putStrLn $ prettyRecipeError badBeans+ putStrLn $ prettyCookingError badBeans pure $ Just badBeans Right action -> do- Entrypoint (Initializer {runInitializer}) (Inspector {inspect}) z <- action+ Result (Initializer {runInitializer}) (Inspector {inspect}) z <- action inspection <- inspect print inspection print z@@ -239,7 +242,7 @@ pure $ Nothing let depGraph = getDependencyGraph cauldron writeAsDot (defaultStyle merr) "beans.dot" depGraph- writeAsDot (defaultStyle merr) "beans-no-agg.dot" $ removeSecondaryBeans $ depGraph- writeAsDot (defaultStyle merr) "beans-no-agg-no-decos.dot" $ removeDecos $ removeSecondaryBeans $ depGraph- writeAsDot (defaultStyle merr) "beans-simple.dot" $ collapseToPrimaryBeans $ removeDecos $ removeSecondaryBeans $ depGraph- writeAsDot (defaultStyle merr) "beans-simple-with-decos.dot" $ collapseToPrimaryBeans $ removeSecondaryBeans $ depGraph+ writeAsDot (defaultStyle merr) "beans-no-agg.dot" $ removeAggregates $ depGraph+ writeAsDot (defaultStyle merr) "beans-no-agg-no-decos.dot" $ removeDecos $ removeAggregates $ depGraph+ writeAsDot (defaultStyle merr) "beans-simple.dot" $ collapseBeans $ removeDecos $ removeAggregates $ depGraph+ writeAsDot (defaultStyle merr) "beans-simple-with-decos.dot" $ collapseBeans $ removeAggregates $ depGraph
cauldron.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: cauldron-version: 0.6.1.0+version: 0.7.0.0 synopsis: Dependency injection library description: Dependency injection library that wires things at runtime. license: BSD-3-Clause@@ -26,8 +26,7 @@ common common-lib import: common-base build-depends:- containers >= 0.5.0 && < 0.8,- text >= 2.0 && < 2.2,+ containers >= 0.7 && < 0.9, common common-tests import: common-lib@@ -35,14 +34,13 @@ tasty ^>= 1.5, tasty-hunit ^>= 0.10, transformers >= 0.5 && < 0.7,- algebraic-graphs ^>= 0.7, cauldron,+ cauldron:graph, library import: common-lib build-depends:- algebraic-graphs ^>= 0.7,- bytestring >= 0.10.0 && < 0.13,+ cauldron:graph, hs-source-dirs: lib exposed-modules: Cauldron@@ -50,6 +48,18 @@ Cauldron.Args Cauldron.Managed Cauldron.Builder+ other-modules:+ Cauldron.Args.Internal++library graph+ import: common-lib+ hs-source-dirs: lib-graph+ exposed-modules: + Cauldron.Graph+ Cauldron.Graph.Algorithm+ Cauldron.Graph.Export+ Cauldron.Graph.Export.Dot+ visibility: public executable cauldron-example-wiring import: common-base
+ lib-graph/Cauldron/Graph.hs view
@@ -0,0 +1,921 @@+-----------------------------------------------------------------------------+-- |+-- Module : Algebra.Graph.AdjacencyMap+-- Copyright : (c) Andrey Mokhov 2016-2024+-- License : MIT (see the file LICENSE)+-- Maintainer : andrey.mokhov@gmail.com+-- Stability : experimental+--+-- __Alga__ is a library for algebraic construction and manipulation of graphs+-- in Haskell. See <https://github.com/snowleopard/alga-paper this paper> for the+-- motivation behind the library, the underlying theory, and implementation details.+--+-- This module defines the 'AdjacencyMap' data type and associated functions.+-- See "Algebra.Graph.AdjacencyMap.Algorithm" for basic graph algorithms.+-- 'AdjacencyMap' is an instance of the 'C.Graph' type class, which can be used+-- for polymorphic graph construction and manipulation.+-- "Algebra.Graph.AdjacencyIntMap" defines adjacency maps specialised to graphs+-- with @Int@ vertices.+-----------------------------------------------------------------------------+module Cauldron.Graph (+ -- * Data structure+ AdjacencyMap, adjacencyMap,++ -- * Basic graph construction primitives+ empty, vertex, edge, overlay, connect, vertices, edges, overlays, connects,++ -- * Relations on graphs+ isSubgraphOf,++ -- * Graph properties+ isEmpty, hasVertex, hasEdge, vertexCount, edgeCount, vertexList, edgeList,+ adjacencyList, vertexSet, edgeSet, preSet, postSet,++ -- * Standard families of graphs+ path, circuit, clique, biclique, star, stars, fromAdjacencySets, tree,+ forest,++ -- * Graph transformation+ removeVertex, removeEdge, replaceVertex, mergeVertices, transpose, gmap,+ induce, induceJust,++ -- * Graph composition+ compose, box,++ -- * Relational operations+ closure, reflexiveClosure, symmetricClosure, transitiveClosure,++ -- * Miscellaneous+ consistent+ ) where++import Data.List ((\\))+import Data.Map.Strict (Map)+import Data.Monoid+import Data.Set (Set)+import Data.String+import Data.Tree hiding (edges)+import GHC.Generics++import qualified Data.Map.Strict as Map+import qualified Data.Maybe as Maybe+import qualified Data.Set as Set++{-| The 'AdjacencyMap' data type represents a graph by a map of vertices to+their adjacency sets. We define a 'Num' instance as a convenient notation for+working with graphs:++@+0 == 'vertex' 0+1 + 2 == 'overlay' ('vertex' 1) ('vertex' 2)+1 * 2 == 'connect' ('vertex' 1) ('vertex' 2)+1 + 2 * 3 == 'overlay' ('vertex' 1) ('connect' ('vertex' 2) ('vertex' 3))+1 * (2 + 3) == 'connect' ('vertex' 1) ('overlay' ('vertex' 2) ('vertex' 3))+@++__Note:__ the 'Num' instance does not satisfy several "customary laws" of 'Num',+which dictate that 'fromInteger' @0@ and 'fromInteger' @1@ should act as+additive and multiplicative identities, and 'negate' as additive inverse.+Nevertheless, overloading 'fromInteger', '+' and '*' is very convenient when+working with algebraic graphs; we hope that in future Haskell's Prelude will+provide a more fine-grained class hierarchy for algebraic structures, which we+would be able to utilise without violating any laws.++The 'Show' instance is defined using basic graph construction primitives:++@show (empty :: AdjacencyMap Int) == "empty"+show (1 :: AdjacencyMap Int) == "vertex 1"+show (1 + 2 :: AdjacencyMap Int) == "vertices [1,2]"+show (1 * 2 :: AdjacencyMap Int) == "edge 1 2"+show (1 * 2 * 3 :: AdjacencyMap Int) == "edges [(1,2),(1,3),(2,3)]"+show (1 * 2 + 3 :: AdjacencyMap Int) == "overlay (vertex 3) (edge 1 2)"@++The 'Eq' instance satisfies all axioms of algebraic graphs:++ * 'overlay' is commutative and associative:++ > x + y == y + x+ > x + (y + z) == (x + y) + z++ * 'connect' is associative and has 'empty' as the identity:++ > x * empty == x+ > empty * x == x+ > x * (y * z) == (x * y) * z++ * 'connect' distributes over 'overlay':++ > x * (y + z) == x * y + x * z+ > (x + y) * z == x * z + y * z++ * 'connect' can be decomposed:++ > x * y * z == x * y + x * z + y * z++The following useful theorems can be proved from the above set of axioms.++ * 'overlay' has 'empty' as the identity and is idempotent:++ > x + empty == x+ > empty + x == x+ > x + x == x++ * Absorption and saturation of 'connect':++ > x * y + x + y == x * y+ > x * x * x == x * x++When specifying the time and memory complexity of graph algorithms, /n/ and /m/+will denote the number of vertices and edges in the graph, respectively.++The total order on graphs is defined using /size-lexicographic/ comparison:++* Compare the number of vertices. In case of a tie, continue.+* Compare the sets of vertices. In case of a tie, continue.+* Compare the number of edges. In case of a tie, continue.+* Compare the sets of edges.++Here are a few examples:++@'vertex' 1 < 'vertex' 2+'vertex' 3 < 'edge' 1 2+'vertex' 1 < 'edge' 1 1+'edge' 1 1 < 'edge' 1 2+'edge' 1 2 < 'edge' 1 1 + 'edge' 2 2+'edge' 1 2 < 'edge' 1 3@++Note that the resulting order refines the 'isSubgraphOf' relation and is+compatible with 'overlay' and 'connect' operations:++@'isSubgraphOf' x y ==> x <= y@++@'empty' <= x+x <= x + y+x + y <= x * y@+-}+newtype AdjacencyMap a = AM {+ -- | The /adjacency map/ of a graph: each vertex is associated with a set of+ -- its direct successors. Complexity: /O(1)/ time and memory.+ --+ -- @+ -- adjacencyMap 'empty' == Map.'Map.empty'+ -- adjacencyMap ('vertex' x) == Map.'Map.singleton' x Set.'Set.empty'+ -- adjacencyMap ('edge' 1 1) == Map.'Map.singleton' 1 (Set.'Set.singleton' 1)+ -- adjacencyMap ('edge' 1 2) == Map.'Map.fromList' [(1,Set.'Set.singleton' 2), (2,Set.'Set.empty')]+ -- @+ adjacencyMap :: Map a (Set a) } deriving (Eq, Generic)++instance Ord a => Ord (AdjacencyMap a) where+ compare x y = mconcat+ [ compare (vertexCount x) (vertexCount y)+ , compare (vertexSet x) (vertexSet y)+ , compare (edgeCount x) (edgeCount y)+ , compare (edgeSet x) (edgeSet y) ]++instance (Ord a, Show a) => Show (AdjacencyMap a) where+ showsPrec p am@(AM m)+ | null vs = showString "empty"+ | null es = showParen (p > 10) $ vshow vs+ | vs == used = showParen (p > 10) $ eshow es+ | otherwise = showParen (p > 10) $ showString "overlay ("+ . vshow (vs \\ used) . showString ") ("+ . eshow es . showString ")"+ where+ vs = vertexList am+ es = edgeList am+ vshow [x] = showString "vertex " . showsPrec 11 x+ vshow xs = showString "vertices " . showsPrec 11 xs+ eshow [(x, y)] = showString "edge " . showsPrec 11 x .+ showString " " . showsPrec 11 y+ eshow xs = showString "edges " . showsPrec 11 xs+ used = Set.toAscList (referredToVertexSet m)++-- | __Note:__ this does not satisfy the usual ring laws; see 'AdjacencyMap'+-- for more details.+instance (Ord a, Num a) => Num (AdjacencyMap a) where+ fromInteger = vertex . fromInteger+ (+) = overlay+ (*) = connect+ signum = const empty+ abs = id+ negate = id++instance IsString a => IsString (AdjacencyMap a) where+ fromString = vertex . fromString++-- | Defined via 'overlay'.+instance Ord a => Semigroup (AdjacencyMap a) where+ (<>) = overlay++-- | Defined via 'overlay' and 'empty'.+instance Ord a => Monoid (AdjacencyMap a) where+ mempty = empty++-- | Construct the /empty graph/.+--+-- @+-- 'isEmpty' empty == True+-- 'hasVertex' x empty == False+-- 'vertexCount' empty == 0+-- 'edgeCount' empty == 0+-- @+empty :: AdjacencyMap a+empty = AM Map.empty+{-# NOINLINE [1] empty #-}++-- | Construct the graph comprising /a single isolated vertex/.+--+-- @+-- 'isEmpty' (vertex x) == False+-- 'hasVertex' x (vertex y) == (x == y)+-- 'vertexCount' (vertex x) == 1+-- 'edgeCount' (vertex x) == 0+-- @+vertex :: a -> AdjacencyMap a+vertex x = AM $ Map.singleton x Set.empty+{-# NOINLINE [1] vertex #-}++-- | Construct the graph comprising /a single edge/.+--+-- @+-- edge x y == 'connect' ('vertex' x) ('vertex' y)+-- 'hasEdge' x y (edge x y) == True+-- 'edgeCount' (edge x y) == 1+-- 'vertexCount' (edge 1 1) == 1+-- 'vertexCount' (edge 1 2) == 2+-- @+edge :: Ord a => a -> a -> AdjacencyMap a+edge x y | x == y = AM $ Map.singleton x (Set.singleton y)+ | otherwise = AM $ Map.fromList [(x, Set.singleton y), (y, Set.empty)]++-- | /Overlay/ two graphs. This is a commutative, associative and idempotent+-- operation with the identity 'empty'.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- 'isEmpty' (overlay x y) == 'isEmpty' x && 'isEmpty' y+-- 'hasVertex' z (overlay x y) == 'hasVertex' z x || 'hasVertex' z y+-- 'vertexCount' (overlay x y) >= 'vertexCount' x+-- 'vertexCount' (overlay x y) <= 'vertexCount' x + 'vertexCount' y+-- 'edgeCount' (overlay x y) >= 'edgeCount' x+-- 'edgeCount' (overlay x y) <= 'edgeCount' x + 'edgeCount' y+-- 'vertexCount' (overlay 1 2) == 2+-- 'edgeCount' (overlay 1 2) == 0+-- @+overlay :: Ord a => AdjacencyMap a -> AdjacencyMap a -> AdjacencyMap a+overlay (AM x) (AM y) = AM $ Map.unionWith Set.union x y+{-# NOINLINE [1] overlay #-}++-- | /Connect/ two graphs. This is an associative operation with the identity+-- 'empty', which distributes over 'overlay' and obeys the decomposition axiom.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory. Note that the+-- number of edges in the resulting graph is quadratic with respect to the number+-- of vertices of the arguments: /m = O(m1 + m2 + n1 * n2)/.+--+-- @+-- 'isEmpty' (connect x y) == 'isEmpty' x && 'isEmpty' y+-- 'hasVertex' z (connect x y) == 'hasVertex' z x || 'hasVertex' z y+-- 'vertexCount' (connect x y) >= 'vertexCount' x+-- 'vertexCount' (connect x y) <= 'vertexCount' x + 'vertexCount' y+-- 'edgeCount' (connect x y) >= 'edgeCount' x+-- 'edgeCount' (connect x y) >= 'edgeCount' y+-- 'edgeCount' (connect x y) >= 'vertexCount' x * 'vertexCount' y+-- 'edgeCount' (connect x y) <= 'vertexCount' x * 'vertexCount' y + 'edgeCount' x + 'edgeCount' y+-- 'vertexCount' (connect 1 2) == 2+-- 'edgeCount' (connect 1 2) == 1+-- @+connect :: Ord a => AdjacencyMap a -> AdjacencyMap a -> AdjacencyMap a+connect (AM x) (AM y) = AM $ Map.unionsWith Set.union+ [ x, y, Map.fromSet (const $ Map.keysSet y) (Map.keysSet x) ]+{-# NOINLINE [1] connect #-}++-- | Construct the graph comprising a given list of isolated vertices.+-- Complexity: /O(L * log(L))/ time and /O(L)/ memory, where /L/ is the length+-- of the given list.+--+-- @+-- vertices [] == 'empty'+-- vertices [x] == 'vertex' x+-- vertices == 'overlays' . map 'vertex'+-- 'hasVertex' x . vertices == 'elem' x+-- 'vertexCount' . vertices == 'length' . 'Data.List.nub'+-- 'vertexSet' . vertices == Set.'Set.fromList'+-- @+vertices :: Ord a => [a] -> AdjacencyMap a+vertices = AM . Map.fromList . map (, Set.empty)+{-# NOINLINE [1] vertices #-}++-- | Construct the graph from a list of edges.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- edges [] == 'empty'+-- edges [(x,y)] == 'edge' x y+-- edges == 'overlays' . 'map' ('uncurry' 'edge')+-- 'edgeCount' . edges == 'length' . 'Data.List.nub'+-- 'edgeList' . edges == 'Data.List.nub' . 'Data.List.sort'+-- @+edges :: Ord a => [(a, a)] -> AdjacencyMap a+edges = fromAdjacencySets . map (fmap Set.singleton)++-- | Overlay a given list of graphs.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- overlays [] == 'empty'+-- overlays [x] == x+-- overlays [x,y] == 'overlay' x y+-- overlays == 'foldr' 'overlay' 'empty'+-- 'isEmpty' . overlays == 'all' 'isEmpty'+-- @+overlays :: Ord a => [AdjacencyMap a] -> AdjacencyMap a+overlays = AM . Map.unionsWith Set.union . map adjacencyMap+{-# NOINLINE overlays #-}++-- | Connect a given list of graphs.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- connects [] == 'empty'+-- connects [x] == x+-- connects [x,y] == 'connect' x y+-- connects == 'foldr' 'connect' 'empty'+-- 'isEmpty' . connects == 'all' 'isEmpty'+-- @+connects :: Ord a => [AdjacencyMap a] -> AdjacencyMap a+connects = foldr connect empty+{-# NOINLINE connects #-}++-- | The 'isSubgraphOf' function takes two graphs and returns 'True' if the+-- first graph is a /subgraph/ of the second.+-- Complexity: /O((n + m) * log(n))/ time.+--+-- @+-- isSubgraphOf 'empty' x == True+-- isSubgraphOf ('vertex' x) 'empty' == False+-- isSubgraphOf x ('overlay' x y) == True+-- isSubgraphOf ('overlay' x y) ('connect' x y) == True+-- isSubgraphOf ('path' xs) ('circuit' xs) == True+-- isSubgraphOf x y ==> x <= y+-- @+isSubgraphOf :: Ord a => AdjacencyMap a -> AdjacencyMap a -> Bool+isSubgraphOf (AM x) (AM y) = Map.isSubmapOfBy Set.isSubsetOf x y++-- | Check if a graph is empty.+-- Complexity: /O(1)/ time.+--+-- @+-- isEmpty 'empty' == True+-- isEmpty ('overlay' 'empty' 'empty') == True+-- isEmpty ('vertex' x) == False+-- isEmpty ('removeVertex' x $ 'vertex' x) == True+-- isEmpty ('removeEdge' x y $ 'edge' x y) == False+-- @+isEmpty :: AdjacencyMap a -> Bool+isEmpty = Map.null . adjacencyMap++-- | Check if a graph contains a given vertex.+-- Complexity: /O(log(n))/ time.+--+-- @+-- hasVertex x 'empty' == False+-- hasVertex x ('vertex' y) == (x == y)+-- hasVertex x . 'removeVertex' x == 'const' False+-- @+hasVertex :: Ord a => a -> AdjacencyMap a -> Bool+hasVertex x = Map.member x . adjacencyMap++-- | Check if a graph contains a given edge.+-- Complexity: /O(log(n))/ time.+--+-- @+-- hasEdge x y 'empty' == False+-- hasEdge x y ('vertex' z) == False+-- hasEdge x y ('edge' x y) == True+-- hasEdge x y . 'removeEdge' x y == 'const' False+-- hasEdge x y == 'elem' (x,y) . 'edgeList'+-- @+hasEdge :: Ord a => a -> a -> AdjacencyMap a -> Bool+hasEdge u v (AM m) = case Map.lookup u m of+ Nothing -> False+ Just vs -> Set.member v vs++-- | The number of vertices in a graph.+-- Complexity: /O(1)/ time.+--+-- @+-- vertexCount 'empty' == 0+-- vertexCount ('vertex' x) == 1+-- vertexCount == 'length' . 'vertexList'+-- vertexCount x \< vertexCount y ==> x \< y+-- @+vertexCount :: AdjacencyMap a -> Int+vertexCount = Map.size . adjacencyMap++-- | The number of edges in a graph.+-- Complexity: /O(n)/ time.+--+-- @+-- edgeCount 'empty' == 0+-- edgeCount ('vertex' x) == 0+-- edgeCount ('edge' x y) == 1+-- edgeCount == 'length' . 'edgeList'+-- @+edgeCount :: AdjacencyMap a -> Int+edgeCount = getSum . foldMap (Sum . Set.size) . adjacencyMap++-- | The sorted list of vertices of a given graph.+-- Complexity: /O(n)/ time and memory.+--+-- @+-- vertexList 'empty' == []+-- vertexList ('vertex' x) == [x]+-- vertexList . 'vertices' == 'Data.List.nub' . 'Data.List.sort'+-- @+vertexList :: AdjacencyMap a -> [a]+vertexList = Map.keys . adjacencyMap++-- | The sorted list of edges of a graph.+-- Complexity: /O(n + m)/ time and /O(m)/ memory.+--+-- @+-- edgeList 'empty' == []+-- edgeList ('vertex' x) == []+-- edgeList ('edge' x y) == [(x,y)]+-- edgeList ('star' 2 [3,1]) == [(2,1), (2,3)]+-- edgeList . 'edges' == 'Data.List.nub' . 'Data.List.sort'+-- edgeList . 'transpose' == 'Data.List.sort' . 'map' 'Data.Tuple.swap' . edgeList+-- @+edgeList :: AdjacencyMap a -> [(a, a)]+edgeList (AM m) = [ (x, y) | (x, ys) <- Map.toAscList m, y <- Set.toAscList ys ]+{-# INLINE edgeList #-}++-- | The set of vertices of a given graph.+-- Complexity: /O(n)/ time and memory.+--+-- @+-- vertexSet 'empty' == Set.'Set.empty'+-- vertexSet . 'vertex' == Set.'Set.singleton'+-- vertexSet . 'vertices' == Set.'Set.fromList'+-- @+vertexSet :: AdjacencyMap a -> Set a+vertexSet = Map.keysSet . adjacencyMap++-- | The set of edges of a given graph.+-- Complexity: /O((n + m) * log(m))/ time and /O(m)/ memory.+--+-- @+-- edgeSet 'empty' == Set.'Set.empty'+-- edgeSet ('vertex' x) == Set.'Set.empty'+-- edgeSet ('edge' x y) == Set.'Set.singleton' (x,y)+-- edgeSet . 'edges' == Set.'Set.fromList'+-- @+edgeSet :: Eq a => AdjacencyMap a -> Set (a, a)+edgeSet = Set.fromAscList . edgeList++-- | The sorted /adjacency list/ of a graph.+-- Complexity: /O(n + m)/ time and memory.+--+-- @+-- adjacencyList 'empty' == []+-- adjacencyList ('vertex' x) == [(x, [])]+-- adjacencyList ('edge' 1 2) == [(1, [2]), (2, [])]+-- adjacencyList ('star' 2 [3,1]) == [(1, []), (2, [1,3]), (3, [])]+-- 'stars' . adjacencyList == id+-- @+adjacencyList :: AdjacencyMap a -> [(a, [a])]+adjacencyList = map (fmap Set.toAscList) . Map.toAscList . adjacencyMap++-- | The /preset/ of an element @x@ is the set of its /direct predecessors/.+-- Complexity: /O(n * log(n))/ time and /O(n)/ memory.+--+-- @+-- preSet x 'empty' == Set.'Set.empty'+-- preSet x ('vertex' x) == Set.'Set.empty'+-- preSet 1 ('edge' 1 2) == Set.'Set.empty'+-- preSet y ('edge' x y) == Set.'Set.fromList' [x]+-- @+preSet :: Ord a => a -> AdjacencyMap a -> Set a+preSet x = Set.fromAscList . map fst . filter p . Map.toAscList . adjacencyMap+ where+ p (_, set) = x `Set.member` set++-- | The /postset/ of a vertex is the set of its /direct successors/.+-- Complexity: /O(log(n))/ time and /O(1)/ memory.+--+-- @+-- postSet x 'empty' == Set.'Set.empty'+-- postSet x ('vertex' x) == Set.'Set.empty'+-- postSet x ('edge' x y) == Set.'Set.fromList' [y]+-- postSet 2 ('edge' 1 2) == Set.'Set.empty'+-- @+postSet :: Ord a => a -> AdjacencyMap a -> Set a+postSet x = Map.findWithDefault Set.empty x . adjacencyMap++-- | The /path/ on a list of vertices.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- path [] == 'empty'+-- path [x] == 'vertex' x+-- path [x,y] == 'edge' x y+-- path . 'reverse' == 'transpose' . path+-- @+path :: Ord a => [a] -> AdjacencyMap a+path xs = case xs of [] -> empty+ [x] -> vertex x+ (_:ys) -> edges (zip xs ys)++-- | The /circuit/ on a list of vertices.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- circuit [] == 'empty'+-- circuit [x] == 'edge' x x+-- circuit [x,y] == 'edges' [(x,y), (y,x)]+-- circuit . 'reverse' == 'transpose' . circuit+-- @+circuit :: Ord a => [a] -> AdjacencyMap a+circuit [] = empty+circuit (x:xs) = path $ [x] ++ xs ++ [x]++-- | The /clique/ on a list of vertices.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- clique [] == 'empty'+-- clique [x] == 'vertex' x+-- clique [x,y] == 'edge' x y+-- clique [x,y,z] == 'edges' [(x,y), (x,z), (y,z)]+-- clique (xs '++' ys) == 'connect' (clique xs) (clique ys)+-- clique . 'reverse' == 'transpose' . clique+-- @+clique :: Ord a => [a] -> AdjacencyMap a+clique = fromAdjacencySets . fst . go+ where+ go [] = ([], Set.empty)+ go (x:xs) = let (res, set) = go xs in ((x, set) : res, Set.insert x set)+{-# NOINLINE [1] clique #-}++-- | The /biclique/ on two lists of vertices.+-- Complexity: /O(n * log(n) + m)/ time and /O(n + m)/ memory.+--+-- @+-- biclique [] [] == 'empty'+-- biclique [x] [] == 'vertex' x+-- biclique [] [y] == 'vertex' y+-- biclique [x1,x2] [y1,y2] == 'edges' [(x1,y1), (x1,y2), (x2,y1), (x2,y2)]+-- biclique xs ys == 'connect' ('vertices' xs) ('vertices' ys)+-- @+biclique :: Ord a => [a] -> [a] -> AdjacencyMap a+biclique xs ys = AM $ Map.fromSet adjacent (x `Set.union` y)+ where+ x = Set.fromList xs+ y = Set.fromList ys+ adjacent v = if v `Set.member` x then y else Set.empty++-- TODO: Optimise.+-- | The /star/ formed by a centre vertex connected to a list of leaves.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- star x [] == 'vertex' x+-- star x [y] == 'edge' x y+-- star x [y,z] == 'edges' [(x,y), (x,z)]+-- star x ys == 'connect' ('vertex' x) ('vertices' ys)+-- @+star :: Ord a => a -> [a] -> AdjacencyMap a+star x [] = vertex x+star x ys = connect (vertex x) (vertices ys)+{-# INLINE star #-}++-- | The /stars/ formed by overlaying a list of 'star's. An inverse of+-- 'adjacencyList'.+-- Complexity: /O(L * log(n))/ time, memory and size, where /L/ is the total+-- size of the input.+--+-- @+-- stars [] == 'empty'+-- stars [(x, [])] == 'vertex' x+-- stars [(x, [y])] == 'edge' x y+-- stars [(x, ys)] == 'star' x ys+-- stars == 'overlays' . 'map' ('uncurry' 'star')+-- stars . 'adjacencyList' == id+-- 'overlay' (stars xs) (stars ys) == stars (xs '++' ys)+-- @+stars :: Ord a => [(a, [a])] -> AdjacencyMap a+stars = fromAdjacencySets . map (fmap Set.fromList)++-- | Construct a graph from a list of adjacency sets; a variation of 'stars'.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- fromAdjacencySets [] == 'empty'+-- fromAdjacencySets [(x, Set.'Set.empty')] == 'vertex' x+-- fromAdjacencySets [(x, Set.'Set.singleton' y)] == 'edge' x y+-- fromAdjacencySets . 'map' ('fmap' Set.'Set.fromList') == 'stars'+-- 'overlay' (fromAdjacencySets xs) (fromAdjacencySets ys) == fromAdjacencySets (xs '++' ys)+-- @+fromAdjacencySets :: Ord a => [(a, Set a)] -> AdjacencyMap a+fromAdjacencySets ss = AM $ Map.unionWith Set.union vs es+ where+ vs = Map.fromSet (const Set.empty) . Set.unions $ map snd ss+ es = Map.fromListWith Set.union ss++-- | The /tree graph/ constructed from a given 'Tree' data structure.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- tree (Node x []) == 'vertex' x+-- tree (Node x [Node y [Node z []]]) == 'path' [x,y,z]+-- tree (Node x [Node y [], Node z []]) == 'star' x [y,z]+-- tree (Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]) == 'edges' [(1,2), (1,3), (3,4), (3,5)]+-- @+tree :: Ord a => Tree a -> AdjacencyMap a+tree (Node x []) = vertex x+tree (Node x f ) = star x (map rootLabel f)+ `overlay` forest (filter (not . null . subForest) f)++-- | The /forest graph/ constructed from a given 'Forest' data structure.+-- Complexity: /O((n + m) * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- forest [] == 'empty'+-- forest [x] == 'tree' x+-- forest [Node 1 [Node 2 [], Node 3 []], Node 4 [Node 5 []]] == 'edges' [(1,2), (1,3), (4,5)]+-- forest == 'overlays' . 'map' 'tree'+-- @+forest :: Ord a => Forest a -> AdjacencyMap a+forest = overlays . map tree++-- | Remove a vertex from a given graph.+-- Complexity: /O(n*log(n))/ time.+--+-- @+-- removeVertex x ('vertex' x) == 'empty'+-- removeVertex 1 ('vertex' 2) == 'vertex' 2+-- removeVertex x ('edge' x x) == 'empty'+-- removeVertex 1 ('edge' 1 2) == 'vertex' 2+-- removeVertex x . removeVertex x == removeVertex x+-- @+removeVertex :: Ord a => a -> AdjacencyMap a -> AdjacencyMap a+removeVertex x = AM . Map.map (Set.delete x) . Map.delete x . adjacencyMap++-- | Remove an edge from a given graph.+-- Complexity: /O(log(n))/ time.+--+-- @+-- removeEdge x y ('edge' x y) == 'vertices' [x,y]+-- removeEdge x y . removeEdge x y == removeEdge x y+-- removeEdge x y . 'removeVertex' x == 'removeVertex' x+-- removeEdge 1 1 (1 * 1 * 2 * 2) == 1 * 2 * 2+-- removeEdge 1 2 (1 * 1 * 2 * 2) == 1 * 1 + 2 * 2+-- @+removeEdge :: Ord a => a -> a -> AdjacencyMap a -> AdjacencyMap a+removeEdge x y = AM . Map.adjust (Set.delete y) x . adjacencyMap++-- | The function @'replaceVertex' x y@ replaces vertex @x@ with vertex @y@ in a+-- given 'AdjacencyMap'. If @y@ already exists, @x@ and @y@ will be merged.+-- Complexity: /O((n + m) * log(n))/ time.+--+-- @+-- replaceVertex x x == id+-- replaceVertex x y ('vertex' x) == 'vertex' y+-- replaceVertex x y == 'mergeVertices' (== x) y+-- @+replaceVertex :: Ord a => a -> a -> AdjacencyMap a -> AdjacencyMap a+replaceVertex u v = gmap $ \w -> if w == u then v else w++-- | Merge vertices satisfying a given predicate into a given vertex.+-- Complexity: /O((n + m) * log(n))/ time, assuming that the predicate takes+-- constant time.+--+-- @+-- mergeVertices ('const' False) x == id+-- mergeVertices (== x) y == 'replaceVertex' x y+-- mergeVertices 'even' 1 (0 * 2) == 1 * 1+-- mergeVertices 'odd' 1 (3 + 4 * 5) == 4 * 1+-- @+mergeVertices :: Ord a => (a -> Bool) -> a -> AdjacencyMap a -> AdjacencyMap a+mergeVertices p v = gmap $ \u -> if p u then v else u++-- | Transpose a given graph.+-- Complexity: /O(m * log(n))/ time, /O(n + m)/ memory.+--+-- @+-- transpose 'empty' == 'empty'+-- transpose ('vertex' x) == 'vertex' x+-- transpose ('edge' x y) == 'edge' y x+-- transpose . transpose == id+-- 'edgeList' . transpose == 'Data.List.sort' . 'map' 'Data.Tuple.swap' . 'edgeList'+-- @+transpose :: Ord a => AdjacencyMap a -> AdjacencyMap a+transpose (AM m) = AM $ Map.foldrWithKey combine vs m+ where+ combine v es = Map.unionWith Set.union (Map.fromSet (const $ Set.singleton v) es)+ vs = Map.fromSet (const Set.empty) (Map.keysSet m)+{-# NOINLINE [1] transpose #-}++{-# RULES+"transpose/empty" transpose empty = empty+"transpose/vertex" forall x. transpose (vertex x) = vertex x+"transpose/overlay" forall g1 g2. transpose (overlay g1 g2) = overlay (transpose g1) (transpose g2)+"transpose/connect" forall g1 g2. transpose (connect g1 g2) = connect (transpose g2) (transpose g1)++"transpose/overlays" forall xs. transpose (overlays xs) = overlays (map transpose xs)+"transpose/connects" forall xs. transpose (connects xs) = connects (reverse (map transpose xs))++"transpose/vertices" forall xs. transpose (vertices xs) = vertices xs+"transpose/clique" forall xs. transpose (clique xs) = clique (reverse xs)+ #-}++-- | Transform a graph by applying a function to each of its vertices. This is+-- similar to @Functor@'s 'fmap' but can be used with non-fully-parametric+-- 'AdjacencyMap'.+-- Complexity: /O((n + m) * log(n))/ time.+--+-- @+-- gmap f 'empty' == 'empty'+-- gmap f ('vertex' x) == 'vertex' (f x)+-- gmap f ('edge' x y) == 'edge' (f x) (f y)+-- gmap 'id' == 'id'+-- gmap f . gmap g == gmap (f . g)+-- @+gmap :: (Ord a, Ord b) => (a -> b) -> AdjacencyMap a -> AdjacencyMap b+gmap f = AM . Map.map (Set.map f) . Map.mapKeysWith Set.union f . adjacencyMap++-- | Construct the /induced subgraph/ of a given graph by removing the+-- vertices that do not satisfy a given predicate.+-- Complexity: /O(n + m)/ time, assuming that the predicate takes constant time.+--+-- @+-- induce ('const' True ) x == x+-- induce ('const' False) x == 'empty'+-- induce (/= x) == 'removeVertex' x+-- induce p . induce q == induce (\\x -> p x && q x)+-- 'isSubgraphOf' (induce p x) x == True+-- @+induce :: (a -> Bool) -> AdjacencyMap a -> AdjacencyMap a+induce p = AM . Map.map (Set.filter p) . Map.filterWithKey (\k _ -> p k) . adjacencyMap++-- | Construct the /induced subgraph/ of a given graph by removing the vertices+-- that are 'Nothing'.+-- Complexity: /O(n + m)/ time.+--+-- @+-- induceJust ('vertex' 'Nothing') == 'empty'+-- induceJust ('edge' ('Just' x) 'Nothing') == 'vertex' x+-- induceJust . 'gmap' 'Just' == 'id'+-- induceJust . 'gmap' (\\x -> if p x then 'Just' x else 'Nothing') == 'induce' p+-- @+induceJust :: Ord a => AdjacencyMap (Maybe a) -> AdjacencyMap a+induceJust = AM . Map.map catMaybesSet . catMaybesMap . adjacencyMap+ where+ catMaybesSet = Set.mapMonotonic Maybe.fromJust . Set.delete Nothing+ catMaybesMap = Map.mapKeysMonotonic Maybe.fromJust . Map.delete Nothing++-- | Left-to-right /relational composition/ of graphs: vertices @x@ and @z@ are+-- connected in the resulting graph if there is a vertex @y@, such that @x@ is+-- connected to @y@ in the first graph, and @y@ is connected to @z@ in the+-- second graph. There are no isolated vertices in the result. This operation is+-- associative, has 'empty' and single-'vertex' graphs as /annihilating zeroes/,+-- and distributes over 'overlay'.+-- Complexity: /O(n * m * log(n))/ time and /O(n + m)/ memory.+--+-- @+-- compose 'empty' x == 'empty'+-- compose x 'empty' == 'empty'+-- compose ('vertex' x) y == 'empty'+-- compose x ('vertex' y) == 'empty'+-- compose x (compose y z) == compose (compose x y) z+-- compose x ('overlay' y z) == 'overlay' (compose x y) (compose x z)+-- compose ('overlay' x y) z == 'overlay' (compose x z) (compose y z)+-- compose ('edge' x y) ('edge' y z) == 'edge' x z+-- compose ('path' [1..5]) ('path' [1..5]) == 'edges' [(1,3), (2,4), (3,5)]+-- compose ('circuit' [1..5]) ('circuit' [1..5]) == 'circuit' [1,3,5,2,4]+-- @+compose :: Ord a => AdjacencyMap a -> AdjacencyMap a -> AdjacencyMap a+compose x y = fromAdjacencySets+ [ (t, ys) | v <- Set.toList vs, let ys = postSet v y, not (Set.null ys)+ , t <- Set.toList (postSet v tx) ]+ where+ tx = transpose x+ vs = vertexSet x `Set.union` vertexSet y++-- | Compute the /Cartesian product/ of graphs.+-- Complexity: /O((n + m) * log(n))/ time and O(n + m) memory.+--+-- @+-- box ('path' [0,1]) ('path' "ab") == 'edges' [ ((0,\'a\'), (0,\'b\'))+-- , ((0,\'a\'), (1,\'a\'))+-- , ((0,\'b\'), (1,\'b\'))+-- , ((1,\'a\'), (1,\'b\')) ]+-- @+--+-- Up to isomorphism between the resulting vertex types, this operation is+-- /commutative/, /associative/, /distributes/ over 'overlay', has singleton+-- graphs as /identities/ and 'empty' as the /annihilating zero/. Below @~~@+-- stands for equality up to an isomorphism, e.g. @(x,@ @()) ~~ x@.+--+-- @+-- box x y ~~ box y x+-- box x (box y z) ~~ box (box x y) z+-- box x ('overlay' y z) == 'overlay' (box x y) (box x z)+-- box x ('vertex' ()) ~~ x+-- box x 'empty' ~~ 'empty'+-- 'transpose' (box x y) == box ('transpose' x) ('transpose' y)+-- 'vertexCount' (box x y) == 'vertexCount' x * 'vertexCount' y+-- 'edgeCount' (box x y) <= 'vertexCount' x * 'edgeCount' y + 'edgeCount' x * 'vertexCount' y+-- @+box :: (Ord a, Ord b) => AdjacencyMap a -> AdjacencyMap b -> AdjacencyMap (a, b)+box (AM x) (AM y) = overlay (AM $ Map.fromAscList xs) (AM $ Map.fromAscList ys)+ where+ xs = do (a, as) <- Map.toAscList x+ b <- Set.toAscList (Map.keysSet y)+ return ((a, b), Set.mapMonotonic (,b) as)+ ys = do a <- Set.toAscList (Map.keysSet x)+ (b, bs) <- Map.toAscList y+ return ((a, b), Set.mapMonotonic (a,) bs)++-- | Compute the /reflexive and transitive closure/ of a graph.+-- Complexity: /O(n * m * log(n)^2)/ time.+--+-- @+-- closure 'empty' == 'empty'+-- closure ('vertex' x) == 'edge' x x+-- closure ('edge' x x) == 'edge' x x+-- closure ('edge' x y) == 'edges' [(x,x), (x,y), (y,y)]+-- closure ('path' $ 'Data.List.nub' xs) == 'reflexiveClosure' ('clique' $ 'Data.List.nub' xs)+-- closure == 'reflexiveClosure' . 'transitiveClosure'+-- closure == 'transitiveClosure' . 'reflexiveClosure'+-- closure . closure == closure+-- 'postSet' x (closure y) == Set.'Set.fromList' ('Algebra.Graph.ToGraph.reachable' y x)+-- @+closure :: Ord a => AdjacencyMap a -> AdjacencyMap a+closure = reflexiveClosure . transitiveClosure++-- | Compute the /reflexive closure/ of a graph by adding a self-loop to every+-- vertex.+-- Complexity: /O(n * log(n))/ time.+--+-- @+-- reflexiveClosure 'empty' == 'empty'+-- reflexiveClosure ('vertex' x) == 'edge' x x+-- reflexiveClosure ('edge' x x) == 'edge' x x+-- reflexiveClosure ('edge' x y) == 'edges' [(x,x), (x,y), (y,y)]+-- reflexiveClosure . reflexiveClosure == reflexiveClosure+-- @+reflexiveClosure :: Ord a => AdjacencyMap a -> AdjacencyMap a+reflexiveClosure (AM m) = AM $ Map.mapWithKey Set.insert m++-- | Compute the /symmetric closure/ of a graph by overlaying it with its own+-- transpose.+-- Complexity: /O((n + m) * log(n))/ time.+--+-- @+-- symmetricClosure 'empty' == 'empty'+-- symmetricClosure ('vertex' x) == 'vertex' x+-- symmetricClosure ('edge' x y) == 'edges' [(x,y), (y,x)]+-- symmetricClosure x == 'overlay' x ('transpose' x)+-- symmetricClosure . symmetricClosure == symmetricClosure+-- @+symmetricClosure :: Ord a => AdjacencyMap a -> AdjacencyMap a+symmetricClosure m = overlay m (transpose m)++-- | Compute the /transitive closure/ of a graph.+-- Complexity: /O(n * m * log(n)^2)/ time.+--+-- @+-- transitiveClosure 'empty' == 'empty'+-- transitiveClosure ('vertex' x) == 'vertex' x+-- transitiveClosure ('edge' x y) == 'edge' x y+-- transitiveClosure ('path' $ 'Data.List.nub' xs) == 'clique' ('Data.List.nub' xs)+-- transitiveClosure . transitiveClosure == transitiveClosure+-- @+transitiveClosure :: Ord a => AdjacencyMap a -> AdjacencyMap a+transitiveClosure old+ | old == new = old+ | otherwise = transitiveClosure new+ where+ new = overlay old (old `compose` old)++-- | Check that the internal graph representation is consistent, i.e. that all+-- edges refer to existing vertices. It should be impossible to create an+-- inconsistent adjacency map, and we use this function in testing.+--+-- @+-- consistent 'empty' == True+-- consistent ('vertex' x) == True+-- consistent ('overlay' x y) == True+-- consistent ('connect' x y) == True+-- consistent ('edge' x y) == True+-- consistent ('edges' xs) == True+-- consistent ('stars' xs) == True+-- @+consistent :: Ord a => AdjacencyMap a -> Bool+consistent (AM m) = referredToVertexSet m `Set.isSubsetOf` Map.keysSet m++-- The set of vertices that are referred to by the edges of an adjacency map.+referredToVertexSet :: Ord a => Map a (Set a) -> Set a+referredToVertexSet m = Set.fromList $ concat+ [ [x, y] | (x, ys) <- Map.toAscList m, y <- Set.toAscList ys ]
+ lib-graph/Cauldron/Graph/Algorithm.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE LambdaCase #-}+module Cauldron.Graph.Algorithm (+ reverseTopSort+ ) where++import Cauldron.Graph+import Data.List.NonEmpty+import Data.Graph qualified+import Data.Foldable (for_)+import Data.Sequence qualified+import Data.Set qualified+import Data.Map.Strict qualified+import Data.Foldable qualified+import Data.Function ((&))++reverseTopSort :: Ord a => AdjacencyMap a -> Either (NonEmpty a) [a] +reverseTopSort g = do+ let theEdges = do+ (i,o) <- adjacencyList g + [(i,i,o)]+ sccs = Data.Graph.stronglyConnComp theEdges+ for_ sccs $ \case+ Data.Graph.AcyclicSCC _ -> pure ()+ Data.Graph.NECyclicSCC vs -> do+ let aCycle = findCycleInSCC g vs+ Left aCycle+ let (g',nodeFromVertex,_) = Data.Graph.graphFromEdges theEdges+ Right $ do+ ves <- Data.Graph.reverseTopSort g'+ let (v,_,_) = nodeFromVertex ves+ [v]++findCycleInSCC :: Ord a => AdjacencyMap a -> NonEmpty a -> NonEmpty a+findCycleInSCC g scc@(start :| _) = go start (Data.Set.singleton start) (Data.Sequence.singleton start)+ where+ sccSet = Data.Set.fromList . Data.Foldable.toList $ scc+ isInScc = (`Data.Set.member` sccSet)+ am = adjacencyMap $ Cauldron.Graph.induce isInScc g+ firstChildOf v = + case Data.Set.toList <$> Data.Map.Strict.lookup v am of+ Nothing -> error "findCycleInSCC: node not in adjacency map"+ -- In a SCC, all vertices should have at least one outgoing edge!+ Just [] -> error "findCycleInSCC: SCC node with no outgoing edge"+ Just (child:_) -> child+ go current visited cycleAcc =+ let child = firstChildOf current+ in if child `Data.Set.member` visited+ then+ Data.List.NonEmpty.fromList $ Data.Foldable.toList $ Data.Sequence.dropWhileL (/= child) cycleAcc+ else+ go child (visited & Data.Set.insert child) (cycleAcc Data.Sequence.|> child)
+ lib-graph/Cauldron/Graph/Export.hs view
@@ -0,0 +1,187 @@+{-# LANGUAGE OverloadedStrings #-}+-----------------------------------------------------------------------------+-- |+-- Module : Algebra.Graph.Export+-- Copyright : (c) Andrey Mokhov 2016-2024+-- License : MIT (see the file LICENSE)+-- Maintainer : andrey.mokhov@gmail.com+-- Stability : experimental+--+-- __Alga__ is a library for algebraic construction and manipulation of graphs+-- in Haskell. See <https://github.com/snowleopard/alga-paper this paper> for the+-- motivation behind the library, the underlying theory, and implementation details.+--+-- This module defines basic functionality for exporting graphs in textual and+-- binary formats. "Algebra.Graph.Export.Dot" provides DOT-specific functions.+-----------------------------------------------------------------------------+module Cauldron.Graph.Export (+ -- * Constructing and exporting documents+ Doc, isEmpty, literal, render,++ -- * Common combinators for text documents+ (<+>), brackets, doubleQuotes, indent, unlines,++ -- * Generic graph export+ export+ ) where++import Data.Foldable (fold)+import Data.String hiding (unlines)+import Prelude hiding (unlines)++import Cauldron.Graph (AdjacencyMap, vertexList, edgeList)++-- | An abstract document data type with /O(1)/ time concatenation (the current+-- implementation uses difference lists). Here @s@ is the type of abstract+-- symbols or strings (text or binary). 'Doc' @s@ is a 'Monoid', therefore+-- 'mempty' corresponds to the /empty document/ and two documents can be+-- concatenated with 'mappend' (or operator 'Data.Semigroup.<>'). Documents+-- comprising a single symbol or string can be constructed using the function+-- 'literal'. Alternatively, you can construct documents as string literals,+-- e.g. simply as @"alga"@, by using the @OverloadedStrings@ GHC extension. To+-- extract the document contents use the function 'render'.+--+-- Note that the document comprising a single empty string is considered to be+-- different from the empty document. This design choice is motivated by the+-- desire to support string types @s@ that have no 'Eq' instance, such as+-- "Data.ByteString.Builder", for which there is no way to check whether a+-- string is empty or not. As a consequence, the 'Eq' and 'Ord' instances are+-- defined as follows:+--+-- @+-- 'mempty' /= 'literal' ""+-- 'mempty' < 'literal' ""+-- @+newtype Doc s = Doc [s] deriving (Monoid, Semigroup)++instance (Monoid s, Show s) => Show (Doc s) where+ show = show . render++instance (Monoid s, Eq s) => Eq (Doc s) where+ x == y | isEmpty x = isEmpty y+ | isEmpty y = False+ | otherwise = render x == render y++-- | The empty document is smallest.+instance (Monoid s, Ord s) => Ord (Doc s) where+ compare x y | isEmpty x = if isEmpty y then EQ else LT+ | isEmpty y = GT+ | otherwise = compare (render x) (render y)++instance IsString s => IsString (Doc s) where+ fromString = literal . fromString++-- | Check if a document is empty. The result is the same as when comparing the+-- given document to 'mempty', but this function does not require the 'Eq' @s@+-- constraint. Note that the document comprising a single empty string is+-- considered to be different from the empty document.+--+-- @+-- isEmpty 'mempty' == True+-- isEmpty ('literal' \"\") == False+-- isEmpty x == (x == 'mempty')+-- @+isEmpty :: Doc s -> Bool+isEmpty (Doc xs) = null xs++-- | Construct a document comprising a single symbol or string. If @s@ is an+-- instance of class 'IsString', then documents of type 'Doc' @s@ can be+-- constructed directly from string literals (see the second example below).+--+-- @+-- literal "Hello, " 'Data.Semigroup.<>' literal "World!" == literal "Hello, World!"+-- literal "I am just a string literal" == "I am just a string literal"+-- 'render' . literal == 'id'+-- @+literal :: s -> Doc s+literal = Doc . pure++-- | Render the document as a single string. An inverse of the function 'literal'.+--+-- @+-- render ('literal' "al" 'Data.Semigroup.<>' 'literal' "ga") :: ('IsString' s, 'Monoid' s) => s+-- render ('literal' "al" 'Data.Semigroup.<>' 'literal' "ga") == "alga"+-- render 'mempty' == 'mempty'+-- render . 'literal' == 'id'+-- @+render :: Monoid s => Doc s -> s+render (Doc x) = fold x++-- | Concatenate two documents, separated by a single space, unless one of the+-- documents is empty. The operator \<+\> is associative with identity 'mempty'.+--+-- @+-- x \<+\> 'mempty' == x+-- 'mempty' \<+\> x == x+-- x \<+\> (y \<+\> z) == (x \<+\> y) \<+\> z+-- "name" \<+\> "surname" == "name surname"+-- @+(<+>) :: IsString s => Doc s -> Doc s -> Doc s+x <+> y | isEmpty x = y+ | isEmpty y = x+ | otherwise = x <> " " <> y++infixl 7 <+>++-- | Wrap a document in square brackets.+--+-- @+-- brackets "i" == "[i]"+-- brackets 'mempty' == "[]"+-- @+brackets :: IsString s => Doc s -> Doc s+brackets x = "[" <> x <> "]"++-- | Wrap a document into double quotes.+--+-- @+-- doubleQuotes "\/path\/with spaces" == "\\"\/path\/with spaces\\""+-- doubleQuotes (doubleQuotes 'mempty') == "\\"\\"\\"\\""+-- @+doubleQuotes :: IsString s => Doc s -> Doc s+doubleQuotes x = "\"" <> x <> "\""++-- | Prepend a given number of spaces to a document.+--+-- @+-- indent 0 == 'id'+-- indent 1 'mempty' == " "+-- @+indent :: IsString s => Int -> Doc s -> Doc s+indent spaces x = fromString (replicate spaces ' ') <> x++-- | Concatenate documents after appending a terminating newline symbol to each.+--+-- @+-- unlines [] == 'mempty'+-- unlines ['mempty'] == "\\n"+-- unlines ["title", "subtitle"] == "title\\nsubtitle\\n"+-- @+unlines :: IsString s => [Doc s] -> Doc s+unlines [] = mempty+unlines (x:xs) = x <> "\n" <> unlines xs++-- TODO: Avoid round-trip graph conversion if g :: AdjacencyMap a.+-- | Export a graph into a document given two functions that construct documents+-- for individual vertices and edges. The order of export is: vertices, sorted+-- by 'Ord' @a@, and then edges, sorted by 'Ord' @(a, a)@.+--+-- For example:+--+-- @+-- vDoc x = 'literal' ('show' x) <> "\\n"+-- eDoc x y = 'literal' ('show' x) <> " -> " <> 'literal' ('show' y) <> "\\n"+-- > putStrLn $ 'render' $ export vDoc eDoc (1 + 2 * (3 + 4) :: 'Algebra.Graph.Graph' Int)+--+-- 1+-- 2+-- 3+-- 4+-- 2 -> 3+-- 2 -> 4+-- @+export :: (Ord a) => (a -> Doc s) -> (a -> a -> Doc s) -> AdjacencyMap a -> Doc s+export v e adjMap = vDoc <> eDoc+ where+ vDoc = mconcat $ map v (vertexList adjMap)+ eDoc = mconcat $ map (uncurry e) (edgeList adjMap)
+ lib-graph/Cauldron/Graph/Export/Dot.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}+-----------------------------------------------------------------------------+-- |+-- Module : Algebra.Graph.Export.Dot+-- Copyright : (c) Andrey Mokhov 2016-2024+-- License : MIT (see the file LICENSE)+-- Maintainer : andrey.mokhov@gmail.com+-- Stability : experimental+--+-- __Alga__ is a library for algebraic construction and manipulation of graphs+-- in Haskell. See <https://github.com/snowleopard/alga-paper this paper> for the+-- motivation behind the library, the underlying theory, and implementation details.+--+-- This module defines functions for exporting graphs in the DOT file format.+-----------------------------------------------------------------------------+module Cauldron.Graph.Export.Dot (+ -- * Graph attributes and style+ Attribute (..), Quoting (..), Style (..), defaultStyle, defaultStyleViaShow,++ -- * Export functions+ export+ ) where++import Data.List (intersperse)+import Data.Monoid+import Data.String hiding (unlines)+import Prelude hiding (unlines)+import Cauldron.Graph+import Cauldron.Graph.Export hiding (export)+import Cauldron.Graph.Export qualified as E++-- | An attribute is just a key-value pair, for example @"shape" := "box"@.+-- Attributes are used to specify the style of graph elements during export.+data Attribute s = (:=) s s++-- TODO: Do we need other quoting styles, for example, 'SingleQuotes'?+-- TODO: Shall we use 'Quoting' for vertex names too?+-- | The style of quoting used when exporting attributes; 'DoubleQuotes' is the+-- default.+data Quoting = DoubleQuotes | NoQuotes++-- | The record 'Style' @a@ @s@ specifies the style to use when exporting a+-- graph in the DOT format. Here @a@ is the type of the graph vertices, and @s@+-- is the type of string to represent the resulting DOT document (e.g. String,+-- Text, etc.). The only field that has no obvious default value is+-- 'vertexName', which holds a function of type @a -> s@ to compute vertex+-- names. See the function 'export' for an example.+data Style a s = Style+ { graphName :: s+ -- ^ Name of the graph.+ , preamble :: [s]+ -- ^ Preamble (a list of lines) is added at the beginning of the DOT file body.+ , graphAttributes :: [Attribute s]+ -- ^ Graph style, e.g. @["bgcolor" := "azure"]@.+ , defaultVertexAttributes :: [Attribute s]+ -- ^ Default vertex style, e.g. @["shape" := "diamond"]@.+ , defaultEdgeAttributes :: [Attribute s]+ -- ^ Default edge style, e.g. @["style" := "dashed"]@.+ , vertexName :: a -> s+ -- ^ Compute a vertex name.+ , vertexAttributes :: a -> [Attribute s]+ -- ^ Attributes of a specific vertex.+ , edgeAttributes :: a -> a -> [Attribute s]+ -- ^ Attributes of a specific edge.+ , attributeQuoting :: Quoting+ -- ^ The quoting style used for attributes.+ }++-- | Default style for exporting graphs. The 'vertexName' field is provided as+-- the only argument; the other fields are set to trivial defaults.+defaultStyle :: Monoid s => (a -> s) -> Style a s+defaultStyle v = Style mempty [] [] [] [] v (const []) (\_ _ -> []) DoubleQuotes++-- | Default style for exporting graphs with 'Show'-able vertices. The+-- 'vertexName' field is computed using 'show'; the other fields are set to+-- trivial defaults.+--+-- @+-- defaultStyleViaShow = 'defaultStyle' ('fromString' . 'show')+-- @+defaultStyleViaShow :: (Show a, IsString s, Monoid s) => Style a s+defaultStyleViaShow = defaultStyle (fromString . show)++-- | Export a graph with a given style.+--+-- For example:+--+-- @+-- style :: 'Style' Int String+-- style = 'Style'+-- { 'graphName' = \"Example\"+-- , 'preamble' = [" // This is an example", ""]+-- , 'graphAttributes' = ["label" := \"Example\", "labelloc" := "top"]+-- , 'defaultVertexAttributes' = ["shape" := "circle"]+-- , 'defaultEdgeAttributes' = 'mempty'+-- , 'vertexName' = \\x -> "v" ++ 'show' x+-- , 'vertexAttributes' = \\x -> ["color" := "blue" | 'odd' x ]+-- , 'edgeAttributes' = \\x y -> ["style" := "dashed" | 'odd' (x * y)]+-- , 'attributeQuoting' = 'DoubleQuotes' }+--+-- > putStrLn $ export style (1 * 2 + 3 * 4 * 5 :: 'Graph' Int)+--+-- digraph Example+-- {+-- // This is an example+--+-- graph [label=\"Example\" labelloc="top"]+-- node [shape="circle"]+-- "v1" [color="blue"]+-- "v2"+-- "v3" [color="blue"]+-- "v4"+-- "v5" [color="blue"]+-- "v1" -> "v2"+-- "v3" -> "v4"+-- "v3" -> "v5" [style="dashed"]+-- "v4" -> "v5"+-- }+-- @+export :: (IsString s, Monoid s, Ord a) => Style a s -> AdjacencyMap a -> s+export Style {..} g = E.render $ header <> body <> "}\n"+ where+ header = "digraph" <+> literal graphName <> "\n{\n"+ with x as = if null as then mempty else line (x <+> attributes attributeQuoting as)+ line s = indent 2 s <> "\n"+ body = unlines (map literal preamble)+ <> ("graph" `with` graphAttributes)+ <> ("node" `with` defaultVertexAttributes)+ <> ("edge" `with` defaultEdgeAttributes)+ <> E.export vDoc eDoc g+ label = doubleQuotes . literal . vertexName+ vDoc x = line $ label x <+> attributes attributeQuoting (vertexAttributes x)+ eDoc x y = line $ label x <> " -> " <> label y <+> attributes attributeQuoting (edgeAttributes x y)++-- | Export a list of attributes using a specified quoting style.+-- Example: @attributes DoubleQuotes ["label" := "A label", "shape" := "box"]@+-- corresponds to document: @[label="A label" shape="box"]@.+attributes :: IsString s => Quoting -> [Attribute s] -> Doc s+attributes _ [] = mempty+attributes q as = brackets . mconcat . intersperse " " $ map dot as+ where+ dot (k := v) = literal k <> "=" <> quote (literal v)+ quote = case q of+ DoubleQuotes -> doubleQuotes+ NoQuotes -> id
lib/Cauldron.hs view
@@ -9,6 +9,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE NoFieldSelectors #-}+{-# LANGUAGE DuplicateRecordFields #-} -- | This is a library for performing dependency injection. It's an alternative -- to manually wiring your functions and passing all required parameters@@ -49,11 +50,10 @@ -- recipe @B $ val $ wire makeB, -- recipe @C $ eff $ wire makeC -- we use eff because the constructor has IO effects -- ]--- action <- either throwIO pure $ cook forbidDepCycles cauldron--- beans <- action--- pure $ taste @C beans+-- action <- cook @C forbidDepCycles cauldron & either throwIO pure+-- action -- :}--- Just C+-- C module Cauldron ( -- * Filling the cauldron Cauldron,@@ -103,13 +103,12 @@ hoistConstructor, hoistConstructor', - -- ** Registering secondary beans+ -- ** Registering aggregate beans -- $secondarybeans -- * Cooking the beans cook,- cookNonEmpty,- cookTree,+ nest, -- ** How loopy can we get? Fire,@@ -117,17 +116,13 @@ allowSelfDeps, allowDepCycles, - -- ** Tasting the results- Beans,- taste,- -- ** When things go wrong- RecipeError (..),+ CookingError (..), MissingDependencies (..), DoubleDutyBeans (..), DependencyCycle (..),- prettyRecipeError,- prettyRecipeErrorLines,+ prettyCookingError,+ prettyCookingErrorLines, -- ** Visualizing dependencies between beans. getDependencyGraph,@@ -140,24 +135,24 @@ -- *** Simplifying the dep graph -- $simplifygraph- removeSecondaryBeans,+ removeAggregates, removeDecos,- collapseToPrimaryBeans,+ collapseBeans, ) where -import Algebra.Graph.AdjacencyMap (AdjacencyMap)-import Algebra.Graph.AdjacencyMap qualified as Graph-import Algebra.Graph.AdjacencyMap.Algorithm qualified as Graph-import Algebra.Graph.Export.Dot qualified as Dot+import Cauldron.Graph (AdjacencyMap)+import Cauldron.Graph qualified as Graph+import Cauldron.Graph.Algorithm qualified as Graph+import Cauldron.Graph.Export.Dot qualified as Dot import Cauldron.Args+import Cauldron.Args.Internal (Args(..)) import Cauldron.Beans (SomeMonoidTypeRep (..)) import Cauldron.Beans qualified import Control.Exception (Exception (..)) import Control.Monad.Fix import Control.Monad.IO.Class import Data.Bifunctor (first)-import Data.ByteString qualified import Data.Dynamic import Data.Foldable qualified import Data.Function ((&))@@ -175,15 +170,15 @@ import Data.Sequence qualified import Data.Set (Set) import Data.Set qualified as Set-import Data.Text qualified-import Data.Text.Encoding qualified-import Data.Tree import Data.Type.Equality (testEquality) import Data.Typeable import GHC.Exception (CallStack, prettyCallStackLines) import GHC.IsList import GHC.Stack (HasCallStack, callStack, withFrozenCallStack) import Type.Reflection qualified+import Data.String (IsString(..))+import System.IO qualified+import Control.Applicative ((<|>)) -- | 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.@@ -369,9 +364,8 @@ -- ] -- } -- ]--- action <- either throwIO pure $ cook forbidDepCycles cauldron--- beans <- action--- let Just Foo {sayFoo} = taste beans+-- action <- cook @Foo forbidDepCycles cauldron & either throwIO pure +-- Foo {sayFoo} <- action -- sayFoo -- :} -- deco2 init@@ -441,10 +435,10 @@ -- -- (The name is admittedly uninformative; the culinary metaphor was stretched too far.) data Fire m = Fire- { shouldOmitDependency :: (BeanConstructionStep, BeanConstructionStep) -> Bool,+ { shouldEnforceDependency :: (BeanConstructionStep, BeanConstructionStep) -> Bool, followPlanCauldron :: Cauldron m ->- Set TypeRep ->+ BeanGetter -> Beans -> Plan -> m Beans@@ -455,13 +449,29 @@ ConstructorReps {argReps = Set.delete beanRep argReps, regReps, beanRep} -- | Forbid any kind of cyclic dependencies between beans. This is probably what you want.+--+-- >>> :{+-- data A = A+-- loopyA :: A -> A+-- loopyA _ = A+-- :}+--+-- >>> :{+-- cook @A forbidDepCycles ([+-- recipe @A $ val $ wire loopyA+-- ] :: Cauldron IO) +-- & \case Left (DependencyCycleError _) -> "self dep is forbidden"; _ -> "oops"+-- :}+-- "self dep is forbidden" forbidDepCycles :: (Monad m) => Fire m forbidDepCycles = Fire- { shouldOmitDependency = \_ -> False,- followPlanCauldron = \cauldron _secondaryBeanReps initial plan ->+ { shouldEnforceDependency = \_ -> True,+ followPlanCauldron = \cauldron previous initial plan -> do+ let makeBareView _ beans = beansBeanGetter beans <> previous+ let makeDecoView _ beans = beansBeanGetter beans <> previous Data.Foldable.foldlM- do followPlanStep (\_ -> id) (\_ -> id) cauldron mempty+ do followPlanStep makeBareView makeDecoView cauldron initial plan }@@ -477,19 +487,46 @@ -- __BEWARE__: Pattern-matching too eagerly on a \"bean from the future\" during -- construction will cause infinite loops or, if you are lucky, throw -- 'Control.Exception.FixIOException's.+--+--+-- >>> :{+-- data A = A+-- loopyA :: A -> A+-- loopyA _ = A+-- :}+--+-- >>> :{+-- cook @A allowSelfDeps ([+-- recipe @A $ val $ wire loopyA+-- ] :: Cauldron IO) +-- & \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+-- :}+--+-- >>> :{+-- cook @U allowSelfDeps ([+-- recipe @U $ val $ wire loopyU,+-- recipe @V $ val $ wire loopyV+-- ] :: Cauldron IO) +-- & \case Left (DependencyCycleError _) -> "cycle between 2 deps"; _ -> "oops"+-- :}+-- "cycle between 2 deps" allowSelfDeps :: (MonadFix m) => Fire m allowSelfDeps = Fire- { shouldOmitDependency = \case- (BarePrimaryBean bean, PrimaryBean anotherBean) | bean == anotherBean -> True- _ -> False,- followPlanCauldron = \cauldron _secondaryBeanReps initial plan ->- mfix do- \final ->- Data.Foldable.foldlM- do followPlanStep Cauldron.Beans.delete (\_ -> id) cauldron final- initial- plan+ { shouldEnforceDependency = \case+ (BarePrimaryBean bean, FinishedBean anotherBean) | bean == anotherBean -> False+ _ -> True,+ followPlanCauldron = fixyFollowPlanCauldron } -- | Allow /any/ kind of dependency cycles.@@ -502,24 +539,52 @@ -- __BEWARE__: Pattern-matching too eagerly on argument beans during -- construction will cause infinite loops or, if you are lucky, throw -- 'Control.Exception.FixIOException's.+--+-- >>> :{+-- data U = U+-- data V = V+-- loopyU :: V -> U+-- loopyU _ = U+-- loopyV :: U -> V+-- loopyV _ = V+-- :}+--+-- >>> :{+-- cook @U allowDepCycles ([+-- recipe @U $ val $ wire loopyU,+-- recipe @V $ val $ wire loopyV+-- ] :: Cauldron IO) +-- & \case Left (DependencyCycleError _) -> "oops"; _ -> "cycles are ok"+-- :}+-- "cycles are ok" allowDepCycles :: (MonadFix m) => Fire m allowDepCycles = Fire- { shouldOmitDependency = \case- (BarePrimaryBean _, PrimaryBean _) -> True- (PrimaryBeanDeco _ _, PrimaryBean _) -> True- _ -> False,- followPlanCauldron = \cauldron secondaryBeanReps initial plan -> do- let makeBareView _ = (`Cauldron.Beans.restrictKeys` secondaryBeanReps)- let makeDecoView tr = (`Cauldron.Beans.restrictKeys` (Set.insert tr secondaryBeanReps))- mfix do- \final ->- Data.Foldable.foldlM- do followPlanStep makeBareView makeDecoView cauldron final- initial- plan+ { shouldEnforceDependency = \case+ (BarePrimaryBean _, FinishedBean _) -> False+ (PrimaryBeanDeco _ _, FinishedBean _) -> False+ (AggregateBean _ , FinishedBean _) -> False+ _ -> True,+ followPlanCauldron = fixyFollowPlanCauldron } ++fixyFollowPlanCauldron :: MonadFix m => Cauldron m -> BeanGetter -> Beans -> [BeanConstructionStep] -> m Beans+fixyFollowPlanCauldron = \cauldron previous initial plan -> do+ mfix do+ \final -> do+ -- We prefer the final beans.+ let makeBareView _ _ = beansBeanGetter final <> previous+ -- We prefer the final beans,+ -- *except* when the bean being decorated, + -- because the decorator needs the in-construction version.+ let makeDecoView tr beans = (beansBeanGetter beans `restrict` Set.singleton tr) <> beansBeanGetter final <> previous+ Data.Foldable.foldlM+ do followPlanStep makeBareView makeDecoView cauldron+ initial+ plan++ -- https://discord.com/channels/280033776820813825/280036215477239809/1147832555828162594 -- https://github.com/ghc-proposals/ghc-proposals/pull/126#issuecomment-1363403330 @@ -545,57 +610,161 @@ | -- | Apply the decorator with the given index. Comes after the 'BarePrimaryBean' and all 'PrimaryBeanDeco's with a lower index value. PrimaryBeanDeco TypeRep Int | -- | Final, fully decorated version of a bean. If there are no decorators, comes directly after 'BarePrimaryBean'.- PrimaryBean TypeRep+ FinishedBean TypeRep | -- | Beans that are secondary registrations of a 'Constructor' and which are aggregated monoidally.- SecondaryBean TypeRep+ AggregateBean TypeRep deriving stock (Show, Eq, Ord) --- | Build the beans using the recipeMap stored in the 'Cauldron'.+-- | Build the requested @bean@ using the 'Recipe's stored in the 'Cauldron'.+-- The 'Cauldron' must contain a 'Recipe' for the requested bean, as well as+-- 'Recipe's for producing all of its transitive dependencies. ----- Any secondary beans that are registered by constructors are aggregated--- monoidally.+-- >>> :{+-- data A = A deriving Show+-- :}+--+-- >>> :{+-- cook @A forbidDepCycles (mempty :: Cauldron IO) +-- & \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+-- :}+--+-- >>> :{+-- cook @B forbidDepCycles ([recipe $ val $ wire B] :: Cauldron IO) +-- & \case Left (MissingDependenciesError _) -> "no recipe for A"; _ -> "oops"+-- :}+-- "no recipe for A"+-- cook ::- forall m.- (Monad m) =>+ forall {m} bean.+ (Monad m, Typeable bean) =>+ -- | The types of dependency cycles that are allowed between beans. Fire m ->+ -- | A 'Cauldron' containing the necessary 'Recipe's. Cauldron m ->- Either RecipeError (m Beans)-cook fire cauldron =- fmap @(Either RecipeError) (fmap @m rootLabel) $- cookTree (Node (fire, cauldron) [])+ Either CookingError (m bean)+cook fire cauldron = do+ (mdeps, c) <- nest' fire cauldron+ _ <- case mdeps of+ [] -> Right ()+ d : _ -> Left $ MissingDependenciesError d+ Right $ do+ (_, bean) <- runConstructor (mempty @BeanGetter) c+ pure bean --- | Cook a nonempty list of 'Cauldron's.+-- | +-- +-- Takes a 'Cauldron' 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. ----- 'Cauldron's later in the list can see the beans in all previous 'Cauldron's,--- but not vice versa.+-- This function never fails with 'MissingDependenciesError'. ----- Beans in a 'Cauldron' have priority over the same beans in previous 'Cauldron's.-cookNonEmpty ::- forall m.- (Monad m) =>- NonEmpty (Fire m, Cauldron m) ->- Either RecipeError (m (NonEmpty Beans))-cookNonEmpty nonemptyCauldronList = do- fmap @(Either RecipeError) (fmap @m unsafeTreeToNonEmpty) $- cookTree (nonEmptyToTree nonemptyCauldronList)---- | Cook a hierarchy of 'Cauldron's.+-- This is an advanced function for when you want limited scopes for some beans.+-- Usually 'cook' is enough. ----- 'Cauldron's down in the branches can see the beans of their ancestor--- 'Cauldron's, but not vice versa.+-- Consider these example definitions: ----- Beans in a 'Cauldron' have priority over the same beans in ancestor 'Cauldron's.-cookTree ::- forall m.- (Monad m) =>- Tree (Fire m, Cauldron m) ->- Either RecipeError (m (Tree Beans))-cookTree (treecipes) = do- accumMap <- first DoubleDutyBeansError do checkNoDoubleDutyBeans (snd <$> treecipes)- () <- first MissingDependenciesError do checkMissingDeps (Map.keysSet accumMap) (snd <$> treecipes)- treeplan <- first DependencyCycleError do buildPlans (Map.keysSet accumMap) treecipes- Right $ followPlan (fromDynList (Data.Foldable.toList accumMap)) (treeplan)+-- >>> :{+-- 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 [+-- 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 [+-- 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 [+-- 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+nest ::+ forall {m} bean.+ (Monad m, Typeable bean, HasCallStack) =>+ -- | The types of dependency cycles that are allowed between beans.+ Fire m ->+ -- | A 'Cauldron', possibly with unfilled dependencies.+ Cauldron m ->+ Either CookingError (Constructor m bean)+nest fire cauldron = withFrozenCallStack do+ (_, c) <- nest' fire cauldron+ pure c +nest' ::+ forall {m} bean.+ (Monad m, Typeable bean, HasCallStack) =>+ Fire m ->+ Cauldron m ->+ Either CookingError ([MissingDependencies], Constructor m bean)+nest' Fire {shouldEnforceDependency, followPlanCauldron} cauldron = withFrozenCallStack do+ accumMap <- first DoubleDutyBeansError do checkNoDoubleDutyBeans cauldron+ () <- first MissingResultBeanError do checkEntryPointPresent (typeRep (Proxy @bean)) (Map.keysSet accumMap) cauldron+ plan <- first DependencyCycleError do buildPlan shouldEnforceDependency cauldron+ let missingDeps = collectMissingDeps (Map.keysSet accumMap) (Cauldron.keysSet cauldron) cauldron+ Right $ (missingDeps, Constructor+ {+ _constructorCallStack = callStack,+ _args = Args {+ _argReps = missingDepsToArgReps missingDeps,+ _regReps = Set.empty,+ _runArgs = \previous -> do+ beans <- followPlanCauldron cauldron (BeanGetter previous) (fromDynList (Data.Foldable.toList accumMap)) plan+ pure $ pure $ fromJust $ taste @bean beans+ }+ })++checkEntryPointPresent :: TypeRep -> Set TypeRep -> Cauldron m -> Either TypeRep ()+checkEntryPointPresent tr secondary cauldron = + if Set.member tr (Cauldron.keysSet cauldron `Set.union` secondary)+ then Right ()+ else Left tr++ newtype DoubleDutyBeans = DoubleDutyBeans (Map TypeRep (CallStack, CallStack)) deriving stock (Show) @@ -603,23 +772,19 @@ -- be obtained even if the 'Cauldron' can't be 'cook'ed successfully. getDependencyGraph :: Cauldron m -> DependencyGraph getDependencyGraph cauldron =- let (accumMap, _) = cauldronRegs cauldron- (_, deps) = buildDepsCauldron (Map.keysSet accumMap) cauldron+ let (_, deps) = buildDepsCauldron cauldron in DependencyGraph {graph = Graph.edges deps} checkNoDoubleDutyBeans ::- Tree (Cauldron m) ->+ Cauldron m -> Either DoubleDutyBeans (Map TypeRep Dynamic)-checkNoDoubleDutyBeans treecipes = do- let (accumMap, beanSet) = cauldronTreeRegs treecipes+checkNoDoubleDutyBeans cauldron = do+ let (accumMap, beanSet) = cauldronRegs cauldron let common = Map.intersectionWith (,) (fst <$> accumMap) beanSet if not (Map.null common) then Left $ DoubleDutyBeans common else Right $ snd <$> accumMap -cauldronTreeRegs :: Tree (Cauldron m) -> (Map TypeRep (CallStack, Dynamic), Map TypeRep CallStack)-cauldronTreeRegs = foldMap cauldronRegs- cauldronRegs :: Cauldron m -> (Map TypeRep (CallStack, Dynamic), Map TypeRep CallStack) cauldronRegs Cauldron {recipeMap} = Map.foldMapWithKey@@ -636,44 +801,24 @@ data MissingDependencies = MissingDependencies CallStack TypeRep (Set TypeRep) deriving stock (Show) -checkMissingDeps ::- -- | accums- Set TypeRep ->- Tree (Cauldron m) ->- Either MissingDependencies ()-checkMissingDeps accums treecipes = do- let decoratedTreecipes = decorate (Map.empty, treecipes)- missing =- decoratedTreecipes <&> \(available, requested) ->- do checkMissingDepsCauldron accums (Map.keysSet available) requested- sequence_ missing- where- decorate ::- (Map TypeRep (SomeRecipe m), Tree (Cauldron m)) ->- Tree (Map TypeRep (SomeRecipe m), Cauldron m)- decorate = unfoldTree- do- \(acc, Node (current@Cauldron {recipeMap}) rest) ->- let -- current level has priority- newAcc = recipeMap `Map.union` acc- newSeeds = do- z <- rest- [(newAcc, z)]- in ((newAcc, current), newSeeds)+missingDepsToArgReps ::+ [MissingDependencies] ->+ Set TypeRep+missingDepsToArgReps = Set.unions . fmap (\(MissingDependencies _ _ missing) -> missing) -checkMissingDepsCauldron ::+collectMissingDeps :: -- | accums Set TypeRep -> -- | available at this level Set TypeRep -> Cauldron m ->- Either MissingDependencies ()-checkMissingDepsCauldron accums available cauldron =- Data.Foldable.for_ (demandsByConstructorsInCauldron cauldron) \(stack, tr, demanded) ->+ [MissingDependencies]+collectMissingDeps accums available cauldron =+ demandsByConstructorsInCauldron cauldron & Data.Foldable.foldMap \(stack, tr, demanded) -> let missing = Set.filter (`Set.notMember` (available `Set.union` accums)) demanded in if Set.null missing- then Right ()- else Left $ MissingDependencies stack tr missing+ then []+ else [MissingDependencies stack tr missing] demandsByConstructorsInCauldron :: Cauldron m -> [(CallStack, TypeRep, Set TypeRep)] demandsByConstructorsInCauldron Cauldron {recipeMap} = do@@ -689,26 +834,20 @@ newtype DependencyCycle = DependencyCycle (NonEmpty (BeanConstructionStep, Maybe CallStack)) deriving stock (Show) -buildPlans :: Set TypeRep -> Tree (Fire m, Cauldron m) -> Either DependencyCycle (Tree (Plan, Fire m, Cauldron m))-buildPlans secondary = traverse \(fire@Fire {shouldOmitDependency}, cauldron) -> do- let (locations, deps) = buildDepsCauldron secondary cauldron++buildPlan :: ((BeanConstructionStep, BeanConstructionStep) -> Bool) -> Cauldron m -> Either DependencyCycle Plan+buildPlan shouldEnforceDependency cauldron = do+ let (locations, deps) = buildDepsCauldron cauldron -- We may omit some dependency edges to allow for cyclic dependencies.- let graph = Graph.edges $ filter (not . shouldOmitDependency) deps- case Graph.topSort graph of+ let graph = Graph.edges $ filter shouldEnforceDependency deps+ case Graph.reverseTopSort graph of Left recipeCycle -> Left $ DependencyCycle $ recipeCycle <&> \step -> (step, Map.lookup step locations)- Right (reverse -> plan) -> do- Right (plan, fire, cauldron)+ Right plan -> do+ Right plan -buildDepsCauldron :: Set TypeRep -> Cauldron m -> (Map BeanConstructionStep CallStack, [(BeanConstructionStep, BeanConstructionStep)])-buildDepsCauldron secondary Cauldron {recipeMap} = do- -- Are we depending on a primary bean, or on a monoidally aggregated secondary bean?- -- I wonder if we could make this more uniform, it's kind of annoying to have to make this decision here...- let makeTargetStep :: TypeRep -> BeanConstructionStep- makeTargetStep rep =- if rep `Set.member` secondary- then SecondaryBean rep- else PrimaryBean rep+buildDepsCauldron :: Cauldron m -> (Map BeanConstructionStep CallStack, [(BeanConstructionStep, BeanConstructionStep)])+buildDepsCauldron Cauldron {recipeMap} = do recipeMap & Map.foldMapWithKey \beanRep@@ -722,25 +861,25 @@ } -> do let bareBean = BarePrimaryBean beanRep- boiledBean = PrimaryBean beanRep+ boiledBean = FinishedBean beanRep decoSteps = do (decoIndex, decoCon) <- zip [0 :: Int ..] (Data.Foldable.toList decos) [(PrimaryBeanDeco beanRep decoIndex, decoCon)] beanDeps = do- constructorEdges makeTargetStep bareBean (constructorReps bean)+ constructorEdges bareBean (constructorReps bean) decoDeps = do (decoStep, decoCon) <- decoSteps -- We remove the bean because from the args becase, in the -- case of decos, we want to depend on the in-the-making -- version of the bean, not the completed bean.- constructorEdges makeTargetStep decoStep (removeBeanFromArgs do constructorReps decoCon)+ constructorEdges decoStep (removeBeanFromArgs do constructorReps decoCon) innerSteps = bareBean Data.List.NonEmpty.:| (fst <$> decoSteps) ++ [boiledBean] innerDeps = -- This explicit dependency between the completed bean and its -- "bare" undecorated form is not strictly required. It will -- always exist in an indirect manner, through the decorators. -- But it might be useful when rendering the dep graph.- (PrimaryBean beanRep, BarePrimaryBean beanRep)+ (FinishedBean beanRep, BarePrimaryBean beanRep) : -- The dep chain of completed bean -> decorators -> bare bean. zip (Data.List.NonEmpty.tail innerSteps) (Data.List.NonEmpty.toList innerSteps)@@ -755,72 +894,83 @@ ) constructorEdges ::- (TypeRep -> BeanConstructionStep) -> BeanConstructionStep -> ConstructorReps -> [(BeanConstructionStep, BeanConstructionStep)]-constructorEdges makeTargetStep item (ConstructorReps {argReps, regReps}) =+constructorEdges item (ConstructorReps {argReps, regReps}) = -- consumers depend on their args ( do argRep <- Set.toList argReps- let argStep = makeTargetStep argRep+ let argStep = FinishedBean argRep [(item, argStep)] ) ++- -- secondary beans depend on their producers+ ( do (regRep, _) <- Map.toList regReps- let repStep = SecondaryBean regRep- [(repStep, item)]+ let repStep = AggregateBean regRep+ [+ -- aggregate beans depend on their producers + (repStep, item), + -- The finished version of the aggregate bean depends on the aggregation step.+ (FinishedBean regRep, repStep) ] ) -followPlan ::- (Monad m) =>- Beans ->- (Tree (Plan, Fire m, Cauldron m)) ->- m (Tree Beans)-followPlan initialBeans treecipes =- let secondaryBeanReps = Cauldron.Beans.keysSet initialBeans- in unfoldTreeM- ( \(previousStageBeans, Node (plan, Fire {followPlanCauldron}, cauldron) rest) -> do- currentStageBeans <- followPlanCauldron cauldron secondaryBeanReps previousStageBeans plan- pure (currentStageBeans, (,) currentStageBeans <$> rest)- )- (initialBeans, treecipes)+data BeanGetter = BeanGetter { _run :: forall t. (Typeable t) => Maybe t } +instance Semigroup BeanGetter where+ BeanGetter { _run = run1 } <> BeanGetter { _run = run2 } =+ BeanGetter { _run = run1 <|> run2 }++instance Monoid BeanGetter where+ mempty = BeanGetter { _run = Nothing }++runBeanGetter :: BeanGetter -> forall t. (Typeable t) => Maybe t +runBeanGetter BeanGetter { _run } = _run++beansBeanGetter :: Beans -> BeanGetter+beansBeanGetter beans = BeanGetter (taste beans) ++restrict :: BeanGetter -> Set TypeRep -> BeanGetter+restrict (BeanGetter { _run }) allowed =+ BeanGetter { _run = _run' }+ where+ _run' :: forall bean. (Typeable bean) => Maybe bean+ _run' =+ let tr = typeRep (Proxy @bean)+ in if tr `Set.member` allowed+ then _run+ else Nothing+++-- | Builds the transition function for a 'foldM'. followPlanStep :: (Monad m) =>- (TypeRep -> Beans -> Beans) ->- (TypeRep -> Beans -> Beans) ->+ (TypeRep -> Beans -> BeanGetter) ->+ (TypeRep -> Beans -> BeanGetter) -> Cauldron m -> Beans ->- Beans -> BeanConstructionStep -> m Beans-followPlanStep makeBareView makeDecoView Cauldron {recipeMap} final super item =+followPlanStep makeBareView makeDecoView Cauldron {recipeMap} super item = case item of BarePrimaryBean rep -> case fromJust do Map.lookup rep recipeMap of SomeRecipe {_recipe = Recipe {bean}} -> do let ConstructorReps {beanRep} = constructorReps bean- -- We delete the beanRep before running the bean,- -- because if we have a self-dependency, we don't want to use the bean- -- from a previous context (if it exists) we want the bean from final.- -- There is a test for this.- inserter <- followConstructor bean final (makeBareView beanRep super)+ inserter <- followConstructor bean (makeBareView beanRep super) pure do inserter super PrimaryBeanDeco rep index -> case fromJust do Map.lookup rep recipeMap of SomeRecipe {_recipe = Recipe {decos}} -> do let deco = decos `Data.Sequence.index` index let ConstructorReps {beanRep} = constructorReps deco- -- Unlike before, we don't delete the beanRep before running the constructor.- inserter <- followConstructor deco final (makeDecoView beanRep super)+ inserter <- followConstructor deco (makeDecoView beanRep super) pure do inserter super- -- \| We do nothing here, the work has been done in previous 'BarePrimaryBean' and- -- 'PrimaryBeanDeco' steps.- PrimaryBean {} -> pure super -- \| We do nothing here, secondary beans are built as a byproduct -- of primary beans and decorators.- SecondaryBean {} -> pure super+ AggregateBean {} -> pure super+ -- \| We do nothing here, the work has been done in previous 'BarePrimaryBean' and+ -- 'PrimaryBeanDeco' steps.+ FinishedBean {} -> pure super -- | Build a bean out of already built beans. -- This can only work without blowing up if there aren't dependecy cycles@@ -828,18 +978,22 @@ followConstructor :: (Monad m, Typeable bean) => Constructor m bean ->- Beans ->- Beans ->+ BeanGetter -> m (Beans -> Beans)-followConstructor c final super = do- (regs, bean) <- runConstructor [super, final] c+followConstructor c getter = do+ -- (regs, bean) <- runConstructor [super, final] c+ (regs, bean) <- runConstructor getter c pure \bs -> Cauldron.Beans.unionBeansMonoidally (getRegsReps (getConstructorArgs c)) bs regs & Cauldron.Beans.insert bean -- | Sometimes the 'cook'ing process goes wrong.-data RecipeError- = -- | A 'Constructor' depends on beans that can't be found either in the current 'Cauldron' or its ancestors.+data CookingError+ + = -- | The bean that was demanded from the 'Cauldron' doesn't have a 'Recipe' that produces it.+ MissingResultBeanError TypeRep + | + -- | A 'Constructor' depends on beans that can't be found in the 'Cauldron'. MissingDependenciesError MissingDependencies | -- | Beans that work both as primary beans and as secondary beans -- are disallowed.@@ -848,14 +1002,16 @@ DependencyCycleError DependencyCycle deriving stock (Show) -instance Exception RecipeError where- displayException = prettyRecipeError+instance Exception CookingError where+ displayException = prettyCookingError -prettyRecipeError :: RecipeError -> String-prettyRecipeError = Data.List.intercalate "\n" . prettyRecipeErrorLines+prettyCookingError :: CookingError -> String+prettyCookingError = Data.List.intercalate "\n" . prettyCookingErrorLines -prettyRecipeErrorLines :: RecipeError -> [String]-prettyRecipeErrorLines = \case+prettyCookingErrorLines :: CookingError -> [String]+prettyCookingErrorLines = \case+ MissingResultBeanError tr ->+ ["No recipe found that produces requested bean " ++ show tr] MissingDependenciesError (MissingDependencies constructorCallStack constructorResultRep missingDependenciesReps) -> [ "This constructor for a value of type "@@ -886,8 +1042,8 @@ [ "- " ++ case step of BarePrimaryBean rep -> "Bare bean " ++ show rep PrimaryBeanDeco rep i -> "Decorator " ++ show i ++ " for bean " ++ show rep- PrimaryBean rep -> "Complete bean " ++ show rep- SecondaryBean rep -> "Secondary bean " ++ show rep+ FinishedBean rep -> "Complete bean " ++ show rep+ AggregateBean rep -> "Secondary bean " ++ show rep ] ++ case mstack of Nothing -> []@@ -908,9 +1064,9 @@ toAdjacencyMap DependencyGraph {graph} = graph -- | Remove all vertices and edges related to secondary beans.-removeSecondaryBeans :: DependencyGraph -> DependencyGraph-removeSecondaryBeans DependencyGraph {graph} =- DependencyGraph {graph = Graph.induce (\case SecondaryBean {} -> False; _ -> True) graph}+removeAggregates :: DependencyGraph -> DependencyGraph+removeAggregates DependencyGraph {graph} =+ DependencyGraph {graph = Graph.induce (\case AggregateBean {} -> False; _ -> True) graph} -- | Remove all vertices and edges related to bean decorators. removeDecos :: DependencyGraph -> DependencyGraph@@ -920,105 +1076,105 @@ -- | Unifies 'PrimaryBean's with their respective 'BarePrimaryBean's and 'PrimaryBeanDeco's. -- -- Also removes any self-loops.-collapseToPrimaryBeans :: DependencyGraph -> DependencyGraph-collapseToPrimaryBeans DependencyGraph {graph} = do+collapseBeans :: DependencyGraph -> DependencyGraph+collapseBeans DependencyGraph {graph} = do let simplified = Graph.gmap ( \case- BarePrimaryBean rep -> PrimaryBean rep- PrimaryBeanDeco rep _ -> PrimaryBean rep- other -> other+ BarePrimaryBean rep -> FinishedBean rep+ PrimaryBeanDeco rep _ -> FinishedBean rep+ AggregateBean rep -> FinishedBean rep+ FinishedBean rep -> FinishedBean rep ) graph -- Is there a simpler way to removoe self-loops? vertices = Graph.vertexList simplified edges = Graph.edgeList simplified edgesWithoutSelfLoops =+ edges & filter ( \case- (PrimaryBean source, PrimaryBean target) -> if source == target then False else True+ (FinishedBean source, FinishedBean target) -> if source == target then False else True _ -> True )- edges DependencyGraph {graph = Graph.vertices vertices `Graph.overlay` Graph.edges edgesWithoutSelfLoops} -- | See the [DOT format](https://graphviz.org/doc/info/lang.html).-writeAsDot :: Dot.Style BeanConstructionStep Data.Text.Text -> FilePath -> DependencyGraph -> IO ()+writeAsDot :: Dot.Style BeanConstructionStep String -> FilePath -> DependencyGraph -> IO () writeAsDot style filepath DependencyGraph {graph} = do let dot = Dot.export style graph- Data.ByteString.writeFile filepath (Data.Text.Encoding.encodeUtf8 dot)+ System.IO.withFile filepath System.IO.WriteMode $ \handle -> do+ System.IO.hSetEncoding handle System.IO.utf8 + System.IO.hPutStrLn handle dot -- | Default DOT rendering style to use with 'writeAsDot'.--- When a 'RecipeError' exists, is highlights the problematic 'BeanConstructionStep's.-defaultStyle :: Maybe RecipeError -> Dot.Style BeanConstructionStep Data.Text.Text+-- When a 'CookingError' exists, is highlights the problematic 'BeanConstructionStep's.+defaultStyle :: (Monoid s, IsString s) => Maybe CookingError -> Dot.Style BeanConstructionStep s defaultStyle merr = -- https://graphviz.org/docs/attr-types/style/ -- https://hackage.haskell.org/package/algebraic-graphs-0.7/docs/Algebra-Graph-Export-Dot.html (Dot.defaultStyle defaultStepToText) { Dot.vertexAttributes = \step -> case merr of Nothing -> []+ Just (MissingResultBeanError _) ->+ [] Just (MissingDependenciesError (MissingDependencies _ _ missing)) -> case step of- PrimaryBean rep+ FinishedBean rep | Set.member rep missing ->- [ Data.Text.pack "style" Dot.:= Data.Text.pack "dashed",- Data.Text.pack "color" Dot.:= Data.Text.pack "red"+ [ fromString "style" Dot.:= fromString "dashed",+ fromString "color" Dot.:= fromString "red" ] _ -> [] Just (DoubleDutyBeansError (DoubleDutyBeans (Map.keysSet -> bs))) -> case step of- PrimaryBean rep+ FinishedBean rep | Set.member rep bs ->- [ Data.Text.pack "style" Dot.:= Data.Text.pack "bold",- Data.Text.pack "color" Dot.:= Data.Text.pack "green"+ [ fromString "style" Dot.:= fromString "bold",+ fromString "color" Dot.:= fromString "green" ]- SecondaryBean rep+ AggregateBean rep | Set.member rep bs ->- [ Data.Text.pack "style" Dot.:= Data.Text.pack "bold",- Data.Text.pack "color" Dot.:= Data.Text.pack "green"+ [ fromString "style" Dot.:= fromString "bold",+ fromString "color" Dot.:= fromString "green" ] _ -> [] Just (DependencyCycleError (DependencyCycle (Set.fromList . Data.Foldable.toList . fmap fst -> cycleStepSet))) -> if Set.member step cycleStepSet then- [ Data.Text.pack "style" Dot.:= Data.Text.pack "bold",- Data.Text.pack "color" Dot.:= Data.Text.pack "blue"+ [ fromString "style" Dot.:= fromString "bold",+ fromString "color" Dot.:= fromString "blue" ] else [] } -- | Change the default way of how 'BeanConstructionStep's are rendered to text.-setVertexName :: (BeanConstructionStep -> Data.Text.Text) -> Dot.Style BeanConstructionStep Data.Text.Text -> Dot.Style BeanConstructionStep Data.Text.Text+setVertexName :: IsString s => (BeanConstructionStep -> s) -> Dot.Style BeanConstructionStep s -> Dot.Style BeanConstructionStep s setVertexName vertexName style = style {Dot.vertexName} -defaultStepToText :: BeanConstructionStep -> Data.Text.Text+defaultStepToText :: IsString s => BeanConstructionStep -> s defaultStepToText =- let p rep = Data.Text.pack do show rep+ let p rep = show rep in \case- BarePrimaryBean rep -> p rep <> Data.Text.pack "#bare"- PrimaryBeanDeco rep index -> p rep <> Data.Text.pack ("#deco#" ++ show index)- PrimaryBean rep -> p rep- SecondaryBean rep -> p rep <> Data.Text.pack "#agg"+ BarePrimaryBean rep -> fromString $ p rep ++ "#bare"+ PrimaryBeanDeco rep index -> fromString $ p rep ++ "#deco#" ++ show index+ AggregateBean rep -> fromString $ p rep ++ "#agg"+ FinishedBean rep -> fromString $ p rep -nonEmptyToTree :: NonEmpty a -> Tree a-nonEmptyToTree = \case- a Data.List.NonEmpty.:| [] -> Node a []- a Data.List.NonEmpty.:| (b : rest) -> Node a [nonEmptyToTree (b Data.List.NonEmpty.:| rest)] -unsafeTreeToNonEmpty :: Tree a -> NonEmpty a-unsafeTreeToNonEmpty = \case- Node a [] -> a Data.List.NonEmpty.:| []- Node a [b] -> Data.List.NonEmpty.cons a (unsafeTreeToNonEmpty b)- _ -> error "tree not list-shaped"- -- | A way of building value of type @bean@, potentially requiring some--- dependencies, potentially returning some secondary beans+-- 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 secondary beans are not.+-- '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 allocate resources with bracket-like operations, and in that@@ -1031,18 +1187,43 @@ -- | Create a 'Constructor' from an 'Args' value that returns a 'bean'. -- -- Usually, the 'Args' value will be created by 'wire'ing a constructor function.+--+-- >>> :{+-- data A = A+-- data B = B+-- makeB :: A -> B+-- makeB _ = B +-- c :: Constructor IO B+-- c = val_ $ wire $ makeB+-- :}+-- val_ :: forall bean m. (Applicative m, HasCallStack) => Args bean -> Constructor m bean val_ x = Constructor callStack $ fmap (pure . pure) x -- | 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 secondary beans (if they have+-- rightmost-innermost one are registered as aggregate beans (if they have -- 'Monoid' instances, otherwise 'val' won't compile).+--+-- >>> :{+-- 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 {nested} bean m. (Registrable nested bean, Applicative m, HasCallStack) => Args nested -> Constructor m bean val x = withFrozenCallStack (val' $ fmap runIdentity $ register $ fmap Identity x) -- | Like 'val', but uses an alternative form of registering secondary beans. -- Less 'Registrable' typeclass magic, but more verbose. Likely not what you want.+-- val' :: forall bean m. (Applicative m, HasCallStack) => Args (Regs bean) -> Constructor m bean val' x = Constructor callStack $ fmap pure x @@ -1050,6 +1231,16 @@ -- effect that produces 'bean'. -- -- Usually, the 'Args' value will be created by 'wire'ing 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+-- :}+-- eff_ :: forall bean m. (Functor m, HasCallStack) => Args (m bean) -> Constructor m bean eff_ x = Constructor callStack $ fmap (fmap pure) x @@ -1059,8 +1250,22 @@ -- | 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 secondary+-- components except the rightmost-innermost one are registered as aggregate -- beans (if they have 'Monoid' instances, otherwise 'eff' won't compile).+--+-- >>> :{+-- 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 {nested} bean m. (Registrable nested bean, Monad m, HasCallStack) => Args (m nested) -> Constructor m bean eff x = withFrozenCallStack (eff' $ register x) @@ -1073,9 +1278,13 @@ eff' :: forall bean m. (HasCallStack) => Args (m (Regs bean)) -> Constructor m bean eff' = Constructor callStack -runConstructor :: (Monad m) => [Beans] -> Constructor m bean -> m (Beans, bean)-runConstructor bss (Constructor {_args}) = do- regs <- _args & runArgs (Data.Foldable.asum (taste <$> bss))+runConstructor :: (Monad m) => + BeanGetter ->+ Constructor m bean -> + m (Beans, bean)+runConstructor getter (Constructor {_args}) = do+ -- regs <- _args & runArgs (Data.Foldable.asum (taste <$> bss))+ regs <- _args & runArgs (runBeanGetter getter) pure (runRegs (getRegsReps _args) regs) -- | Change the monad in which the 'Constructor'\'s effects take place.@@ -1124,32 +1333,32 @@ -- be produced by a single 'Recipe' in the 'Cauldron'. -- -- 'Constructor's can produce, besides their \"primary\" bean result,--- \"secondary\" beans that are not reflected in the 'Constructor' signature.+-- secondary \"aggregate\" beans that are not reflected in the 'Constructor' signature. -- Multiple constructors across different 'Recipe's can produce secondary beans of the -- same type. ----- Secondary beans are a bit special, in that:+-- Aggregate beans are a bit special, in that: ----- * The value that is \"seen"\ by a 'Constructor' that depends on a secondary bean--- is the aggregation of /all/ values produced for that bean in the 'Cauldron'. This--- means that secondary beans must have 'Monoid' instances, to enable aggregation.+-- * The value that is \"seen"\ by a 'Constructor' that depends on an aggregate bean+-- is the aggregation of /all/ values produced for that bean in the 'Cauldron'. Therefore,+-- these beans must have 'Monoid' instances. ----- * When calculating build plan steps for a 'Cauldron', 'Constructor's that depend on a--- secondary bean come after /all/ of the 'Constructor's that produce that secondary bean.+-- * When calculating build plan order for a 'Cauldron', 'Constructor's that depend on a+-- aggregate bean come after /all/ of the 'Constructor's that produce that aggregate bean. ----- * Secondary beans can't be decorated.+-- * Aggregate beans can't be decorated. ----- * A bean type can't be primary and secondary at the same time. See 'DoubleDutyBeansError'.+-- * A bean type can't be primary and aggregate at the same time. See 'DoubleDutyBeansError'. ----- What are secondary beans useful for?+-- 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 secondary beans is to pass an 'Args' value returning a tuple+-- 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 secondary beans:+-- of the tuple other than the rightmost component are considered aggregate beans: -- -- >>> :{ -- con :: Constructor Identity String@@ -1158,7 +1367,7 @@ -- effCon = eff $ pure $ pure @IO (Sum @Int, All False, "foo") -- :} ----- Example of how secondary bean values are accumulated:+-- Example of how aggregate bean values are aggregated: -- -- >>> :{ -- data U = U deriving Show@@ -1167,7 +1376,7 @@ -- makeU = (Sum 1, U) -- makeV :: U -> (Sum Int, V) -- makeV = \_ -> (Sum 7, V)--- newtype W = W (Sum Int) deriving Show -- depends on the secondary bean+-- newtype W = W (Sum Int) deriving Show -- depends on the aggregate bean -- :} -- -- >>> :{@@ -1178,10 +1387,30 @@ -- recipe @V $ val $ wire makeV, -- recipe @W $ val $ wire W -- ]--- Identity beans <- either throwIO pure $ cook forbidDepCycles cauldron--- pure $ taste @W beans+-- Identity w <- cook @W forbidDepCycles cauldron & either throwIO pure+-- pure w -- :}--- Just (W (Sum {getSum = 8}))+-- 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+-- :}+--+-- >>> :{+-- cook @X forbidDepCycles ([+-- recipe @X $ val $ wire makeX,+-- recipe @(Sum Int) $ val $ wire makeAgg+-- ] :: Cauldron IO) +-- & \case Left (DoubleDutyBeansError _) -> "Sum Int is aggregate and primary"; _ -> "oops"+-- :}+-- "Sum Int is aggregate and primary"+-- -- $setup -- >>> :set -XBlockArguments@@ -1190,5 +1419,6 @@ -- >>> import Data.Functor.Identity -- >>> import Data.Function ((&)) -- >>> import Data.Monoid--- >>> import Data.Either (either)+-- >>> import Data.Either (either, isLeft) -- >>> import Control.Exception (throwIO)+
lib/Cauldron/Args.hs view
@@ -19,7 +19,6 @@ runArgs, getArgsReps, contramapArgs,- -- ** Reducing 'arg' boilerplate with 'wire' Wireable (wire), @@ -44,391 +43,5 @@ ) where +import Cauldron.Args.Internal import Cauldron.Beans (Beans, SomeMonoidTypeRep (..), fromDynList, taste)-import Cauldron.Beans qualified-import Control.Exception (Exception, throw)-import Data.Dynamic-import Data.Foldable qualified-import Data.Function ((&))-import Data.Functor ((<&>))-import Data.Kind-import Data.Sequence (Seq)-import Data.Sequence qualified-import Data.Set (Set)-import Data.Set qualified as Set-import Data.Typeable-import Type.Reflection (SomeTypeRep (..))-import Type.Reflection qualified---- | 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.-data Args a = Args- { _argReps :: Set SomeTypeRep,- _regReps :: Set SomeMonoidTypeRep,- _runArgs :: (forall t. (Typeable t) => Maybe t) -> a- }- deriving stock (Functor)---- | 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--- :}-arg :: forall a. (Typeable a) => Args a-arg =- let tr = typeRep (Proxy @a)- in Args- { _argReps = Set.singleton tr,- _regReps = Set.empty,- _runArgs = \f ->- case f @a of- Just v -> v- Nothing -> throw (LazilyReadBeanMissing tr)- }---- | Here the 'Beans' map is not passed /directly/, instead, we pass a--- function-like value that, given a type, will return a value of that type or--- 'Nothing'. Such function is usually constructed using 'taste' on some 'Beans'--- map.------ >>> :{--- let beans = fromDynList [toDyn @Int 5]--- in runArgs (taste beans) (arg @Int)--- :}--- 5------ See also 'LazilyReadBeanMissing'.-runArgs :: (forall b. (Typeable b) => Maybe b) -> Args a -> a-runArgs f (Args _ _ _runArgs) =- -- https://www.reddit.com/r/haskell/comments/16diti/comment/c7vc9ky/- _runArgs f---- | Inspect ahead of time what types will be searched in the 'Beans' map.------ >>> :{--- let beans = fromDynList [toDyn @Int 5, toDyn False]--- args = (,) <$> arg @Int <*> arg @Bool--- in (getArgsReps args, runArgs (taste beans) args)--- :}--- (fromList [Int,Bool],(5,False))-getArgsReps :: Args a -> Set TypeRep-getArgsReps (Args {_argReps}) = _argReps---- | Tweak the look-by-type function that is eventually passed to 'runArgs'.------ Unlikely to be commonly useful.------ >>> :{--- let tweak :: forall t. Typeable t => Maybe t -> Maybe t--- tweak _ = case Type.Reflection.typeRep @t--- `Type.Reflection.eqTypeRep`--- Type.Reflection.typeRep @Int of--- Just HRefl -> Just 5--- Nothing -> Nothing--- in runArgs (taste Cauldron.Beans.empty) $ contramapArgs tweak $ arg @Int--- :}--- 5-contramapArgs :: (forall t. (Typeable t) => Maybe t -> Maybe t) -> Args a -> Args a-contramapArgs tweak args@Args {_runArgs} = args {_runArgs = \f -> _runArgs (tweak f)}---- | Inspect ahead of time the types of registrations that might be contained in--- the result value of an 'Args'.------ >>> :{--- let args = foretellReg @(Sum Int) *> pure ()--- in getRegsReps args--- :}--- fromList [Sum Int]-getRegsReps :: Args a -> Set SomeMonoidTypeRep-getRegsReps (Args {_regReps}) = _regReps---- | This function is used in an 'Args' context to create a tell-like function--- that can later be used to register a value into a 'Regs'.------ The type of the future registration must be an instance of 'Monoid'.------ There are no other ways of registering values into 'Regs'.-foretellReg :: forall a. (Typeable a, Monoid a) => Args (a -> Regs ())-foretellReg =- let tr = SomeMonoidTypeRep (Type.Reflection.typeRep @a)- in Args- { _argReps = Set.empty,- _regReps = Set.singleton tr,- _runArgs = \_ a -> Regs (Data.Sequence.singleton (toDyn a)) ()- }--instance Applicative Args where- pure a =- Args- { _argReps = Set.empty,- _regReps = Set.empty,- _runArgs = \_ -> a- }- Args- { _argReps = _argReps1,- _regReps = _regReps1,- _runArgs = f- }- <*> Args- { _argReps = _argReps2,- _regReps = _regReps2,- _runArgs = a- } =- Args- { _argReps = _argReps1 `Set.union` _argReps2,- _regReps = _regReps1 `Set.union` _regReps2,- _runArgs = \beans -> (f beans) (a beans)- }--someMonoidTypeRepToSomeTypeRep :: SomeMonoidTypeRep -> SomeTypeRep-someMonoidTypeRepToSomeTypeRep (SomeMonoidTypeRep tr) = SomeTypeRep tr---- | A writer-like monad for collecting the values of registrations.-data Regs a = Regs (Seq Dynamic) a- deriving stock (Functor)---- | Extract the 'Beans' map of registrations, along with the main result value.------ The 'Set' of 'SomeMonoidTypeRep's will typically come from 'getRegsReps'.------ Only values for 'TypeRep's present in the set will be returned. There will be--- values for all 'TypeRep's present in the set (some of them might be the--- 'mempty' for that type).-runRegs :: Set SomeMonoidTypeRep -> Regs a -> (Beans, a)-runRegs monoidReps (Regs dyns a) =- -- https://www.reddit.com/r/haskell/comments/16diti/comment/c7vc9ky/- let onlyStaticlyKnown =- ( manyMemptys monoidReps : do- dyn <- Data.Foldable.toList dyns- -- This bit is subtle. I mistakenly used Cauldron.Beans.singleton here- -- and ended up with the Dynamic type as the *key*. It was hell to debug.- [fromDynList [dyn]]- )- & do foldl (Cauldron.Beans.unionBeansMonoidally monoidReps) (mempty @Beans)- & do flip Cauldron.Beans.restrictKeys (Set.map someMonoidTypeRepToSomeTypeRep monoidReps)- in (onlyStaticlyKnown, a)--instance Applicative Regs where- pure a = Regs Data.Sequence.empty a- Regs w1 f <*> Regs w2 a2 =- Regs (w1 Data.Sequence.>< w2) (f a2)--instance Monad Regs where- (Regs w1 a) >>= k =- let Regs w2 r = k a- in Regs (w1 Data.Sequence.>< w2) r--manyMemptys :: Set SomeMonoidTypeRep -> Beans-manyMemptys reps =- reps- & Data.Foldable.toList- <&> Cauldron.Beans.someMonoidTypeRepMempty- & fromDynList---- | Imprecise exception that might lie hidden in the result of 'runArgs', if--- the 'Beans' map lacks a value for some type demanded by the 'Args'.------ Why not make 'runArgs' return a 'Maybe' instead of throwing an imprecise--- exception? The answer is that, for my purposes, using 'Maybe' or 'Either'--- caused undesirable strictness when doing weird things like reading values--- \"from the future\".------ >>> :{--- runArgs (taste Cauldron.Beans.empty) (arg @Int)--- :}--- *** Exception: LazilyReadBeanMissing Int------ If more safety is needed, one can perform additional preliminary checks with--- the help of 'getArgsReps'.-newtype LazilyReadBeanMissing = LazilyReadBeanMissing TypeRep- deriving stock (Show)- deriving anyclass (Exception)---- | Convenience typeclass for wiring all the arguments of a curried function in one go.-class Wireable curried tip | curried -> tip where- -- | 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.- --- -- >>> :{- -- 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- -- :}- wire :: curried -> Args tip--instance (Wireable_ (IsFunction curried) curried tip) => Wireable curried tip where- wire curried = wire_ (Proxy @(IsFunction curried)) do pure curried--class Wireable_ (where_ :: Where) curried tip | where_ curried -> tip where- wire_ :: Proxy where_ -> Args curried -> Args tip--instance Wireable_ AtTheTip a a where- wire_ _ r = r--instance (Typeable b, Wireable_ (IsFunction rest) rest tip) => Wireable_ NotYetThere (b -> rest) tip where- wire_ _ af = wire_ (Proxy @(IsFunction rest)) do af <*> arg @b--type IsFunction :: Type -> Where-type family IsFunction f :: Where where- IsFunction (_ -> _) = 'NotYetThere- IsFunction _ = 'AtTheTip--data Where- = NotYetThere- | AtTheTip--data WhereNested- = Tup2- | Tup3- | Tup4- | Innermost--type IsReg :: Type -> WhereNested-type family IsReg f :: WhereNested where- IsReg (_, _) = 'Tup2- IsReg (_, _, _) = 'Tup3- IsReg (_, _, _, _) = 'Tup4- IsReg _ = 'Innermost---- | Convenience typeclass for automatically extracting registrations from a value.--- Counterpart of 'Wireable' for registrations.-class Registrable nested tip | nested -> tip where- -- | We look for (potentially nested) tuples in the value. All tuple- -- components except the rightmost-innermost must have 'Monoid' instances, and- -- are put into a 'Regs'.- --- -- >>> :{- -- args :: Args (Identity (Sum Int, All, String))- -- args = pure (Identity (Sum 5, All False, "foo"))- -- registeredArgs :: Args (Identity (Regs String))- -- registeredArgs = register args- -- :}- --- -- >>> :{- -- let reps = getRegsReps registeredArgs- -- in ( reps == Data.Set.fromList [ SomeMonoidTypeRep $ Type.Reflection.typeRep @(Sum Int)- -- , SomeMonoidTypeRep $ Type.Reflection.typeRep @All]- -- , registeredArgs & runArgs (taste Cauldron.Beans.empty)- -- & runIdentity- -- & runRegs reps- -- & \(beans,_) -> (taste @(Sum Int) beans, taste @All beans)- -- )- -- :}- -- (True,(Just (Sum {getSum = 5}),Just (All {getAll = False})))- --- -- Tuples can be nested:- --- -- >>> :{- -- args :: Args (Identity (Sum Int, (All, String)))- -- args = pure (Identity (Sum 5, (All False, "foo")))- -- registeredArgs :: Args (Identity (Regs String))- -- registeredArgs = register args- -- :}- --- -- If there are no tuples in the result type, no values are put into 'Regs'.- --- -- >>> :{- -- args :: Args (Identity String)- -- args = pure (Identity "foo")- -- registeredArgs :: Args (Identity (Regs String))- -- registeredArgs = register args- -- :}- register :: forall m. (Functor m) => Args (m nested) -> Args (m (Regs tip))--instance (Registrable_ (IsReg nested) nested tip) => Registrable nested tip where- register amnested = register_ (Proxy @(IsReg nested)) do fmap (fmap pure) amnested--class Registrable_ (where_ :: WhereNested) nested tip | where_ nested -> tip where- register_ :: forall m. (Functor m) => Proxy where_ -> Args (m (Regs nested)) -> Args (m (Regs tip))--instance Registrable_ Innermost a a where- register_ _ = id--instance (Typeable b, Monoid b, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup2 (b, rest) tip where- register_ _ af =- register_ (Proxy @(IsReg rest)) do- tell1 <- foretellReg @b- action <- af- pure (action <&> \regs -> regs >>= \(b, rest) -> tell1 b *> pure rest)--instance (Typeable b, Monoid b, Typeable c, Monoid c, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup3 (b, c, rest) tip where- register_ _ af =- register_ (Proxy @(IsReg rest)) do- tell1 <- foretellReg @b- tell2 <- foretellReg @c- action <- af- pure (action <&> \regs -> regs >>= \(b, c, rest) -> tell1 b *> tell2 c *> pure rest)--instance (Typeable b, Monoid b, Typeable c, Monoid c, Typeable d, Monoid d, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup3 (b, c, d, rest) tip where- register_ _ af =- register_ (Proxy @(IsReg rest)) do- tell1 <- foretellReg @b- tell2 <- foretellReg @c- tell3 <- foretellReg @d- action <- af- pure (action <&> \regs -> regs >>= \(b, c, d, rest) -> tell1 b *> tell2 c *> tell3 d *> pure rest)---- $registrations------ The 'Args' applicative has an additional feature: it lets you \"register\"--- ahead of time the types of some values that /might/ be included in the result--- of the 'Args', but without being reflected in the result type. It's not--- mandatory that these values must be ultimately produced, however.------ Here's an example. We have an 'Args' value that returns a 'Regs'. While--- constructing the 'Args' value, we register the @Sum Int@ and @All@ types--- using 'foretellReg', which also gives us the means of later writing into the--- 'Regs'. By using 'getRegsReps', we can inspect the 'TypeRep's of the types we--- registered without having to run the 'Args',------ >>> :{--- fun2 :: String -> Bool -> Int--- fun2 _ _ = 5--- args :: Args (Regs Int)--- args = do -- Using ApplicativeDo--- r <- fun2 <$> arg <*> arg -- could also have used 'wire'--- tell1 <- foretellReg @(Sum Int)--- tell2 <- foretellReg @All--- pure $ do--- tell1 (Sum 11)--- tell2 (All False)--- pure r--- :}------ >>> :{--- let reps = getRegsReps args--- in ( reps == Data.Set.fromList [ SomeMonoidTypeRep $ Type.Reflection.typeRep @(Sum Int)--- , SomeMonoidTypeRep $ Type.Reflection.typeRep @All]--- , args & runArgs (taste $ fromDynList [toDyn @String "foo", toDyn False])--- & runRegs reps--- & \(beans,_) -> (taste @(Sum Int) beans, taste @All beans)--- )--- :}--- (True,(Just (Sum {getSum = 11}),Just (All {getAll = False})))---- $setup--- >>> :set -XBlockArguments--- >>> :set -XOverloadedLists--- >>> :set -XApplicativeDo--- >>> :set -XGADTs--- >>> :set -Wno-incomplete-uni-patterns--- >>> import Data.Functor.Identity--- >>> import Data.Function ((&))--- >>> import Data.Monoid
+ lib/Cauldron/Args/Internal.hs view
@@ -0,0 +1,407 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE BlockArguments #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE NoFieldSelectors #-}++module Cauldron.Args.Internal+ +where++import Cauldron.Beans (Beans, SomeMonoidTypeRep (..), fromDynList)+import Cauldron.Beans qualified+import Control.Exception (Exception, throw)+import Data.Dynamic+import Data.Foldable qualified+import Data.Function ((&))+import Data.Functor ((<&>))+import Data.Kind+import Data.Sequence (Seq)+import Data.Sequence qualified+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Typeable+import Type.Reflection (SomeTypeRep (..))+import Type.Reflection qualified++-- | 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.+data Args a = Args+ { _argReps :: Set SomeTypeRep,+ _regReps :: Set SomeMonoidTypeRep,+ _runArgs :: (forall t. (Typeable t) => Maybe t) -> a+ }+ deriving stock (Functor)++-- | 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+-- :}+arg :: forall a. (Typeable a) => Args a+arg =+ let tr = typeRep (Proxy @a)+ in Args+ { _argReps = Set.singleton tr,+ _regReps = Set.empty,+ _runArgs = \f ->+ case f @a of+ Just v -> v+ Nothing -> throw (LazilyReadBeanMissing tr)+ }++-- | Here the 'Beans' map is not passed /directly/, instead, we pass a+-- function-like value that, given a type, will return a value of that type or+-- 'Nothing'. Such function is usually constructed using 'taste' on some 'Beans'+-- map.+--+-- >>> :{+-- let beans = fromDynList [toDyn @Int 5]+-- in runArgs (taste beans) (arg @Int)+-- :}+-- 5+--+-- See also 'LazilyReadBeanMissing'.+runArgs :: (forall b. (Typeable b) => Maybe b) -> Args a -> a+runArgs f (Args _ _ _runArgs) =+ -- https://www.reddit.com/r/haskell/comments/16diti/comment/c7vc9ky/+ _runArgs f++-- | Inspect ahead of time what types will be searched in the 'Beans' map.+--+-- >>> :{+-- let beans = fromDynList [toDyn @Int 5, toDyn False]+-- args = (,) <$> arg @Int <*> arg @Bool+-- in (getArgsReps args, runArgs (taste beans) args)+-- :}+-- (fromList [Int,Bool],(5,False))+getArgsReps :: Args a -> Set TypeRep+getArgsReps (Args {_argReps}) = _argReps++-- | Tweak the look-by-type function that is eventually passed to 'runArgs'.+--+-- Unlikely to be commonly useful.+--+-- >>> :{+-- let tweak :: forall t. Typeable t => Maybe t -> Maybe t+-- tweak _ = case Type.Reflection.typeRep @t+-- `Type.Reflection.eqTypeRep`+-- Type.Reflection.typeRep @Int of+-- Just HRefl -> Just 5+-- Nothing -> Nothing+-- in runArgs (taste Cauldron.Beans.empty) $ contramapArgs tweak $ arg @Int+-- :}+-- 5+contramapArgs :: (forall t. (Typeable t) => Maybe t -> Maybe t) -> Args a -> Args a+contramapArgs tweak args@Args {_runArgs} = args {_runArgs = \f -> _runArgs (tweak f)}++-- | Inspect ahead of time the types of registrations that might be contained in+-- the result value of an 'Args'.+--+-- >>> :{+-- let args = foretellReg @(Sum Int) *> pure ()+-- in getRegsReps args+-- :}+-- fromList [Sum Int]+getRegsReps :: Args a -> Set SomeMonoidTypeRep+getRegsReps (Args {_regReps}) = _regReps++-- | This function is used in an 'Args' context to create a tell-like function+-- that can later be used to register a value into a 'Regs'.+--+-- The type of the future registration must be an instance of 'Monoid'.+--+-- There are no other ways of registering values into 'Regs'.+foretellReg :: forall a. (Typeable a, Monoid a) => Args (a -> Regs ())+foretellReg =+ let tr = SomeMonoidTypeRep (Type.Reflection.typeRep @a)+ in Args+ { _argReps = Set.empty,+ _regReps = Set.singleton tr,+ _runArgs = \_ a -> Regs (Data.Sequence.singleton (toDyn a)) ()+ }++instance Applicative Args where+ pure a =+ Args+ { _argReps = Set.empty,+ _regReps = Set.empty,+ _runArgs = \_ -> a+ }+ Args+ { _argReps = _argReps1,+ _regReps = _regReps1,+ _runArgs = f+ }+ <*> Args+ { _argReps = _argReps2,+ _regReps = _regReps2,+ _runArgs = a+ } =+ Args+ { _argReps = _argReps1 `Set.union` _argReps2,+ _regReps = _regReps1 `Set.union` _regReps2,+ _runArgs = \beans -> (f beans) (a beans)+ }++someMonoidTypeRepToSomeTypeRep :: SomeMonoidTypeRep -> SomeTypeRep+someMonoidTypeRepToSomeTypeRep (SomeMonoidTypeRep tr) = SomeTypeRep tr++-- | A writer-like monad for collecting the values of registrations.+data Regs a = Regs (Seq Dynamic) a+ deriving stock (Functor)++-- | Extract the 'Beans' map of registrations, along with the main result value.+--+-- The 'Set' of 'SomeMonoidTypeRep's will typically come from 'getRegsReps'.+--+-- Only values for 'TypeRep's present in the set will be returned. There will be+-- values for all 'TypeRep's present in the set (some of them might be the+-- 'mempty' for that type).+runRegs :: Set SomeMonoidTypeRep -> Regs a -> (Beans, a)+runRegs monoidReps (Regs dyns a) =+ -- https://www.reddit.com/r/haskell/comments/16diti/comment/c7vc9ky/+ let onlyStaticlyKnown =+ ( manyMemptys monoidReps : do+ dyn <- Data.Foldable.toList dyns+ -- This bit is subtle. I mistakenly used Cauldron.Beans.singleton here+ -- and ended up with the Dynamic type as the *key*. It was hell to debug.+ [fromDynList [dyn]]+ )+ & do foldl (Cauldron.Beans.unionBeansMonoidally monoidReps) (mempty @Beans)+ & do flip Cauldron.Beans.restrictKeys (Set.map someMonoidTypeRepToSomeTypeRep monoidReps)+ in (onlyStaticlyKnown, a)++instance Applicative Regs where+ pure a = Regs Data.Sequence.empty a+ Regs w1 f <*> Regs w2 a2 =+ Regs (w1 Data.Sequence.>< w2) (f a2)++instance Monad Regs where+ (Regs w1 a) >>= k =+ let Regs w2 r = k a+ in Regs (w1 Data.Sequence.>< w2) r++manyMemptys :: Set SomeMonoidTypeRep -> Beans+manyMemptys reps =+ reps+ & Data.Foldable.toList+ <&> Cauldron.Beans.someMonoidTypeRepMempty+ & fromDynList++-- | Imprecise exception that might lie hidden in the result of 'runArgs', if+-- the 'Beans' map lacks a value for some type demanded by the 'Args'.+--+-- Why not make 'runArgs' return a 'Maybe' instead of throwing an imprecise+-- exception? The answer is that, for my purposes, using 'Maybe' or 'Either'+-- caused undesirable strictness when doing weird things like reading values+-- \"from the future\".+--+-- >>> :{+-- runArgs (taste Cauldron.Beans.empty) (arg @Int)+-- :}+-- *** Exception: LazilyReadBeanMissing Int+--+-- If more safety is needed, one can perform additional preliminary checks with+-- the help of 'getArgsReps'.+newtype LazilyReadBeanMissing = LazilyReadBeanMissing TypeRep+ deriving stock (Show)+ deriving anyclass (Exception)++-- | Convenience typeclass for wiring all the arguments of a curried function in one go.+class Wireable curried tip | curried -> tip where+ -- | 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.+ --+ -- >>> :{+ -- 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+ -- :}+ wire :: curried -> Args tip++instance (Wireable_ (IsFunction curried) curried tip) => Wireable curried tip where+ wire curried = wire_ (Proxy @(IsFunction curried)) do pure curried++class Wireable_ (where_ :: Where) curried tip | where_ curried -> tip where+ wire_ :: Proxy where_ -> Args curried -> Args tip++instance Wireable_ AtTheTip a a where+ wire_ _ r = r++instance (Typeable b, Wireable_ (IsFunction rest) rest tip) => Wireable_ NotYetThere (b -> rest) tip where+ wire_ _ af = wire_ (Proxy @(IsFunction rest)) do af <*> arg @b++type IsFunction :: Type -> Where+type family IsFunction f :: Where where+ IsFunction (_ -> _) = 'NotYetThere+ IsFunction _ = 'AtTheTip++data Where+ = NotYetThere+ | AtTheTip++data WhereNested+ = Tup2+ | Tup3+ | Tup4+ | Innermost++type IsReg :: Type -> WhereNested+type family IsReg f :: WhereNested where+ IsReg (_, _) = 'Tup2+ IsReg (_, _, _) = 'Tup3+ IsReg (_, _, _, _) = 'Tup4+ IsReg _ = 'Innermost++-- | Convenience typeclass for automatically extracting registrations from a value.+-- Counterpart of 'Wireable' for registrations.+class Registrable nested tip | nested -> tip where+ -- | We look for (potentially nested) tuples in the value. All tuple+ -- components except the rightmost-innermost must have 'Monoid' instances, and+ -- are put into a 'Regs'.+ --+ -- >>> :{+ -- args :: Args (Identity (Sum Int, All, String))+ -- args = pure (Identity (Sum 5, All False, "foo"))+ -- registeredArgs :: Args (Identity (Regs String))+ -- registeredArgs = register args+ -- :}+ --+ -- >>> :{+ -- let reps = getRegsReps registeredArgs+ -- in ( reps == Data.Set.fromList [ SomeMonoidTypeRep $ Type.Reflection.typeRep @(Sum Int)+ -- , SomeMonoidTypeRep $ Type.Reflection.typeRep @All]+ -- , registeredArgs & runArgs (taste Cauldron.Beans.empty)+ -- & runIdentity+ -- & runRegs reps+ -- & \(beans,_) -> (taste @(Sum Int) beans, taste @All beans)+ -- )+ -- :}+ -- (True,(Just (Sum {getSum = 5}),Just (All {getAll = False})))+ --+ -- Tuples can be nested:+ --+ -- >>> :{+ -- args :: Args (Identity (Sum Int, (All, String)))+ -- args = pure (Identity (Sum 5, (All False, "foo")))+ -- registeredArgs :: Args (Identity (Regs String))+ -- registeredArgs = register args+ -- :}+ --+ -- If there are no tuples in the result type, no values are put into 'Regs'.+ --+ -- >>> :{+ -- args :: Args (Identity String)+ -- args = pure (Identity "foo")+ -- registeredArgs :: Args (Identity (Regs String))+ -- registeredArgs = register args+ -- :}+ register :: forall m. (Functor m) => Args (m nested) -> Args (m (Regs tip))++instance (Registrable_ (IsReg nested) nested tip) => Registrable nested tip where+ register amnested = register_ (Proxy @(IsReg nested)) do fmap (fmap pure) amnested++class Registrable_ (where_ :: WhereNested) nested tip | where_ nested -> tip where+ register_ :: forall m. (Functor m) => Proxy where_ -> Args (m (Regs nested)) -> Args (m (Regs tip))++instance Registrable_ Innermost a a where+ register_ _ = id++instance (Typeable b, Monoid b, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup2 (b, rest) tip where+ register_ _ af =+ register_ (Proxy @(IsReg rest)) do+ tell1 <- foretellReg @b+ action <- af+ pure (action <&> \regs -> regs >>= \(b, rest) -> tell1 b *> pure rest)++instance (Typeable b, Monoid b, Typeable c, Monoid c, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup3 (b, c, rest) tip where+ register_ _ af =+ register_ (Proxy @(IsReg rest)) do+ tell1 <- foretellReg @b+ tell2 <- foretellReg @c+ action <- af+ pure (action <&> \regs -> regs >>= \(b, c, rest) -> tell1 b *> tell2 c *> pure rest)++instance (Typeable b, Monoid b, Typeable c, Monoid c, Typeable d, Monoid d, Registrable_ (IsReg rest) rest tip) => Registrable_ Tup3 (b, c, d, rest) tip where+ register_ _ af =+ register_ (Proxy @(IsReg rest)) do+ tell1 <- foretellReg @b+ tell2 <- foretellReg @c+ tell3 <- foretellReg @d+ action <- af+ pure (action <&> \regs -> regs >>= \(b, c, d, rest) -> tell1 b *> tell2 c *> tell3 d *> pure rest)++-- $registrations+--+-- The 'Args' applicative has an additional feature: it lets you \"register\"+-- ahead of time the types of some values that /might/ be included in the result+-- of the 'Args', but without being reflected in the result type. It's not+-- mandatory that these values must be ultimately produced, however.+--+-- Here's an example. We have an 'Args' value that returns a 'Regs'. While+-- constructing the 'Args' value, we register the @Sum Int@ and @All@ types+-- using 'foretellReg', which also gives us the means of later writing into the+-- 'Regs'. By using 'getRegsReps', we can inspect the 'TypeRep's of the types we+-- registered without having to run the 'Args',+--+-- >>> :{+-- fun2 :: String -> Bool -> Int+-- fun2 _ _ = 5+-- args :: Args (Regs Int)+-- args = do -- Using ApplicativeDo+-- r <- fun2 <$> arg <*> arg -- could also have used 'wire'+-- tell1 <- foretellReg @(Sum Int)+-- tell2 <- foretellReg @All+-- pure $ do+-- tell1 (Sum 11)+-- tell2 (All False)+-- pure r+-- :}+--+-- >>> :{+-- let reps = getRegsReps args+-- in ( reps == Data.Set.fromList [ SomeMonoidTypeRep $ Type.Reflection.typeRep @(Sum Int)+-- , SomeMonoidTypeRep $ Type.Reflection.typeRep @All]+-- , args & runArgs (taste $ fromDynList [toDyn @String "foo", toDyn False])+-- & runRegs reps+-- & \(beans,_) -> (taste @(Sum Int) beans, taste @All beans)+-- )+-- :}+-- (True,(Just (Sum {getSum = 11}),Just (All {getAll = False})))++-- $setup+-- >>> :set -XBlockArguments+-- >>> :set -XOverloadedLists+-- >>> :set -XApplicativeDo+-- >>> :set -XGADTs+-- >>> :set -Wno-incomplete-uni-patterns+-- >>> import Data.Functor.Identity+-- >>> import Data.Function ((&))+-- >>> import Data.Monoid+-- >>> import Cauldron.Beans (taste)
lib/Cauldron/Builder.hs view
@@ -128,7 +128,7 @@ then Left $ DuplicateBeans beanDefinitions else Right c --- | Because cauldron inject dependencies based on their types, a do-notation block which+-- | Because 'Cauldron's inject dependencies based on their types, a do-notation block which -- binds two or more values of the same type would be ambiguous. -- -- >>> :{
test/appTests.hs view
@@ -9,7 +9,6 @@ module Main (main) where import Cauldron-import Data.Maybe (fromJust) import Test.Tasty import Test.Tasty.HUnit @@ -107,9 +106,9 @@ makeZDeco2 :: F -> Z -> (Initializer, Z) makeZDeco2 = \_ z -> (Initializer (putStrLn "Z deco init"), z) -coolWiring :: Fire IO -> Either RecipeError (IO Entrypoint)+coolWiring :: Fire IO -> Either CookingError (IO Result) coolWiring fire = do- fmap (fmap (fromJust . taste @Entrypoint)) $ cook fire cauldron+ cook fire cauldron cauldron :: Cauldron IO cauldron :: Cauldron IO =@@ -139,10 +138,10 @@ val $ wire makeZDeco2 ] },- recipe @Entrypoint $ val $ wire Entrypoint+ recipe @Result $ val $ wire Result ] -data Entrypoint = Entrypoint Initializer Inspector Z+data Result = Result Initializer Inspector Z tests :: TestTree tests =
test/argsTests.hs view
@@ -15,10 +15,11 @@ import Data.Dynamic import Data.Function ((&)) import Data.Proxy-import Data.Text (Text) import Data.Typeable (typeRep) import Test.Tasty import Test.Tasty.HUnit++type Text = String data A = A
test/codecTests.hs view
@@ -165,7 +165,7 @@ Right _ -> assertFailure "Builder should have failed with duplicate beans error", testCase "should fail cycle wiring" do Data.Foldable.for_ @[] [("forbid", forbidDepCycles), ("selfdeps", allowSelfDeps)] \(name, fire) ->- case cook fire cauldron of+ case cook @(Serializer Foo) fire cauldron of Left (DependencyCycleError _) -> pure () Left _ -> assertFailure $ "Unexpected error when wiring" ++ name Right _ -> assertFailure $ "Unexpected success when wiring" ++ name,@@ -175,25 +175,22 @@ ("someConsume", cauldronAccums2, Acc 10) ] \(name, c, expected) ->- case cook allowDepCycles c of+ case cook @Acc allowDepCycles c of Left _err -> do -- putStrLn $ prettyRecipeError err assertFailure $ "could not wire " ++ name- Right (Identity bs) ->- case taste bs of- Nothing -> assertFailure $ "accum not found " ++ name- Just (acc :: Acc) -> do+ Right (Identity acc) -> assertEqual "experted result" expected acc,- testCase "problematic wiring with accums" do+ testCase "wiring with accums" do Data.Foldable.for_ @[]- [ ("selfacc", cauldronAccumsOops1),- ("indirectacc", cauldronAccumsOops2)+ [ ("aggcyle", cauldronAccumsOops1),+ ("indirectagg", cauldronAccumsOops2) ] \(name, c) ->- case cook allowDepCycles c of- Left (DependencyCycleError _) -> pure ()+ case cook @(Serializer Foo) allowDepCycles c of+ Left (DependencyCycleError _) -> assertFailure $ "We should be able to wire cycles with accs" Left _ -> assertFailure $ "Unexpected error when wiring" ++ name- Right _ -> assertFailure $ "Unexpected success when wiring" ++ name+ Right _ -> pure () ] where makeBasicTest :: Cauldron Identity -> IO ()@@ -202,10 +199,7 @@ Left _ -> do -- putStrLn $ prettyRecipeError err assertFailure "could not wire"- Right (Identity bs) ->- case taste bs of- Nothing -> assertFailure "serializer not found"- Just (Serializer {runSerializer}) -> do+ Right (Identity (Serializer {runSerializer})) -> do let value = FooToBar (BarToFoo (FooToBar (BarToBaz EndBaz))) assertEqual "experted result" ".FooToBar.BarToFoo.FooToBar.BarToBar.EndBaz" (runSerializer value)
test/managedTests.hs view
@@ -10,10 +10,11 @@ import Cauldron.Managed import Data.IORef import Data.Maybe (fromJust)-import Data.Text (Text) import Test.Tasty import Test.Tasty.HUnit +type Text = String+ newtype Logger m = Logger { logMessage :: Text -> m () }@@ -95,7 +96,7 @@ case cook allowSelfDeps (managedCauldron ref) of Left _ -> assertFailure "could not wire" Right beansAction -> with beansAction \boiledBeans -> do- let (Logger {logMessage}, (Weird {anotherWeirdOp}) :: Weird IO) = fromJust . taste $ boiledBeans+ let (Logger {logMessage}, (Weird {anotherWeirdOp}) :: Weird IO) = boiledBeans logMessage "foo" anotherWeirdOp pure ()
test/tests.hs view
@@ -8,28 +8,23 @@ module Main (main) where -import Algebra.Graph.AdjacencyMap import Cauldron-import Control.Monad import Control.Monad.IO.Class import Control.Monad.Trans.Writer import Data.Function ((&))-import Data.Functor.Identity import Data.IORef-import Data.List.NonEmpty (NonEmpty)-import Data.List.NonEmpty qualified import Data.Map (Map) import Data.Map qualified as Map-import Data.Maybe (fromJust) import Data.Monoid import Data.Proxy import Data.Set qualified-import Data.Text (Text)-import Data.Tree import Data.Typeable (typeRep) import Test.Tasty import Test.Tasty.HUnit+import Data.Foldable qualified +type Text = String+ type M = WriterT [Text] IO -- | And initialization action which some beans might register.@@ -137,13 +132,15 @@ & insert @(Logger M) (eff $ wire \(_ :: Repository M) -> makeLogger) -cauldronNonEmpty :: NonEmpty (Cauldron M)-cauldronNonEmpty =- Data.List.NonEmpty.fromList- [ fromRecipeList- [ recipe @(Logger M) $ eff $ pure makeLogger,- recipe @(Weird M) $ eff $ wire makeWeird- ],+cauldronX1 :: Cauldron M+cauldronX1 =+ fromRecipeList+ [ recipe @(Logger M) $ eff $ pure makeLogger, + recipe @(Weird M) $ eff $ wire makeWeird -- overwritten+ ]++cauldronX2 :: Cauldron M+cauldronX2 = fromRecipeList [ recipe @(Repository M) $ eff $ do action <- wire makeRepository@@ -159,48 +156,21 @@ val $ wire (weirdDeco "outer") ] },- recipe @(Initializer, Repository M, Weird M) $ val_ do wire (,,)+ recipe @Result $ val_ do wire Result ]- ] -cauldronNonEmptyWrongOrder :: NonEmpty (Cauldron M)-cauldronNonEmptyWrongOrder = do- Data.List.NonEmpty.fromList- [ fromRecipeList- [ recipe @(Weird M) $ eff $ wire makeWeird- ],- fromRecipeList- [ recipe @(Logger M) $ eff $ pure makeLogger- ]- ]+data Result = Result Initializer (Repository M) (Weird M) +cauldronX :: Cauldron M+cauldronX = cauldronX1 <> cauldronX2++ cauldronLonely :: Cauldron M cauldronLonely = fromRecipeList [ recipe @(Lonely M) $ val $ pure makeLonely ] -data A = A--makeA :: (Sum Int, A)-makeA = (Sum 1, A)--data B = B--makeB :: A -> (Sum Int, B)-makeB _ = (Sum 7, B)--data C = C--makeC :: A -> (Sum Int, C)-makeC _ = (Sum 11, C)--treeOfCauldrons :: Tree (Cauldron Identity)-treeOfCauldrons =- Node- [recipe $ val $ wire makeA]- [Node [recipe $ val $ wire makeB] [], Node [recipe $ val $ wire makeC] []]- tests :: TestTree tests = testGroup@@ -210,7 +180,7 @@ Left _ -> assertFailure "could not wire" Right beansAction -> runWriterT do boiledBeans <- beansAction- let (Initializer {runInitializer}, Repository {findById, store}) = fromJust . taste $ boiledBeans+ let (Initializer {runInitializer}, Repository {findById, store}) = boiledBeans runInitializer store 1 "foo" _ <- findById 1@@ -225,15 +195,12 @@ ] traces, testCase "value sequential" do- ((), traces) <- case cookNonEmpty' cauldronNonEmpty of+ ((), traces) <- case cook @Result allowSelfDeps cauldronX of Left _ -> assertFailure "could not wire" Right beansAction -> do runWriterT do- _ Data.List.NonEmpty.:| [boiledBeans] <- beansAction- let ( Initializer {runInitializer},- Repository {findById, store},- Weird {anotherWeirdOp}- ) = fromJust . taste $ boiledBeans+ boiledBeans <- beansAction+ let Result (Initializer {runInitializer}) (Repository {findById, store}) (Weird {anotherWeirdOp}) = boiledBeans runInitializer store 1 "foo" _ <- findById 1@@ -241,8 +208,10 @@ pure () assertEqual "traces"- [ "logger constructor",- "weird constructor",+ [ + -- "weird constructor", -- not happens, because overwritten+ -- the order of the traces here is a bit too overspecified. several orders could be valid.+ "logger constructor", "self-invoking weird constructor", "logger init", "repo init invoking logger",@@ -258,37 +227,66 @@ "weirdOp 2" ] traces- case getDependencyGraph <$> cauldronNonEmpty of- dg1 Data.List.NonEmpty.:| [dg2] -> do- let _adj1 = toAdjacencyMap dg1- let adj2 = toAdjacencyMap dg2- unless (hasVertex (PrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do- assertFailure "cauldron 2 doesn't have the fully built logger from cauldron 1 in its dep graph"- when (hasVertex (BarePrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do- assertFailure "cauldron 2 has the bare undecorated logger from cauldron 1 in its dep graph, despite not depending on it directly"- pure ()- _ -> assertFailure "should never happen, malformed test",- testCase "value sequential - parents can't see beans in children" do- case cookNonEmpty' cauldronNonEmptyWrongOrder of- Left (MissingDependenciesError _) -> pure ()- Left _ -> assertFailure "Unexpected error"- Right _ -> assertFailure "parent cauldron sees bean in child cauldron",- testCase "tree of cauldrons" do- case cookTree' treeOfCauldrons of- Left err -> assertFailure $ "failed to build tree: " ++ show err- Right (Identity beans) -> case beans of- Node bbase [Node bbranch1 [], Node bbranch2 []] ->- assertEqual- "expected accs across branches"- (Just (Sum 1), Just (Sum 8), Just (Sum 12))- (taste @(Sum Int) bbase, taste @(Sum Int) bbranch1, taste @(Sum Int) bbranch2)- _ -> assertFailure $ "tree has unexpected shape",+ --case getDependencyGraph cauldronNonEmpty of+ -- dg2 -> do+ -- let adj2 = toAdjacencyMap dg2+ -- unless (hasVertex (PrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do+ -- assertFailure "cauldron 2 doesn't have the fully built logger from cauldron 1 in its dep graph"+ -- when (hasVertex (BarePrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do+ -- assertFailure "cauldron 2 has the bare undecorated logger from cauldron 1 in its dep graph, despite not depending on it directly"+ -- pure ()+ ,+ testCase "value nested" do+ ((), traces) <- case (+ do constructorX2 <- nest allowSelfDeps cauldronX2+ cook @Result allowSelfDeps (cauldronX1 & Cauldron.insert @Result constructorX2)) of+ Left _ -> assertFailure "could not wire"+ Right beansAction -> do+ runWriterT do+ boiledBeans <- beansAction+ let Result (Initializer {runInitializer}) (Repository {findById, store}) (Weird {anotherWeirdOp}) = boiledBeans+ runInitializer+ store 1 "foo"+ _ <- findById 1+ anotherWeirdOp+ pure ()+ assertEqual+ "traces"+ [ + -- the order of the traces here is a bit too overspecified. several orders could be valid.+ "logger constructor",+ "self-invoking weird constructor",+ "weird constructor", -- note that this is present. Overwritten by nested, but still built+ -- The absence of the logger init is because we are only getting the aggregate beans from the nested.+ -- "logger init",+ "repo init invoking logger",+ "store",+ "findById",+ -- the deco is applied! The outer the deco, the earliest is invoked.+ "deco for anotherWeirdOp outer",+ "deco for anotherWeirdOp inner",+ "another weirdOp 2",+ "deco for weirdOp outer",+ "deco for weirdOp inner",+ -- note that the self-invocation used the method from 'makeSelfInvokingWeird'+ "weirdOp 2"+ ]+ traces+ --case getDependencyGraph cauldronNonEmpty of+ -- dg2 -> do+ -- let adj2 = toAdjacencyMap dg2+ -- unless (hasVertex (PrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do+ -- assertFailure "cauldron 2 doesn't have the fully built logger from cauldron 1 in its dep graph"+ -- when (hasVertex (BarePrimaryBean (typeRep (Proxy @(Logger M)))) adj2) do+ -- assertFailure "cauldron 2 has the bare undecorated logger from cauldron 1 in its dep graph, despite not depending on it directly"+ -- pure ()+ , testCase "lonely beans get built" do- (_, _) <- case cook' cauldronLonely of+ (_, _) <- case cook allowSelfDeps cauldronLonely of Left _ -> assertFailure "could not wire" Right beansAction -> runWriterT do boiledBeans <- beansAction- let Lonely {soLonely} = fromJust . taste $ boiledBeans+ let Lonely {soLonely} = boiledBeans soLonely pure () pure (),@@ -305,14 +303,14 @@ pure (), testCase "cauldron with cycle" do case cook' cauldronWithCycle of- Left (DependencyCycleError _) -> pure ()+ Left (DependencyCycleError (DependencyCycle vs)) -> + -- Why not a cycle of length 3? Because there also are bare versions for each bean.+ assertEqual "cycle of the expected length" 4 (Data.Foldable.length vs) _ -> assertFailure "dependency cycle not detected" pure () ] where cook' = cook allowSelfDeps- cookNonEmpty' = cookNonEmpty . fmap (allowSelfDeps,)- cookTree' = cookTree . fmap (allowSelfDeps,) main :: IO () main = defaultMain tests