Making the controllers more linear

Basically, the idea of `(oneBranch <> secondBranch)` instead of

```
do
x <- foo
case x of
  ...
```
This commit is contained in:
MasseR committed 2026-10-23 22:55:06 +03:00
1 parent 617bd255ff
commit 15e50322c4
4 files changed
+51 -17

No files matched your search

+27 -3
View File
@@ -1,4 +1,6 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE Arrows #-}
{-# LANGUAGE TypeOperators #-}
module AFRP
( Mealy(..)
@@ -18,8 +20,10 @@ module AFRP
, changes
, whenA
, filterA
, filterEvent
, thenA
, (>>|)
, (>>||)
, toEvent
, lMerge
, Pair(..)
@@ -271,7 +275,7 @@ data Mealy eff a b = Mealy
-- Provide a name
named :: Text -> Mealy eff a b -> Mealy eff a b
named n f = f{runDiagramC = \run -> run (Step n (emptyDiagram run))}
named n f = f{runDiagramC = \run -> run (Step n (runDiagramC f run))}
instance Semigroup b => Semigroup (Mealy eff a b) where
Mealy ast ad af <> Mealy bst bd bf = Mealy (ast <> bst) (combineDiagram Merge ad bd) $ \nt -> af nt <> bf nt
@@ -290,15 +294,19 @@ eff f = Mealy mempty emptyDiagram $ \nt -> do
instance Category (Mealy eff) where
{- HLINT ignore "Use const" -}
id = Mealy mempty emptyDiagram $ \_nt -> id
(Mealy ast ad f) . (Mealy bst bd g) = Mealy (ast <> bst) (combineDiagram Then ad bd) $ \nt -> do
(Mealy ast ad f) . (Mealy bst bd g) = Mealy (ast <> bst) (combineDiagram Then bd ad) $ \nt -> do
f nt . g nt
instance Arrow (Mealy eff) where
arr f = Mealy mempty emptyDiagram $ \_nt -> arr f
first (Mealy st d f) = Mealy st d $ \nt -> first (f nt)
(Mealy ast ad f) &&& (Mealy bst bd g) = Mealy (ast <> bst) (combineDiagram Both ad bd) $ \nt -> do
f nt &&& g nt
instance ArrowChoice (Mealy eff) where
left (Mealy st d f) = Mealy st d $ \nt -> left (f nt)
(Mealy ast ad f) +++ (Mealy bst bd g) = Mealy (ast <> bst) (combineDiagram Branch ad bd) $ \nt -> do
f nt +++ g nt
instance Functor (Mealy eff a) where
@@ -486,13 +494,29 @@ whenA predicate auto = arr (\a -> if predicate a then Left a else Right ()) >>>
filterA :: (a -> Bool) -> Mealy eff a (Either () a)
filterA f = arr $ \a -> bool (Left ()) (Right a) (f a)
-- filters events through if they match the predicate
filterEvent :: (ArrowChoice cat, Arrow cat) => (a -> Bool) -> Event a `cat` Event a
filterEvent predicate = proc ev -> do
case ev of
Event a | predicate a -> id -< ev
_ -> arr (const Tick) -< ()
thenA :: (ArrowChoice cat, Arrow cat) => cat a (Either b1 c) -> cat c (Either b1 b2) -> cat a (Either b1 b2)
thenA f g = f >>> arr Left ||| g
(>>|) :: (ArrowChoice cat, Arrow cat) => cat a (Either b1 c) -> cat c (Either b1 b2) -> cat a (Either b1 b2)
(>>|) = thenA
infixl 2 >>|
infixl 2 >>||
(>>||) :: (ArrowChoice cat, Arrow cat) => cat a (Event b) -> cat b c -> cat a (Event c)
f >>|| g = proc x -> do
e <- f -< x
case e of
Event e' -> g >>> arr Event -< e'
Tick -> arr (const Tick) -< ()
infixr 3 >>|
toEvent :: Mealy eff (Either () a) (Event a)
toEvent = arr (either (const Tick) Event)
+12 -7
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE OverloadedStrings #-}
module AFRP.Diagram where
import Data.Text (Text)
import qualified Text.Printf as Text
@@ -23,22 +24,26 @@ emptyDiagram :: DiagramC
emptyDiagram r = r Empty
renderF :: DiagramF (Maybe String) -> Maybe String
renderF :: DiagramF (Maybe T.Text) -> Maybe T.Text
renderF = \case
Empty -> Nothing
Step n x -> Just $ T.unpack n <> maybe "" (Text.printf "( %s )") x
Step n x -> Just $ n <> T.pack (maybe "" (Text.printf "( %s )") x)
Then (Just a) (Just b) -> Just $ a <> " -> " <> b
Then (Just a) _ -> Just a
Then _ b -> b
Both (Just a) (Just b) -> Just $ Text.printf "(%s, %s)" a b
Both (Just a) (Just b) -> Just $ T.pack $ Text.printf "(%s, %s)" a b
Both (Just a) _ -> Just a
Both _ b -> b
Branch (Just a) (Just b) -> Just $ Text.printf "(%s || %s)" a b
Branch (Just a) Nothing -> Just $ Text.printf "(%s || <skip>)" a
Branch (Just a) (Just b) -> Just $ T.pack $ Text.printf "(%s || %s)" a b
-- I'm leaving this here. This would be technically more correct (showing
-- the empty branch, ie. the full if-then-else, but often we care about a
-- more simplistic `when` style structure
-- Branch (Just a) Nothing -> Just $ T.pack $ Text.printf "(%s || <skip>)" a
Branch (Just a) Nothing -> Just $ T.pack $ Text.printf "%s" a
Branch _ b -> b
Merge (Just a) (Just b) -> Just $ Text.printf "(%s, %s)" a b
Merge (Just a) (Just b) -> Just $ T.pack $ Text.printf "(%s, %s)" a b
Merge (Just a) _ -> Just a
Merge _ b -> b
Compose (Just a) (Just b) -> Just $ Text.printf "(%s, %s)" a b
Compose (Just a) (Just b) -> Just $ T.pack $ Text.printf "(%s, %s)" a b
Compose (Just a) _ -> Just a
Compose _ b -> b
+9 -7
View File
@@ -5,7 +5,7 @@ module HomeAssistant.Controller.Bedroom where
import HomeAssistant.Controller
import qualified Data.Text as T
import AFRP (Event (..), lMerge, duration, edge)
import AFRP (Event (..), lMerge, duration, edge, filterEvent, named, (>>||))
import Data.Aeson (Value, object, (.=))
import Control.Arrow ((>>>), returnA, arr, Arrow (..))
import Data.Bool (bool)
@@ -134,14 +134,16 @@ drawer = entityBool "binary_sensor.bedroom_nightstand_drawer_sensor_masse_contac
>>> arr (fmap (bool Closed Open))
bedroomDrawerController :: HASS (Event Value) ()
bedroomDrawerController = proc x -> do
st <- drawer >>> traceEvent -< x
case st of
Event Open -> callService "drawer light on" (switchService [entity] True) -< ()
Event Closed -> callService "drawer light off" (switchService [entity] False) -< ()
_ -> returnA -< ()
bedroomDrawerController =
drawer >>>
(named "Drawer opened" lightOn <> named "Drawer closed" lightOff)
>>> arr (const ())
where
entity = EntityId "switch.bedroom_drawer_light_masse"
lightOff = filterEvent (== Closed)
>>|| callService "drawer light off" (switchService [entity] False)
lightOn = filterEvent (== Open)
>>|| callService "drawer light on" (switchService [entity] True)
door :: HASS (Event Value) (Event DoorState)
door = entityBool "binary_sensor.makuuhuone_ovi_contact"
+3
View File
@@ -48,6 +48,7 @@ import Text.Read (readMaybe)
import UnliftIO.Async
import Control.Monad.Catch (MonadCatch)
import Control.Exception.Annotated (checkpoint, Annotation (..))
import AFRP.Diagram (renderF)
step :: (MonadIO m) => FilePath -> T.Text -> UUID -> Auto m a b -> a -> m (b, Auto m a b)
step path name trace st a = do
@@ -122,6 +123,8 @@ defaultMain =
rrdtool <- fromMaybe "rrdtool" <$> liftIO (lookupEnv "HA_RRDTOOL")
metricsPort <- lookupPort
writerLimiter <- slidingWindowLimiter rateLimitMetrics 10 20
forM_ controllers $ \(Controller n m) ->
logLocM InfoS $ ls $ n <> ": " <> fromMaybe "" (runDiagramC m renderF)
let ents = foldMap (\(Controller _ m) -> entities m) controllers
workers =
[ ("reader", readerAction host 8123 token ents bus),