summaryrefslogtreecommitdiff
path: root/provider/posts/thermoprint/5.md
diff options
context:
space:
mode:
Diffstat (limited to 'provider/posts/thermoprint/5.md')
-rw-r--r--provider/posts/thermoprint/5.md198
1 files changed, 198 insertions, 0 deletions
diff --git a/provider/posts/thermoprint/5.md b/provider/posts/thermoprint/5.md
new file mode 100644
index 0000000..0249734
--- /dev/null
+++ b/provider/posts/thermoprint/5.md
@@ -0,0 +1,198 @@
1---
2title: Building an Extensible Framework for Specifying Compile-Time Configuration using Universal Quantification
3tags: Thermoprint
4published: 2016-02-18
5---
6
7When I write *Universal Quantification* I mean what is commonly referred to as
8[existential quantification](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/data-type-extensions.html#existential-quantification),
9which I think is a misnomer. To wit:
10
11$( \exists x \ldotp f(x) ) \to y$ is isomorphic to $\forall x \ldotp (f(x) \to y)$ (I
12won´t try to back this claim up with actual category theory just now. You might want to
13nag me occasionally if this bothers you -- I really should invest some more time into
14category theory). Since haskell does not support `exists` we´re required to use the
15`forall`-version, which really is universally quantified.
16
17## Printer Configuration
18
19What we want is to have the user provide us with a set of specifications of how to
20interact with one printer each.
21Something like the following:
22
23~~~ {.haskell}
24newtype PrinterMethod = PM { unPM :: Printout -> IO (Maybe PrintingError) }
25
26data Printer = Printer
27 { print :: PrinterMethod
28 , queue :: TVar Queue
29 }
30~~~
31
32The first step in refining this is necessitated by having the user provide the
33[monad-transformer-stack](http://book.realworldhaskell.org/read/monad-transformers.html)
34to use at compile time.
35Thus we introduce our first universal quantification (in conjunction with
36[polymorphic components](https://prime.haskell.org/wiki/PolymorphicComponents)) -- this
37one is not isomorphic to an existential one:
38
39~~~ {.haskell}
40newtype PrinterMethod = PM { unPm :: forall m. MonadResource m => Printout -> m (Maybe PrintingError) }
41~~~
42
43Since we don´t want to *burden* the user with the details of setting up `TVar Queue`{.haskell} we
44also introduce function to help with that:
45
46~~~ {.haskell}
47printer :: MonadResource m => PrinterMethod -> m Printer
48printer p = Printer p <$> liftIO (newTVarIO def)
49~~~
50
51We could at this point provide ways to set up `PrinterMethod`{.haskell}s and have the user
52provide us with a list of them.
53
54We, however, have numerous examples of printers which require some setup (such opening a
55file descriptor). The idiomatic way to handle this is to decorate that setup with some
56constraints and construct our list of printers in an
57[`Applicative`{.haskell}](https://hackage.haskell.org/package/base/docs/Control-Applicative.html#t:Applicative)
58fashion:
59
60~~~ {.haskell}
61printer :: MonadResource m => m PrinterMethod -> m Printer
62printer p = Printer <$> p <*> liftIO (newTVarIO def)
63~~~
64
65At this point a toy implementation of a printer we might provide looks like this:
66
67~~~ {.haskell}
68debugPrint :: Applicative m => m PrinterMethod
69debugPrint = pure . PM $ const return Nothing <=< liftIO . putStrLn . toString
70
71toString :: Printout -> String
72toString = undefined
73~~~
74
75## Management of Printer Queues
76
77We would like the user to be able to modify the printer queues we maintain in arbitrary
78ways.
79The motivation for this being various cleanup operations such as pruning all successful
80jobs older than a few minutes or limiting the size of history to an arbitrary number of
81entries.
82
83A pattern for this type of modification of a value residing in a `TVar`{.haskell} might
84look like this:
85
86~~~ {.haskell}
87modify :: TVar a -> StateT a STM () -> IO ()
88modify q f = atomically $ writeTVar =<< runStateT f =<< readTVar q
89~~~
90
91A rather natural extension of this is to allow what we will henceforth call a
92`QueueManager`{.haskell} (currently `StateT a STM ()`{.haskell}) to return an indication
93of when it wants to be run again:
94
95~~~ {.haskell}
96type QueueManager = StateT Queue STM Micro
97
98runQM :: QueueManager -> TVar Queue -> IO ()
99runQM qm q = sleep << qm'
100 where
101 qm' = atomically $ (\(a, s) -> a <$ writeTVar q s) =<< runStateT qm =<< readTVar q
102 sleep (abs -> delay) = threadDelay (fromEnum delay) >> runQM qm q
103~~~
104
105It stands to reason that sometimes we don't want to run the `QueueManager`{.haskell} ever
106again (probably causing the thread running it to terminate).
107For doing so we
108[extend the real numbers](https://en.wikipedia.org/wiki/Extended_real_number_line) as
109represented by `Micro`{.haskell} to
110[`Extended Micro`{.haskell}](https://hackage.haskell.org/package/extended-reals):
111
112~~~ {.haskell}
113type QueueManager = StateT Queue STM (Extended Micro)
114
115runQM …
116 where
117
118 sleep (abs -> delay)
119 | (Finite d) <- delay = threadDelay (fromEnum d) >> runQM qm q
120 | otherwise = return ()
121~~~
122
123`QueueManager`{.haskell}s whose type effectively is `Queue -> STM (Queue, Extended Micro)`{.haskell}
124are certainly useful but can carry no state between invocations (which would be useful
125e.g. for limiting the rate at which we prune jobs).
126
127Therefore we allow the user to provide an arbitrary monad functor (we use
128`MFunctor`{.haskell} from
129[mmorph](https://hackage.haskell.org/package/mmorph-1.0.6/docs/Control-Monad-Morph.html#t:MFunctor)
130instead of `Servant.Server.Internal.Enter` because
131[servant-server](https://hackage.haskell.org/package/servant-server-0.4.4.6/docs/Servant-Server-Internal-Enter.html#v:Nat)
132doesn't provide all the tools we require for this) which can carry all the state we could
133ever want:
134
135~~~ {.haskell}
136type QueueManager t = QueueManagerM t (Extended Micro)
137type QueueManagerM t = ComposeT (StateT Queue) t STM -- 'ComposeT' is required since we need 'QueueManagerM' to have the form 't' STM' for some 't'' in order to be able to use 'lift'
138
139runQM :: (MFunctor t, MonadTrans t, MonadIO (t IO), Monad (t STM)) => QueueManager t -> TVar Queue -> t IO ()
140runQM … -- nearly identical except for a sprinkling of 'lift'
141~~~
142
143The final touches are to introduce a typeclass `HasQueue`{.haskell} for convenience:
144
145~~~ {.haskell}
146class HasQueue a where
147 extractQueue :: a -> TVar Queue
148
149instance HasQueue (TVar Queue) where
150 extractQueue = id
151
152instance HasQueue Printer where
153 extractQueue = queue
154~~~
155
156and provide some utility functions for composing `QueueManager`{.haskell}s:
157
158~~~ {.haskell}
159intersection :: (Foldable f, MonadState Queue (QueueManagerM t)) => f (QueueManager t) -> QueueManager t
160-- ^ Combine two 'QueueManager's keeping only 'QueueEntry's both managers decide to keep
161--
162-- Side effects propagate left to right
163
164idQM :: Monad (QueueManagerM t) => QueueManager t
165-- ^ Identity of 'intersect'
166
167union :: (Foldable f, MonadState Queue (QueueManagerM t)) => f (QueueManager t) -> QueueManager t
168-- ^ Combine two 'QueueManager's keeping all 'QueueEntry's either of the managers decides to keep
169--
170-- Side effects propagate left to right
171
172nullQM :: MonadState Queue (QueueManagerM t) => QueueManager t
173-- ^ Identity of 'union'
174~~~
175
176We merge the effects of two `QueueManager`{.haskell}s by converting the resulting
177`Queue`{.haskell}s to `Set`{.haskell}s and using `Set.union`{.haskell} and
178`Set.intersection`{.haskell} with appropriate `Ord`{.haskell} and `Eq`{.haskell}
179instances.
180
181### Configuration of `QueueManager`{.haskell}s
182
183A `QueueManager`{.haskell}s configuration shall be a `QueueManager t`{.haskell} associated
184with a specification of how to collapse its monad transformer `t`{.haskell}.
185Using universal quantification this is straightforward:
186
187~~~ {.haskell}
188data QMConfig m = forall t. ( MonadTrans t
189 , MFunctor t
190 , Monad (t STM)
191 , MonadIO (t IO)
192 ) => QMConfig { manager :: QueueManager t
193 , collapse :: (t IO) :~> m
194 }
195
196runQM' :: Printer -> QMConfig m -> m ()
197runQM' printer (QMConfig qm nat) = unNat nat $ runQM qm printer
198~~~