Make for Haskell values, part alpha

…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.
  • generic: Resources include arbitrary Haskell types.
  • dynamic: 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.
  • instances: 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.
  • stability: 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.
  • precise configuration: 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.
  • 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.
  • predictable: avoid surprising feature interactions
  • flexible: 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:

  1. Data.Typeable to support generic types as resources.
  2. 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`.
  3. 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.
  4. 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.
  5. 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.
  6. For external configurability, the first assignment of a rule generally overrides later assignments.
  7. 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.
  8. 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.
  9. 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.

About these ads
This entry was posted in Language Design, Modularity, Open Systems Programming, Types. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s