131 lines
6.2 KiB
Markdown
131 lines
6.2 KiB
Markdown
# 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:
|
||
1. AFRP (Mealy) internals — generic FRP machinery.
|
||
2. 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`, `callService`
|
||
- `HomeAssistant.Controller.Entity` — entity parsers
|
||
- `HomeAssistant.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(..)`, `eff`
|
||
- `Event(..)`, `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`, `step`
|
||
- `CallIdGen`, `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:
|
||
|
||
```haskell
|
||
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:
|
||
|
||
1. **`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.hs` import (`MyLib` -> `HomeAssistant.Runtime`).
|
||
- Update cabal `exposed-modules`.
|
||
- Build still warns on remaining items.
|
||
|
||
2. **`Fix warnings`**
|
||
- Delete `numericDirection`/`Direction` and dead `where` clauses (`isEntity` in `entityChangeEvent`, `state` in `entityRead`/`entityBool`).
|
||
- Add `_ -> False` catch-all to `toBool`.
|
||
- Implement real `hassEval` (uncommented `wsCallService`) + add `dryRunHassEval`; export both from `HomeAssistant.Runtime`.
|
||
- Wire `app` to `dryRunHassEval` to preserve current behavior.
|
||
- Build clean (no warnings).
|
||
|
||
## Verification
|
||
|
||
After each commit:
|
||
- `cabal build` succeeds.
|
||
- After commit 2: `cabal build --ghc-options="-Wall -Wincomplete-uni-patterns -Wincomplete-record-updates"` produces zero warnings.
|
||
- `app/Main.hs` still compiles and imports `defaultMain` from its new home.
|
||
|
||
## Non-goals
|
||
|
||
- Splitting `HomeAssistant.Controller` into Effect/Entity/Controller sub-modules (deferred until real controllers exist).
|
||
- Adding tests (test suite is a placeholder; out of scope).
|
||
- Changing `app` to call real `hassEval` (stays on `dryRunHassEval` to preserve behavior).
|
||
- Any behavioral changes beyond warning fixes and the dormant `wsCallService` activation (which is itself inert until `app` switches to `hassEval`).
|