Closed ronyeh closed 2 years ago
Hi Ron,
this is the reason: 1) in order to remove the dependencies with Dot and Accidental the following functions were removed from note:
// Helper function to add a dot on a specific key
addDot(index: number): this {
const dot = new Dot();
dot.setDotShiftY(this.glyph.dot_shiftY);
this.dots++;
return this.addModifier(dot, index);
}
// Convenience method to add dot to all keys in note
addDotToAll(): this {
for (let i = 0; i < this.keys.length; ++i) {
this.addDot(i);
}
return this;
}
// Get all accidentals in the `ModifierContext`
getAccidentals(): Accidental[] {
return this.checkModifierContext().getMembers(Accidental.CATEGORY) as Accidental[];
}
// Get all dots in the `ModifierContext`
getDots(): Dot[] {
return this.checkModifierContext().getMembers(Dot.CATEGORY) as Dot[];
}
2) as a consequence the following functions were also removed to be consistent and this functions added only an addtional call in the stack
// Helper function to add an accidental to a key
addAccidental(index: number, accidental: Modifier): this {
return this.addModifier(accidental, index);
}
// Helper function to add an articulation to a key
addArticulation(index: number, articulation: Modifier): this {
return this.addModifier(articulation, index);
}
// Helper function to add an annotation to a key
addAnnotation(index: number, annotation: Modifier): this {
return this.addModifier(annotation, index);
}
3) you can see that the order of the parameters of the functions in 2) differ from the order in addModifier
one way or the other it was 100s of calls I unified to the order of 2) because the number of changes were less.
Hope this clarifies the reasoning behind. One way or the other it has an impact in the API.
OK thanks for explaining.
For the historical record, in version 3.0.9 there was an API design issue:
StaveNote
extends StemmableNote
StemmableNote
extends Note
StaveNote defines addModifier as:
addModifier(index, modifier)
https://unpkg.com/browse/vexflow@3.0.9/src/stavenote.js
Note defines addModifier with the parameters reversed:
addModifier(modifier, index = 0) {
https://unpkg.com/browse/vexflow@3.0.9/src/note.js
It looks like it has been this way since 1.2.6: https://unpkg.com/browse/vexflow@1.2.6/src/note.js https://unpkg.com/browse/vexflow@1.2.6/src/stavenote.js
So if we are trying to unify all the addXXX()
methods, there will be a breaking API change either way.
But since we are breaking APIs anyways.... Which one of the following seems better?
A) addXYZ(index, xyzInstance)
B) addXYZ(xyzInstance, index)
I actually think option B makes more intuitive sense. When I type addXYZ(, I usually want to provide a xyz object immediately afterward. Option B also allows us to make the index optional (default: 0) so we can omit the second parameter if we are just adding one modifier.
It looks like stave.ts also has a addModifier(modifier: StaveModifier, position?: number)
.
So at the moment, note.ts
and stave.ts
each have a addModifier()
method, but their arguments are reversed.
My vote would be to flip the argument order in note.ts, so that they are consistent with how they were in versions 1.2.6...3.0.9.
My vote would be to leave it as it is because there is higher use of addAccidental
, addAnnotation
and addArticulation
than addModifier
. I would also avoid to do the hundreds of changes again, at least manually :).
Now that we are changing this API I would also rename addModifer
in Stave
to addStaveModifier
.
Option B also allows us to make the index optional (default: 0) so we can omit the second parameter if we are just adding one modifier.
This seems like an important argument. In a lot of cases you wouldn't need to add indices when adding modifiers, it would just be a hassle for the dev using the API, always incrementing the index, etc.
higher use of
addAccidental
,addAnnotation
andaddArticulation
thanaddModifier
But you said these methods were removed?
If we go with the modifier instance first, index second, I'm happy to submit the PR for it.
... though I think I see what you mean now. If we consider the impact this breaking change will have on existing developers who are trying to upgrade to VexFlow 4.0 from 1.2.93, then they will have to do more work to fix their code (if they made heavy use of addAccidental, addAnnotation, addArticulation).
If we leave it as is, then developers who are upgrading only need to do a search and replace to rename calls to those three methods (addAccidental => addModifier, addAnnotation => addModifier, addArticulation => addModifier).
I'm still leaning towards having the modifier instance go first though. I think the Stave class has it right:
addModifier(modifier: StaveModifier, position?: number): this {
I turned on the circular reference tool, and it looks like those three methods didn't contribute to the circular references. They took a Modifier
object as a parameter, so it didn't add any links to chain of references.
// Helper function to add an accidental to a key
addAccidental(index: number, accidental: Modifier): this {
return this.addModifier(index, accidental);
}
// Helper function to add an articulation to a key
addArticulation(index: number, articulation: Modifier): this {
return this.addModifier(index, articulation);
}
// Helper function to add an annotation to a key
addAnnotation(index: number, annotation: Modifier): this {
return this.addModifier(index, annotation);
}
We could argue that those methods were not very useful.... :-) However, they do not cause circular references, even if we replaced Modifier
with the imported types Accidental
, Articulation
, and Annotation
.
Even though those look like references, those classes are just used as types, so they are erased when the TypeScript compiler processes it.
Before compilation:
// Helper function to add an accidental to a key
addAccidental(index: number, accidental: Accidental): this {
return this.addModifier(index, accidental);
}
// Helper function to add an articulation to a key
addArticulation(index: number, articulation: Articulation): this {
return this.addModifier(index, articulation);
}
// Helper function to add an annotation to a key
addAnnotation(index: number, annotation: Annotation): this {
return this.addModifier(index, annotation);
}
After compilation (vexflow-debug.js):
// Helper function to add an accidental to a key
addAccidental(index, accidental) {
return this.addModifier(index, accidental);
}
// Helper function to add an articulation to a key
addArticulation(index, articulation) {
return this.addModifier(index, articulation);
}
// Helper function to add an annotation to a key
addAnnotation(index, annotation) {
return this.addModifier(index, annotation);
}
Here was the block that was removed:
The following definitely DOES contribute to circular references:
getAccidentals(): Accidental[] {
return this.checkModifierContext().getMembers(Accidental.CATEGORY) as Accidental[];
}
This makes a hard reference to a static member inside the Accidental class (Accidental.CATEGORY
), which will not be erased by the TypeScript compiler & webpack.
Compiled code:
getAccidentals() {
return this.checkModifierContext().getMembers(_index__WEBPACK_IMPORTED_MODULE_0__.Accidental.CATEGORY);
}
Notice the type information Accidental[]
and as Accidental[]
was erased.
Summary:
Circular references are not always immediately problematic. If they break the runtime tests, like they did in December, then we should definitely diagnose and resolve them.
I think the typeguard methods and .CATEGORY approach definitely could be improved. The isCategory()
method uses instanceof
, which is one of the hard references that causes circular references.
Anyways, thanks for working on this @rvilarl ... I definitely learned something while trying to understand circular references better.
higher use of
addAccidental
,addAnnotation
andaddArticulation
thanaddModifier
But you said these methods were removed?
If we go with the modifier instance first, index second, I'm happy to submit the PR for it.
... though I think I see what you mean now. If we consider the impact this breaking change will have on existing developers who are trying to upgrade to VexFlow 4.0 from 1.2.93, then they will have to do more work to fix their code (if they made heavy use of addAccidental, addAnnotation, addArticulation).
If we leave it as is, then developers who are upgrading only need to do a search and replace to rename calls to those three methods (addAccidental => addModifier, addAnnotation => addModifier, addArticulation => addModifier).
This is what I meant, @sschmidTU In OSM I thought that the prefered approach was the one in addAccidental due to the comments stating "Vexflow made a mess with the addModifier signature that changes through each class so we just cast to any :(" This casting also introduced bugs because the parameters were in some instances in the order expected by addAccidental.
I'm still leaning towards having the modifier instance go first though. I think the Stave class has it right:
addModifier(modifier: StaveModifier, position?: number): this {
The problem with this
addModifier
inStave
is the name.StaveModifier
does not extendModifier
it extendsElement
as a consequeence I think thataddStaveModifier(staveModifier: StaveModifier, position?: number): this {
would be clearer.
I think it is a matter of taste, having index at the end allows to make it optional and having it at the beginning is more in line with the lists add functions or splice in arrays. In any case I suggest to look for an automatic refactoring tool if we change it (there are 762 references in VexFlow!) @0xfe which order do you prefer?
Either parameter order is fine with me, but I would prefer if the ordering is more consistent.
We can rename Stave's method to addStaveModifier(), but the parameter order there is still 1) object instance, then 2) index/position which is optional.
In Note, it's the reverse. Whichever one we choose in the end, I'd prefer if both method signatures took the same approach.
If we decide to flip Note.addModifier(index: number, modifier: Modifier)
=> Note.addModifier(modifier: Modifier, index: number=0)
, I can volunteer to fix the 700+ instances. Maybe there is some magical regex that will help. :-) :-]
This is partly my fault for starting this mess (sorry!), and I agree that it's super important to keep the the ordering consistent.
I think addModifier(modifier, ...)
is the ideal approach from an API design perspective. Typically when there's a doSomethingWithNOUN
method, the first parameter should always be a required NOUN
. It allows for optional parameters after.
So unless there are any major objections, I propose that's what we do.
OK, I can tackle this one after my gruntfile PR is done.
I will have to research some magic regex :-).
@ronyeh this can be closed now
Hi @rvilarl @0xfe
I'm just curious why the order of parameters in
Note.addModifier()
swapped places recently. I see mention of it in the recent circular dependencies PR, but I don't see how changing the order of the params fixes a circular dependency. Can you help me understand? Thanks!The swap happened here: https://github.com/0xfe/vexflow/pull/1279/commits/37a9fdea9a191bb15b444e2b517bae3cfb680c9c#diff-d6198cff69fcf66b5a1c2b4d4f8e2101a5e9c64dc24bae1a904d49abc2ae4f0cR538-R541
Here is what the method looked like in previous releases: GITHUB:
UNPKG:
The current version is: https://github.com/0xfe/vexflow/blob/b404958b192f84d65043f5697ac3c22de2d0d881/src/note.ts#L535
So as far as I can tell, since version 1.2.0, the modifier was first and the index second.
I know the comments indicate otherwise, and when I was porting this file I thought it was curious which "legacy" versions of VexFlow had the parameters swapped. I never bothered to check.
So if it has always been
addModifier(modifier, index)
, according to the old releases, then we should probably stick with it unless there's a great reason to swap now.