Harmonia.Graded
- Package
- purescript-harmonia
- Repository
- afcondon/purescript-harmonia
Harmonia.Graded — grade-gated harmonic operations, honest about partiality.
Two type classes over anything harmonic:
Harmonic—grade(what leverage does this afford?) andtranspose(the always-available floor; never blames).Reflavourable— the operations that consult the reading and may fail to apply:modulate(needs a key) andreflavour(needs a diatonic reading). They are total functions — you can always call them — but each returns aGraded, carrying both a result and any blame.
This is the design inversion made concrete. In code we make illegal states
unrepresentable; a chord that has left its scale is not illegal, it is
expressive, so we make it representable and let the operation report where
its reading ran out. Building it against the real types settled a design
question left open in the sketch: every operation is total-with-
annotations — the input always survives as a fallback (worst case, passed
through untouched) — so the honest result shape is a value carrying blame,
not a These with an uninhabited "nothing came back" case. The blame is
the leverage: Both-style partial results force the caller to see the
boundaries rather than silently getting a mangled chord.
The classes instance for both a single Anchor and a Phrase (a sequence
of readings) — the fractal the design turns on: a progression is the same
kind of object as a chord, one level up, and its grade is emergent (the
meet of its members in v1), not a property of any single chord.
The "lock" from the design discussion turns out to be a value, not a
class: every Reflavourable thing may attempt every operation; the grade
decides which succeed and which come back blamed. Locked = high grade,
unlocked = low grade — checked at the value level, exactly so the illegal
move stays representable.
Pure Prelude/Data.*; builds unchanged on JS and purerl.
#BlameReason Source
data BlameReasonWhy a chord could not follow an operation. Closed, and richer than a bare reference on purpose: the reason is exactly what a renderer needs to mark the boundary (a passed-through chord in a reflavoured phrase, say) — but the choice of how to mark it stays with the caller.
Constructors
Instances
#Blame Source
type Blame = { index :: Int, reason :: BlameReason }A single blame: which position (0 for a lone chord; the phrase index for a
member of a Phrase) and why.
#Graded Source
newtype Graded aThe outcome of a grade-gated operation: a value plus the blame accrued applying it. Empty blame = a clean, fully-applied result; non-empty blame = a partial result whose listed positions were passed through untouched.
Constructors
Instances
#blamed Source
blamed :: forall a. Int -> BlameReason -> a -> Graded aA blamed result — the value (typically the input, unchanged) plus one reason it could not be transformed at the given position.
#Reflavourable Source
class (Harmonic a) <= Reflavourable a whereOperations that consult the reading and may not apply. modulate moves the
tonic (needs a key); reflavour re-reads the same degree in a new mode
(needs a diatonic reading). Both return Graded, blaming what they cannot
move.
Members
Instances
#Phrase Source
newtype PhraseAn ordered sequence of chord readings. Its grade is emergent: in v1 the meet of its members, so a single foreign chord drops the whole phrase to what all of it can uniformly do. (The intended growth is to absorb subordinate chromaticism — a secondary dominant read against a phrase degree shouldn't lower the phrase — which is why this is the meet for now, with a clearly-marked seam rather than a claim that the meet is the last word.)