diff options
Diffstat (limited to 'provider/posts')
-rw-r--r-- | provider/posts/events/02.lhs | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/provider/posts/events/02.lhs b/provider/posts/events/02.lhs new file mode 100644 index 0000000..9b919a7 --- /dev/null +++ b/provider/posts/events/02.lhs | |||
@@ -0,0 +1,174 @@ | |||
1 | --- | ||
2 | title: "A Monad Encoding Co-Total Lists Admitting Cutoff-Rules" | ||
3 | published: 2016-07-29 | ||
4 | tags: events | ||
5 | repo: https://git.yggdrasil.li/gkleen/pub/events | ||
6 | base: https://git.yggdrasil.li/gkleen/pub/events/tree/events/src/Events/Types/NDT.hs?id=9bffd435230514c00177a315bf65d9c13969f7dc | ||
7 | --- | ||
8 | |||
9 | In [the last post](01.html) I presented `Eval`{.haskell}, a monad transformer | ||
10 | for constructing a finite list of `Object`{.haskell}s together with a | ||
11 | `Bool`{.haskell}-Annotation (denoting whether the `Event`{.haskell} represented | ||
12 | by the `Object`{.haskell} should occur or not) while having access to arbitrary | ||
13 | monadic state. | ||
14 | |||
15 | The Implementation presented used [`ListT`{.haskell}][] to encapsulate | ||
16 | non-determinism. | ||
17 | |||
18 | Consider the following (nonfunctional[^brokenExample] for the sake of readability) example: | ||
19 | |||
20 | ~~~ {.haskell} | ||
21 | fmap filterTime . evaluate {- This is not the `evaluate` defined in Events.Eval -} $ do | ||
22 | n <- fromFoldable [1..] | ||
23 | objPayload .= [ ("date", toJSON $ (n * 7) `addDays` (fromGregorian 2016 07 29)) | ||
24 | ] | ||
25 | where | ||
26 | filterTime :: [Object] -> [Object] | ||
27 | filterTime = filter $ maybe False (\t -> qDate <= t && t <= qDate') <$> view (objPayload . key "date" . asDate) | ||
28 | qDate = fromGregorian 2016 08 01 | ||
29 | qDate' = fromGregorian 2016 08 07 | ||
30 | ~~~ | ||
31 | |||
32 | Due to `fromFoldable [1..]`{.haskell} this will surely never terminate yet it is obvious that the question, whether the week from 2016-08-01 to 2016-08-07 contains one of our series of events, is answerable in finite time. | ||
33 | |||
34 | We would like to be able to employ constructions like the one above for the sake of convenience – it is often the case that we don´t yet know when a regularly scheduled appointment will stop taking place. | ||
35 | Ensuring that our list-comprehensions always terminate is of course the halting problem – we can however employ some trickery to make rather simple constructs, like the one above, work. | ||
36 | |||
37 | To do so in our concrete application we leverage the fact that, at evaluation time, we always know which `TimeRange`{.haskell} we´re interested in and can stop, given that we generate new `Event`{.haskell}s only monotonically in time, checking new `Event`{.haskell}s exactly when we encounter the first in our worldline which lies entirely after our `TimeRange`{.haskell}. | ||
38 | |||
39 | > {-# LANGUAGE GADTs #-} | ||
40 | > {-# LANGUAGE TupleSections #-} | ||
41 | > {-# LANGUAGE ViewPatterns #-} | ||
42 | > {-# LANGUAGE FlexibleContexts, FlexibleInstances #-} | ||
43 | > | ||
44 | > module Events.Types.NDT | ||
45 | > ( NDT | ||
46 | > , foldNDT | ||
47 | > , cons | ||
48 | > , fromFoldable | ||
49 | > ) where | ||
50 | > | ||
51 | > import Data.Monoid | ||
52 | > import Data.Foldable (foldr) | ||
53 | > import Data.Maybe | ||
54 | > import Data.Either | ||
55 | > | ||
56 | > import Control.Applicative (Alternative) | ||
57 | > import qualified Control.Applicative as Alt (Alternative(..)) | ||
58 | > import Control.Monad | ||
59 | > import Control.Monad.Identity | ||
60 | > | ||
61 | > import Control.Monad.Trans | ||
62 | > | ||
63 | > data NDT m a where | ||
64 | > NDTBind :: NDT m a -> (a -> NDT m b) -> NDT m b | ||
65 | > NDTCons :: m (Maybe (a, NDT m a)) -> NDT m a | ||
66 | |||
67 | `NDT m a`{.haskell} as defined above is essentially just `m [a]`{.haskell}. | ||
68 | The more involved construction, reminiscent of [`Free`{.haskell}][][^usingFree] is owed to the fact that stopping to evaluate after some criterion requires that criterion be known; we´d have to save it in `NDT`{.haskell}, which is incompatible with `return`{.haskell}. | ||
69 | |||
70 | What follows are some fairly straight forward and not particularly interesting instances needed essentially for making `NDT`{.haskell} into a proper monad-transformer. | ||
71 | |||
72 | > instance Functor m => Functor (NDT m) where | ||
73 | > fmap f (NDTBind a g) = NDTBind a (fmap f . g) | ||
74 | > fmap f (NDTCons x) = NDTCons $ fmap f' x | ||
75 | > where | ||
76 | > f' Nothing = Nothing | ||
77 | > f' (Just (x, xs)) = Just (f x, fmap f xs) | ||
78 | > | ||
79 | > instance Applicative m => Applicative (NDT m) where | ||
80 | > pure x = NDTCons . pure $ Just (x, empty) | ||
81 | > fs <*> xs = fs >>= (\f -> xs >>= pure . (f $)) | ||
82 | > | ||
83 | > instance Applicative m => Monad (NDT m) where | ||
84 | > return = pure | ||
85 | > (>>=) = NDTBind | ||
86 | > | ||
87 | > instance Monad m => Monoid (NDT m a) where | ||
88 | > mempty = empty | ||
89 | > mappend (NDTCons x) y'@(NDTCons y) = NDTCons $ maybe y (\(x, xs) -> return $ Just (x, xs <> y')) =<< x | ||
90 | > mappend (NDTBind x f) (NDTBind y g) = NDTBind (fmap Left x <> fmap Right y) (either f g) | ||
91 | > mappend x@(NDTBind _ _) y = x <> NDTBind y return | ||
92 | > mappend x y@(NDTBind _ _) = NDTBind x return <> y | ||
93 | > | ||
94 | > instance MonadTrans NDT where | ||
95 | > lift = NDTCons . fmap Just . fmap (,empty) | ||
96 | > | ||
97 | > instance Monad m => Alternative (NDT m) where | ||
98 | > empty = mempty | ||
99 | > (<|>) = mappend | ||
100 | > | ||
101 | > instance Monad m => MonadPlus (NDT m) where | ||
102 | > mzero = mempty | ||
103 | > mplus = mappend | ||
104 | > | ||
105 | > instance MonadIO m => MonadIO (NDT m) where | ||
106 | > liftIO = lift . liftIO | ||
107 | > | ||
108 | > empty :: Applicative m => NDT m a | ||
109 | > empty = NDTCons $ pure Nothing | ||
110 | > | ||
111 | > cons :: Applicative m => a -> NDT m a -> NDT m a | ||
112 | > cons x xs = NDTCons . pure $ Just (x, xs) | ||
113 | > | ||
114 | > fromFoldable :: (Foldable f, Monad m) => f a -> NDT m a | ||
115 | > fromFoldable = foldr cons empty | ||
116 | |||
117 | The evaluation function below implements the pruning behaviour described above: | ||
118 | |||
119 | > foldNDT :: (Foldable f, Applicative f, Monoid (f a), Monad m) => (a -> m Bool) -> NDT m a -> m (f a) | ||
120 | > -- ^ Evaluate depth-first, pruning leaves under the assumption that the selection predicate is monotonic on siblings and children | ||
121 | |||
122 | Cons-cells are rather straightforward – iff the head doesn´t fulfil our criterion we ignore both it and the tail: | ||
123 | |||
124 | > foldNDT sel (NDTCons mx) = do | ||
125 | > mx' <- mx | ||
126 | > case mx' of | ||
127 | > Nothing -> return mempty | ||
128 | > Just (x, mxs) -> do | ||
129 | > continue <- sel x | ||
130 | > case continue of | ||
131 | > False -> return mempty | ||
132 | > True -> (pure x <>) <$> foldNDT sel mxs | ||
133 | |||
134 | This encodes pruning children of cons-cells which don´t satisfy the criterion in our tree composed of `NDTCons`{.haskell} and `NDTBind`{.haskell}. | ||
135 | |||
136 | Deferred bind operations (`NDTBind`{.haskell}) are somewhat more involved. | ||
137 | To make handling them easier we first squash `NDTBind`{.haskell}s such that the only children of `NDTBind`{.haskell}s are always cons-cells. | ||
138 | |||
139 | > foldNDT sel (NDTBind (NDTBind x g) f) = foldNDT sel $ NDTBind x (f <=< g) | ||
140 | |||
141 | The desired pruning behaviour for `NDTBind`{.haskell}s is as follows: iff evaluating the tree produced by applying `f`{.haskell} to the head of the cons-cell does not produce a valid object we discard the tail, too[^unproductivePaths]. | ||
142 | |||
143 | > foldNDT sel (NDTBind (NDTCons x) f) = do | ||
144 | > x' <- x | ||
145 | > case x' of | ||
146 | > Nothing -> return mempty | ||
147 | > Just (x'', xs) -> do | ||
148 | > x3 <- foldNDT sel $ f x'' | ||
149 | > xs' <- if null x3 then return mempty else foldNDT sel (NDTBind xs f) | ||
150 | > return $ x3 <> xs' | ||
151 | |||
152 | Using the above the following now works: | ||
153 | |||
154 | ~~~ {.haskell} | ||
155 | (Yaml.encode <=< evaluate predicate) $ do | ||
156 | n <- lift $ NDT.fromFoldable ([1..] :: [Integer]) | ||
157 | objOccurs .= (n >= 2) | ||
158 | objPayload ?= [ ("num", Yaml.Number $ fromIntegral n) | ||
159 | ] | ||
160 | where | ||
161 | predicate :: Monad m => Maybe Yaml.Object -> m Bool | ||
162 | predicate = ordPredicate $ maybe LT (`compare` 5) . view (at "num" . asDouble) | ||
163 | ordPredicate :: Applicative m => (Object -> Ordering) -> (Maybe Object -> m Bool) | ||
164 | ordPredicate _ Nothing = pure True | ||
165 | ordPredicate f (Just (f -> o)) = pure $ o <= EQ -- View Patterns | ||
166 | ~~~ | ||
167 | |||
168 | [^brokenExample]: Neither `fromFoldable`{.haskell} nor `asDate`{.haskell} currently exist. I didn´t even bother to see whether the example typechecks. | ||
169 | [^usingFree]: I am confident that `NDT`{.haskell} could be alternatively constructed using the monad-transformer version of `Free`{.haskell}: [`FreeT`{.haskell}][] | ||
170 | [^unproductivePaths]: Currently this discards trees which don´t produce valid objects not because they produce invalid ones (which is our stated termination-criterion) but because they don´t produce anything at all. I´ll probably end up changing that after I find a case where that´s a problem (Care to provide one?). | ||
171 | |||
172 | [`ListT`{.haskell}]: <https://hackage.haskell.org/package/list-t> | ||
173 | [`Free`{.haskell}]: <https://hackage.haskell.org/package/free/docs/Control-Monad-Free.html#t:Free> | ||
174 | [`FreeT`{.haskell}]: https://hackage.haskell.org/package/free/docs/Control-Monad-Trans-Free.html#t:FreeT<> | ||