Class: HeadMusic::Style::Guides::CompositeGuide
- Inherits:
-
Object
- Object
- HeadMusic::Style::Guides::CompositeGuide
- Defined in:
- lib/head_music/style/guides/composite_guide.rb
Overview
Several guides graded together as one – a species is a melody guide and a harmony guide, and a student submits one line to be judged by both.
Stands in for a guide class wherever one is expected, the way Configured does, and is registered the same way: as an instance among the classes.
COMPOSES GRADES, NOT ITEMS. The tempting shape – one guide whose items are the union of its members’ – is wrong twice, and the second reason is the one that matters.
It cannot be built: a melody guide and its harmony partner both gate on MinimumNotes.with(3), GuideItem equality is by value, and Base.reject_duplicates refuses the union outright.
And it would undo the tier budgets. Those exist so that what a guide teaches does not thin out as inherited craft accumulates around it – see the comment in GuideAssessment. Merging first species puts nineteen primaries into a single phi^-1 budget and halves every taught rule’s share, which is that erosion arrived at from a different direction. The two levels also grade by different arithmetic on purpose: rules inside a rubric trade off against each other by weight, while a melody grade and a harmony grade must both hold.
So guide_items here is FOR DISPLAY, NOT FOR GRADING. It is deduplicated, and a fully graded composite therefore reports more item assessments than it has items – 27 against 26 for first species, because each member assesses the shared gate itself.
Instance Attribute Summary collapse
-
#guides ⇒ Object
readonly
Returns the value of attribute guides.
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
-
#assess(voice) ⇒ Object
-
#assess_items(voice) ⇒ Object
Every finding, flattened.
-
#categories ⇒ Object
-
#category ⇒ Object
A composite spans its members’ categories rather than claiming one.
-
#composite? ⇒ Boolean
-
#display_name ⇒ Object
-
#guide_items ⇒ Object
-
#hash ⇒ Object
-
#initialize(*guides) ⇒ CompositeGuide
constructor
Members resolved eagerly, for the reason Configured resolves its own: a bad member should raise here rather than mid-grading.
-
#inspect ⇒ Object
-
#instruction ⇒ Object
An unregistered composite borrows its members’ instructions rather than rendering a template no locale has.
-
#items_by_tier ⇒ Object
Deduplicated within each tier rather than across the whole list, so a rule a guide gates on and another grades on stays two entries – the distinction Base.reject_duplicates exists to protect.
-
#key ⇒ Object
Reverse lookup, not derivation, matching Configured: an unregistered composite has no key rather than claiming one that resolves elsewhere.
-
#name ⇒ Object
(also: #to_s)
-
#reject_bad_members! ⇒ Object
private
Nesting is refused rather than left unspecified.
Constructor Details
#initialize(*guides) ⇒ CompositeGuide
Members resolved eagerly, for the reason Configured resolves its own: a bad member should raise here rather than mid-grading.
Nothing here may ask for key, display_name, or instruction. All three read Guide::REGISTRY, which does not exist yet while the registry’s second pass is constructing composites, and reaching for it fails on require.
39 40 41 42 43 44 45 46 47 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 39 def initialize(*guides) @guides = guides.flatten.freeze reject_bad_members! # Both memoize, and this object freezes: anything lazy must be resolved here # or its first reader raises FrozenError. guide_items categories freeze end |
Instance Attribute Details
#guides ⇒ Object (readonly)
Returns the value of attribute guides.
31 32 33 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 31 def guides @guides end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
103 104 105 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 103 def ==(other) other.is_a?(self.class) && guides == other.guides end |
#assess(voice) ⇒ Object
49 50 51 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 49 def assess(voice) HeadMusic::Style::CompositeAssessment.new(self, voice) end |
#assess_items(voice) ⇒ Object
Every finding, flattened. Deliberately not what the grade is computed from – GuideAssessment refuses a composite, so this cannot silently become the grading path.
56 57 58 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 56 def assess_items(voice) guides.flat_map { |guide| guide.assess_items(voice) } end |
#categories ⇒ Object
83 84 85 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 83 def categories @categories ||= guides.flat_map(&:categories).uniq.freeze end |
#category ⇒ Object
A composite spans its members’ categories rather than claiming one. Consumers grouping the registry by category get a nil bucket; categories is the answer.
79 80 81 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 79 def category nil end |
#composite? ⇒ Boolean
73 74 75 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 73 def composite? true end |
#display_name ⇒ Object
93 94 95 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 93 def display_name key ? HeadMusic::Style::Guide.display_name_for(key) : guides.map(&:display_name).join(" + ") end |
#guide_items ⇒ Object
60 61 62 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 60 def guide_items @guide_items ||= HeadMusic::Style::Guides::Base::TIERS.flat_map { |tier| items_by_tier[tier] }.freeze end |
#hash ⇒ Object
108 109 110 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 108 def hash guides.hash end |
#inspect ⇒ Object
117 118 119 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 117 def inspect "#{self.class.name}(#{guides.map { |guide| guide.key || guide.name }.join(", ")})" end |
#instruction ⇒ Object
An unregistered composite borrows its members’ instructions rather than rendering a template no locale has.
99 100 101 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 99 def instruction key ? HeadMusic::Style::Guide.instruction_for(key) : guides.map(&:instruction).join(" ") end |
#items_by_tier ⇒ Object
Deduplicated within each tier rather than across the whole list, so a rule a guide gates on and another grades on stays two entries – the distinction Base.reject_duplicates exists to protect.
67 68 69 70 71 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 67 def items_by_tier @items_by_tier ||= HeadMusic::Style::Guides::Base::TIERS.to_h { |tier| [tier, guides.flat_map { |guide| guide.items_by_tier[tier] }.uniq.freeze] }.freeze end |
#key ⇒ Object
Reverse lookup, not derivation, matching Configured: an unregistered composite has no key rather than claiming one that resolves elsewhere.
89 90 91 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 89 def key HeadMusic::Style::Guide.key_for(self) end |
#name ⇒ Object Also known as: to_s
112 113 114 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 112 def name guides.map(&:name).join(" + ") end |
#reject_bad_members! ⇒ Object (private)
Nesting is refused rather than left unspecified. At depth two, assessments
would have to mean both the members that fitness divides by and the leaves a
consumer walks, and those stop being the same list.
126 127 128 129 130 131 132 133 |
# File 'lib/head_music/style/guides/composite_guide.rb', line 126 def reject_bad_members! raise ArgumentError, "a composite grades more than one guide" if guides.length < 2 nested = guides.select { |guide| guide.respond_to?(:composite?) && guide.composite? } return if nested.empty? raise ArgumentError, "a composite cannot hold another composite: #{nested.map(&:inspect).join(", ")}" end |