6.2 KiB
Module split & warning cleanup for MyLib.hs
Date: 2026-08-20 Status: Approved (pending spec review)
Goal
Split src/MyLib.hs (352 lines, monolithic) into vertically-separated modules and fix all compiler warnings, committing between every change.
Two concerns were named upfront:
- AFRP (Mealy) internals — generic FRP machinery.
- Home Assistant business logic.
A third concern emerged during exploration:
3. IO runtime — websocket client, effect interpreter, defaultMain.
Module layout
Three vertical modules with hierarchical naming. MyLib is removed; app/Main.hs and the cabal exposed-modules are updated.
| Module | Concern | Depends on |
|---|---|---|
AFRP |
Generic Mealy/Event machinery — no HA knowledge | base, time |
HomeAssistant.Controller |
HA domain: effect type, entity parsers, controllers | AFRP, aeson, lens |
HomeAssistant.Runtime |
IO interpreter, websocket client, defaultMain |
both, websockets |
Naming rationale
AFRP lives at the top level (not HomeAssistant.AFRP) because the code is generic FRP machinery with zero HA references; nesting it under HomeAssistant.* would misrepresent it. It stays in-project per the user's decision, so a short top-level name is fine.
Future direction (deferred)
When real controllers exist, split HomeAssistant.Controller further into sub-modules:
HomeAssistant.Controller.Effect—HASSEff,Service,HASS,callServiceHomeAssistant.Controller.Entity— entity parsersHomeAssistant.Controller(or.Controllers) — actual controllers (lightController, etc.)
Deferred now because there's only one proof-of-concept controller.
Module contents
AFRP (from MyLib.hs lines 29–91, 99–173)
Exports:
Mealy(..),effEvent(..),hold,events,switch- accumulators:
preMapAccum,preMapAccumUTCTime,mapAccum,mapAccumUTCTime changes,whenA,filterA,thenA,(>>|),toEvent- instances come along with
Mealy(..)
HomeAssistant.Controller (from lines 32–42, 50–53, 175–256)
Exports:
Service(..),HASSEff(..),HASS,callService- entity helpers:
entityChangeEvent,entityChangeEvent',entityRead,entityRead',entityBool,entityBool' - domain types/values:
Ruuvi(..),ruuvi,ruuviTemperatures,ruuviPressures,DoorState(..),door,light,lightController
HomeAssistant.Runtime (from lines 259–268, 270–352)
Exports:
defaultMain,app,stepCallIdGen,mkCallIdGen,hassEval,dryRunHassEval,receiveJSON,wsCallService
Warning fixes
Per-warning, applied during the split-then-fix commit sequence:
| Warning (line) | Fix |
|---|---|
numericDirection (190) + Direction/Increase/Decrease/Steady (187) — unused, no sig |
Delete. True experiment; used nowhere. |
entityChangeEvent (224), entityRead' (236), entityRead (249) — unused top-binds |
Export from HomeAssistant.Controller. Useful Either/Event-returning entity helpers the others build on; the split's export lists make them used. |
isEntity (227) — unused local in entityChangeEvent |
Delete. entityChangeEvent delegates to entityChangeEvent' >>> toEvent; its isEntity is a dead duplicate. |
state (251) in entityRead, state (256) in entityBool — unused locals |
Delete. Both functions delegate to their '-primed versions; the where clauses are dead duplicates. |
toBool (244) — non-exhaustive ("on"/"off" only) |
Add catch-all _ -> False. Conservative: treat unknown HA state as off rather than crashing. |
wsCallService (327) + conn (346) — unused, paired |
See option C below. |
wsCallService / conn — option C (chosen)
The commented-out wsCallService conn ... call on line 350 made conn unused. Rather than uncomment (behavior change) or paper over with _conn (dishonest), expose both behaviors as named, exported functions:
hassEval :: CallIdGen -> WS.Connection -> HASSEff a -> IO a
hassEval gen conn = \case
CallService x -> do
callId <- generateCallId gen
wsCallService conn callId (serviceDomain x) (serviceName x) (serviceTarget x)
Pure a -> pure a
dryRunHassEval :: CallIdGen -> HASSEff a -> IO a
dryRunHassEval gen = \case
CallService x -> do
callId <- generateCallId gen
print (callId, x)
Pure a -> pure a
Both conn and wsCallService become used by the real hassEval; the current print-only behavior lives in dryRunHassEval. app wires up dryRunHassEval to preserve current runtime behavior — flipping to real hassEval later is a one-word change.
Commit sequence
Two commits, each building cleanly:
-
Split MyLib into AFRP, Controller, Runtime- Mechanical move of code into
src/AFRP.hs,src/HomeAssistant/Controller.hs,src/HomeAssistant/Runtime.hs. - Proper export lists (which naturally exports the "useful but unused" entity helpers, resolving those three unused-binding warnings).
- Delete
src/MyLib.hs. - Update
app/Main.hsimport (MyLib->HomeAssistant.Runtime). - Update cabal
exposed-modules. - Build still warns on remaining items.
- Mechanical move of code into
-
Fix warnings- Delete
numericDirection/Directionand deadwhereclauses (isEntityinentityChangeEvent,stateinentityRead/entityBool). - Add
_ -> Falsecatch-all totoBool. - Implement real
hassEval(uncommentedwsCallService) + adddryRunHassEval; export both fromHomeAssistant.Runtime. - Wire
apptodryRunHassEvalto preserve current behavior. - Build clean (no warnings).
- Delete
Verification
After each commit:
cabal buildsucceeds.- After commit 2:
cabal build --ghc-options="-Wall -Wincomplete-uni-patterns -Wincomplete-record-updates"produces zero warnings. app/Main.hsstill compiles and importsdefaultMainfrom its new home.
Non-goals
- Splitting
HomeAssistant.Controllerinto Effect/Entity/Controller sub-modules (deferred until real controllers exist). - Adding tests (test suite is a placeholder; out of scope).
- Changing
appto call realhassEval(stays ondryRunHassEvalto preserve behavior). - Any behavioral changes beyond warning fixes and the dormant
wsCallServiceactivation (which is itself inert untilappswitches tohassEval).