altitude: attach in DSL.Condition pattern-matches IR leaf constructors — couples DSL layer to IR internals #27

Closed
opened 2026-06-10 14:48:36 +02:00 by yorunikakeru · 1 comment
Owner

Problem

DSL.Condition.attach reaches through the IR domain constructors to attach an ObserveStrategy to each leaf condition:

attach (ProcessCondition (Process.ProcessRunning name _)) (ObserveStrategy s) =
    ProcessCondition $ Process.ProcessRunning name (Just s)
attach (SystemCondition (System.CpuLoad t _)) (ObserveStrategy s) =
    SystemCondition $ System.CpuLoad t (Just s)
attach (SystemCondition (System.BatteryBelow p _)) (ObserveStrategy s) =
    SystemCondition $ System.BatteryBelow p (Just s)
attach (SystemCondition (System.BatteryAbove p _)) (ObserveStrategy s) =
    SystemCondition $ System.BatteryAbove p (Just s)
attach (And l r) s = And (attach l s) (attach r s)
attach (Not c)   s = Not (attach c s)

Every new IR.Domain.Process.Condition or IR.Domain.System.Condition constructor forces a change to attach in the DSL layer — even if the strategy semantics are unchanged. The DSL layer should not need to know about domain-level constructor shapes.

Better approach

Push the attachStrategy responsibility into the IR layer via a typeclass or a function exposed by each domain module:

-- In IR.Domain.Process:
attachStrategy :: ObserveStrategy -> Condition -> Condition
attachStrategy s (ProcessRunning name _) = ProcessRunning name (Just s)

-- In IR.Domain.System:
attachStrategy :: ObserveStrategy -> Condition -> Condition
attachStrategy s (CpuLoad t _)      = CpuLoad t (Just s)
attachStrategy s (BatteryBelow p _) = BatteryBelow p (Just s)
attachStrategy s (BatteryAbove p _) = BatteryAbove p (Just s)

-- In IR.Condition (or DSL.Condition):
attach :: Condition -> ObserveStrategy -> Condition
attach (ProcessCondition c) s = ProcessCondition (Process.attachStrategy s c)
attach (SystemCondition c)  s = SystemCondition  (System.attachStrategy s c)
attach (And l r)            s = And (attach l s) (attach r s)
attach (Not c)              s = Not (attach c s)

Adding a new System.Condition constructor then requires only updating System.attachStrategy — a single-file change in the IR package, with -Wall enforcing exhaustiveness there.

## Problem `DSL.Condition.attach` reaches through the IR domain constructors to attach an `ObserveStrategy` to each leaf condition: ```haskell attach (ProcessCondition (Process.ProcessRunning name _)) (ObserveStrategy s) = ProcessCondition $ Process.ProcessRunning name (Just s) attach (SystemCondition (System.CpuLoad t _)) (ObserveStrategy s) = SystemCondition $ System.CpuLoad t (Just s) attach (SystemCondition (System.BatteryBelow p _)) (ObserveStrategy s) = SystemCondition $ System.BatteryBelow p (Just s) attach (SystemCondition (System.BatteryAbove p _)) (ObserveStrategy s) = SystemCondition $ System.BatteryAbove p (Just s) attach (And l r) s = And (attach l s) (attach r s) attach (Not c) s = Not (attach c s) ``` Every new `IR.Domain.Process.Condition` or `IR.Domain.System.Condition` constructor forces a change to `attach` in the DSL layer — even if the strategy semantics are unchanged. The DSL layer should not need to know about domain-level constructor shapes. ## Better approach Push the `attachStrategy` responsibility into the IR layer via a typeclass or a function exposed by each domain module: ```haskell -- In IR.Domain.Process: attachStrategy :: ObserveStrategy -> Condition -> Condition attachStrategy s (ProcessRunning name _) = ProcessRunning name (Just s) -- In IR.Domain.System: attachStrategy :: ObserveStrategy -> Condition -> Condition attachStrategy s (CpuLoad t _) = CpuLoad t (Just s) attachStrategy s (BatteryBelow p _) = BatteryBelow p (Just s) attachStrategy s (BatteryAbove p _) = BatteryAbove p (Just s) -- In IR.Condition (or DSL.Condition): attach :: Condition -> ObserveStrategy -> Condition attach (ProcessCondition c) s = ProcessCondition (Process.attachStrategy s c) attach (SystemCondition c) s = SystemCondition (System.attachStrategy s c) attach (And l r) s = And (attach l s) (attach r s) attach (Not c) s = Not (attach c s) ``` Adding a new `System.Condition` constructor then requires only updating `System.attachStrategy` — a single-file change in the IR package, with `-Wall` enforcing exhaustiveness there.
Author
Owner

