…Make is a monad transformer…

I commanded Google to search the web on my behalf for word of Dependency Injection in Haskell. I was disappointed to see only a few ruminations on using typeclasses. Inflexible, static, global typeclasses. Impenetrability! That’s what I say!

I’m not looking for much. Just the following features in one simple package:

demand-driven: No need to provide a pool of resources up front. Build only the resources I need. I.e. factories or build rules.

No need to provide a pool of resources up front. Build only the resources I need. I.e. factories or build rules. generic: Resources include arbitrary Haskell types.

Resources include arbitrary Haskell types. dynamic: Rules can be adjusted some based on runtime data and awareness of what is being constructed.

Rules can be adjusted some based on runtime data and awareness of what is being constructed. identity: Ability to access the “same” unique resource from multiple locations in code, potentially widely distributed in an application.

Ability to access the “same” unique resource from multiple locations in code, potentially widely distributed in an application. instances: Ability to create multiple instances of a resource, each with a unique identity.

Ability to create multiple instances of a resource, each with a unique identity. instance groups: Ability to create whole substructures with unique resources, and multiple distinct instances of complex multi-component resources.

Ability to create whole substructures with unique resources, and multiple distinct instances of complex multi-component resources. stability: Stability of identity suitable for persistent resources and pseudorandom (creative) resources.

Stability of identity suitable for persistent resources and pseudorandom (creative) resources. configurable: Ability to change behavior of deep code with shallow changes. Ability to model simple configuration properties (preferences, policies, environment vars, etc.) as resources.

Ability to change behavior of deep code with shallow changes. Ability to model simple configuration properties (preferences, policies, environment vars, etc.) as resources. precise configuration: Ability to modify build rules for specific, deep structures that I know will be built – i.e. specific instances or instance groups.

Ability to modify build rules for specific, deep structures that I know will be built – i.e. specific instances or instance groups. good defaults: Ability to provide rules inline, so the client can be ignorant of most needed types. `Soft` rules.

Ability to provide rules inline, so the client can be ignorant of most needed types. `Soft` rules. recursion: Build rules may depend on build rules for other resources. Instance groups can have instance groups. Build rules or factories may themselves be resources.

Build rules may depend on build rules for other resources. Instance groups can have instance groups. Build rules or factories may themselves be resources. predictable: avoid surprising feature interactions

avoid surprising feature interactions flexible: useful in a broad range of situations – IO and pure, multi-use, etc.

useful in a broad range of situations – IO and pure, multi-use, etc. easy to use: simple; minimal boiler-plate; easy to write examples.

I wouldn’t have thought up this list of requirements without an itch to scratch with respect to declarative resource construction. But I was hoping to find more work on the subject.

I can envision a potential solution, or at least facets of one:

Data.Typeable to support generic types as resources. The idea of building resources can be modeled by having rules be monadic. This is flexible by making it an arbitrary monad. A rule for ` Typeable a ` might be ` m a ` for some monad `m`. For recursive rules, the rule is not actually ` m a `; rather, a rule is ` Make m a ` where Make is a monad transformer that allows lifting m. Instances and instance-groups can be named by types (using newtype as needed). Instance groups are recursive, so we have something like a directory of typenames. For simplicity, it might be useful to combine the notions of instances and instance-groups. For stability, can add a unique identifier as parameter to the construction rule. Tweak the above suggestion for a rule to: ` [TypeRep] -> Make m a `, where the [TypeRep] identifies the instance and type, and a recursive path of instance groups. This will be a stable identifier from one Haskell execution to another, supporting persistent resources. It would also make a good seed for pseduo-random number generation (i.e. hash it) which can be useful for creative construction. For external configurability, the first assignment of a rule generally overrides later assignments. For predictability, we can keep rule assignment monotonic: using a default rule is the same as assigning it as the permanent rule. The idea is that all instances of a type in a group use the same rule modulo explicit overrides. For simple defaults, inherit the rules in the recursive instance-group path. This also ensures that all instances in subgroups use the same rules, modulo explicit, prior override. This is predictable and consistent with the individual instances. Support a `chroot` or like concept for secure composition.

Putting these pieces together will require sitting down and hammering it out, but I’m thinking of something close to:



