A few days ago I wrote an article about Make for Haskell values. Somehow people read “Make” and ignore the “for Haskell values” part. So I’ll clarify: Control.Make is intended to be a declarative dependency injection, build, and configurations model for Haskell values.
Control.Make is intended for staged programming, where the Haskell value in question is perhaps a program described by Haskell EDSL, such as arrows. I am developing this for Sirea – an RDP implementation – where Make would be responsible for:
- implicitly create a thread for each primary partition type
- automatically establish communication between threads
- create special threads for special tasks as needed – e.g. audio, rendering, monitoring the filesystem
- automatically hook reactions to various resources, such as framerate of video or state of the file
A point is that this connectivity is non-local in RDP. A resource might be observed and influenced concurrently from multiple independent locations in one program. I needed a way to initialize and hook those resources from multiple locations in the program that did not push a lot of boiler-plate to the client. Further, I wanted a way to configure resources externally.
The main features that set Control.Make apart from other configuration techniques for Haskell, such as Oleg’s Implicit Configurations, are support for defaults and precise, demand driven construction. Defaults allow clients to opt-in to configuration gradually, controlling their descent into dependency hell. Defaults also allow libraries to extend their set of dependencies without breaking clients. Demand driven construction means that resources are only built if it seems necessary, as opposed to building resources up front in case we need them.
This weekend I’ve spent some time developing Control.Make a bit further.
I added a requirement that wasn’t in my original list: all or nothing construction. That is, I do not want a build failing halfway through due to missing or inconsistent rules. This is a sort of static resource safety property, to augment Haskell’s type safety. I would also like more flexibility in selecting rules and defaults, perhaps based on soft constraints rather than than the simplistic “first specified is first favored”. These properties have required a considerable overhaul of the prior design, to support staged computations.
On Sunday I was considering a technique with the following basic shape (still using
Dynamic.Typeable magic to make it work):
runMake :: Make m (MakePromise m a) -> Either (m a) MakeFailure
addMakeRule :: (Typeable a) => Make m (MakePromise m a) -> Make m ()
make :: (Typeable a) => Make m (MakePromise m a)
makeAction :: m a -> Make m (MakePromise m a)
The idea of MakePromise is to represent a value that will be available at build time.
Unfortunately, I’ve had much difficulty understanding what should happen when I pass a MakePromise as an argument to a new make rule (
make >>= addMakeRule . return).
I think the answer might be to switch from Make-the-monad to Make-the-applicative. With applicative, I might also be able to drop the promises by switching to a GADT for static analysis (since the flow of outputs is under much tighter control).