Follow-up: make strategy conflicts an error instead of silent override

Discussed alongside this refactor: the current attach semantics silently replace any existing strategy on a leaf. Both "outer wins" (current) and "inner wins" (old <|> Just s) lose a strategy silently — e.g.:

processRunning "nginx" `via` poll 100 `via` poll 200
-- current: poll 100 silently lost
(cpuLoad 0.9 `via` poll 50) <&&> batteryBelow 20 `via` poll 500
-- current: poll 50 silently overwritten on the leaf

Decision: at this stage a conflict should be a DomainError, not a silent override. The DSL already returns Either DomainError everywhere, so via can surface it naturally:

-- DSL.Condition
via condition strategy = do
    c <- condition
    s <- strategy
    attach c s

attach :: Condition -> ObserveStrategy -> Either DomainError Condition

-- In each IR domain module (per this issue's refactor):
attachStrategy :: ObserveStrategy -> Condition -> Either DomainError Condition
attachStrategy s (ProcessRunning name Nothing)  = Right (ProcessRunning name (Just s))
attachStrategy _ (ProcessRunning _ (Just _))    = Left StrategyConflict

Implications:

  • New DomainError constructor (StrategyConflict or similar) lives in IR.Domain.Error → cross-repo change in frogos-ir, then repin in cabal.project.
  • Breaking change to documented via behavior ("replaces any existing strategy", "overriding any strategies previously set on individual leaves") — docstring, override tests, and CHANGELOG must be updated.
  • Composite conditions become strict: via over And/Not fails if any leaf already carries a strategy.

Worth doing in the same pass as this refactor: the per-leaf conflict check is exactly the code this issue moves into the IR layer, so doing both at once avoids touching the same lines twice.

## Follow-up: make strategy conflicts an error instead of silent override Discussed alongside this refactor: the current `attach` semantics silently *replace* any existing strategy on a leaf. Both "outer wins" (current) and "inner wins" (`old <|> Just s`) lose a strategy silently — e.g.: processRunning "nginx" `via` poll 100 `via` poll 200 -- current: poll 100 silently lost (cpuLoad 0.9 `via` poll 50) <&&> batteryBelow 20 `via` poll 500 -- current: poll 50 silently overwritten on the leaf Decision: at this stage a conflict should be a `DomainError`, not a silent override. The DSL already returns `Either DomainError` everywhere, so `via` can surface it naturally: -- DSL.Condition via condition strategy = do c <- condition s <- strategy attach c s attach :: Condition -> ObserveStrategy -> Either DomainError Condition -- In each IR domain module (per this issue's refactor): attachStrategy :: ObserveStrategy -> Condition -> Either DomainError Condition attachStrategy s (ProcessRunning name Nothing) = Right (ProcessRunning name (Just s)) attachStrategy _ (ProcessRunning _ (Just _)) = Left StrategyConflict Implications: - New `DomainError` constructor (`StrategyConflict` or similar) lives in `IR.Domain.Error` → cross-repo change in `frogos-ir`, then repin in `cabal.project`. - Breaking change to documented `via` behavior ("replaces any existing strategy", "overriding any strategies previously set on individual leaves") — docstring, override tests, and CHANGELOG must be updated. - Composite conditions become strict: `via` over `And`/`Not` fails if any leaf already carries a strategy. Worth doing in the same pass as this refactor: the per-leaf conflict check is exactly the code this issue moves into the IR layer, so doing both at once avoids touching the same lines twice.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
FrogOS/DSL#27
No description provided.