module Control.Make ( Make, MakeCX, MakeStrat(..) , make, makeRule, makeInner, makeChroot) where import Control.Monad.State import Data.Typeable import Data.Dynamic import qualified Data.Map as M type MakeCX m = CX m type MakeRule m a = [TypeRep] -> Make m a newtype Make m a = Make { unMake :: StateT (CX m) m a } data CX m = CX { cx_uplv :: CX cx_path :: [TypeRep] cx_rule :: Maybe (MakeRule m Dynamic) cx_data :: Maybe Dynamic cx_more :: M.Map TypeRep CX } instance MonadTrans Make where ... data MakeFailure = ... -- exception type data Chroot instance (Typeable p) => Typeable (Chroot p) ... emptyMakeCX :: CX emptyMakeCX = CX emptyCX [] Nothing Nothing M.empty runMake :: Make m a -> CX -> m (a, CX) runMake = runStateT . unMake -- make in current group make :: (Typeable a) => Make m a make = makeInst () -- make a named instance makeInst :: (Typeable inst, Typeable a) => inst -> Make m a makeInst = let tyInst = typeOf inst in let tyObj = typeOf (undefined :: a) in -- look under cx_more for typeOf inst -- look under the inst f -- look in cx_more for tyrep and cx_data -- use value if it exists -- if not: find, set, use rule -- if no rule found, throw MakeFailure -- if everything works, return . fromDyn -- make in an instance group; -- instance names can double as groups -- makeInner is also used to configure inner makeInner :: (Typeable inst) => inst -> Make m a -> Make m a makeInner inst op = -- look in cx_more for cx @ typeOf inst -- if it doesn't exist create it -- set child context as current state -- run op -- set parent context to current state -- (preserving changes) makeChroot :: (Typeable inst) => inst -> Make m a -> Make m a makeChroot inst op = makeInner Chroot $ makeInner inst op -- almost, but need to set cx_path for Chroot to [] -- to block searches for rules and start with empty path makeRule :: (Typeable a) => MakeRule m a -> Make m () makeRule rule = -- look in cx_more for typeOf (undefined :: a) -- assign cx_rule (rule >>= return . toDyn) -- unless it is already assigned makeDefaultRule :: (Typeable a) => MakeRule m a -> Make m () -- same as makeRule -- but assigns at root level, i.e. where cx_path = []

This design combines instances, values, and groups into one big map. I could try a little separation instead, but I’ve always liked those filesystems that unify files and directories. The ability to add sub-data to specific values is like adding meta-data to them.

Anyhow, there are still some desiderata I could achieve. It shouldn’t be difficult to support a `makeUnique` that adds a Temp instance and keeps a counter (and tweaks the TypeRep accordingly, and perhaps provides the counter in the environment) then cleans up after itself. (I could instead achieve temporaries by keeping a copy of the prior state. But I’d prefer to do things consistently.) Some ability to access instances in a lower level of the path might be useful.

More critical, I think: I would like the ability to manipulate some strategies, i.e. so I don’t need to spell out how to build a tuple each time. But at the moment I might need to use inflexible typeclasses at the strategy level:

class MakeStrat a where makeStrat :: Make m a instance (MakeStrat a, MakeStrat b) => MakeStrat (a,b) where makeStrat = do a <- makeStrat b <- makeStrat return (a,b) -- about six more of those -- followed by one of these for each type I need instance MakeStrat foo where makeStrat = make

Alternatively I could use `make` directly inside the MakeStrat for (a,b). In that case I wouldn’t need makeStrat for `foo`. But that would cause problems when I have tuples containing tuples. What I’d like to do is build simple strategies inside the CX itself, using TyCon, in which case I could create stuff like usingTupleStrat :: Make m () to add seven strategies, and I’d have flexibility to tweak strategies at runtime. (Or it might be worth just having a dedicated make2..make7.)

I can think of a lot of use-cases for this sort of configuration model. But the only one I care about right now is declarative resource construction for Sirea.

Addendum Apr 14: I would like to have an all-or-nothing Make, and some more declarative properties such as commutativity and idempotence, maybe some support for preferences (based on something other than first write). To achieve these properties, I am wondering if I should impose an extra stage, based on building a future value. I.e. `make` would return a future for an element, and add an obligation to the context. I will still need dynamic context to make this work. Might also be better to have a dedicated operation for getting the instance ID, rather than using a functional make rule.