-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add increment methods #15
Conversation
This change turns the versions into mutable instances. Can you share some detail on your use case? |
yes indeed. I use it to bump my package automatically using one verb of |
yeah, the change to mutability is a big loss. versions are value objects; if you need to change one, make a new one. plus, when #11 happens, this just won't work at all anymore. if, instead, the methods returned a new |
@sdboyer Can you explain why the change to immutability is a big deal? In a practical real world use sense. You say it's a "big loss" but that needs to be articulated. |
Sure. There's a semantics reason, and a performance reason. The performance reason is slightly indirect - basically, #11 needs to happen (I just added a more detailed explanation of the performance issues), and if The semantics is a less clear-cut argument. Right now, even though pointer receivers are required for the methods, there are no methods that allow change. This makes them, in practice, immutable. This is lovely, because it means I can pass a version off to some function I don't directly control (e.g., an injected instance of an interface that takes a Adding these mutability methods takes away that guarantee. With that guarantee gone, I now have to code defensively: if I need to call any code that I'm not absolutely sure doesn't use these proposed methods, I first have to copy the version value into a new pointer address, then make the call. At least, I have to do that if I know that that version comes out of a sorted array...which, wait, now I have to keep track of that TOO? This becomes a big extra headache if you're trying to make a system in with real guarantees - that is, in which certain kinds of bad things literally can't happen. To make matters worse, this defensive copying is explicitly about keeping the callee from being able to modify the real Ordinarily, that's the sort of information I'd encode with function signatures: if I call a function with my Perhaps most importantly, there's very little expressivity lost by making these methods return a new version, rather than modifying the existing one. // As-is. No point in checking return because it tells us nothing, the method cannot fail
v.IncMajor()
// But if the signature is changed so a new version is returned instead:
func (v *Version) IncMajor() *Version { ... }
// Then the use of it becomes:
v = v.IncMajor() I'm not sure what Note that in my proposed form, it technically IS still mutable in a way that would propagate back to the "caller": func SomeFuncWhereTheCallerDoesntWantAChange(v *Version) {
// This would actually change the value at the pointer address,
// making the caller rather unhappy
*v = *v.IncMajor()
// But this issue exists today, so it's no real change
*v, _ = semver.NewVersion("1.0.0")
} I'm banking on #11 going in to 2.x, at least, which will make that possibility go away. |
Hi, thanks for all those valuable explanations @sdboyer In a straight way, what would fit you more ? I suggest to set a signature which is 2.x compatible by now. func (v *Version) Inc(how string) (Version, error){}
func (v *Version) IncPatch() (Version, error){}
func (v *Version) IncMinor() (Version, error){}
func (v *Version) IncMajor() (Version, error){}
func (v *Version) SetPrerelease(metadata string) (Version, error){}
func (v *Version) SetMetadata(metadata string) (Version, error){}
type IncorrectIncrementVerb error
type IncorrectMetadataValue error
type IncorrectPrereleaseValue error
IMHO, i m more concerned by |
Sorry, yeah, I should have actually made the proposal concrete. Here's what I would propose - pretty close to what you have: func (v *Version) IncPatch() *Version {}
func (v *Version) IncMinor() *Version {}
func (v *Version) IncMajor() *Version {}
func (v *Version) SetPrerelease(metadata string) (*Version, error) {}
func (v *Version) SetMetadata(metadata string) (*Version, error) {}
type IncorrectMetadataValue error
type IncorrectPrereleaseValue error This drops the error return from As you said,
Agreed, this is the tricky one. I think it's the kind of thing we could also easily implement later, without much harm. But if we have a notion of incrementing prereleases, then we have to internally define the ordering relationship within prereleases, and have that extend to all the other order-sensitive code in the lib.
We've let the 2.x branch sit for a while, but in my mind at least, the big thing blocking a 2.x release is #11. I think it's fine to have this change go in, also pull it up to 2.x, and then when #11 happens we just change all the pointer receivers and returns to values. |
We should drop There's a question of decrement. Why should there be a case for incrementing and not decrementing? |
Hah. Good point. So: func (v *Version) IncPatch() *Version {}
func (v *Version) DecPatch() *Version {}
func (v *Version) IncMinor() *Version {}
func (v *Version) DecMinor() *Version {}
func (v *Version) IncMajor() *Version {}
func (v *Version) DecMajor() *Version {}
func (v *Version) SetPrerelease(metadata string) (*Version, error) {}
func (v *Version) SetMetadata(metadata string) (*Version, error) {}
type IncorrectMetadataValue error
type IncorrectPrereleaseValue error ? |
mhhhhh. Decrement a version. What should happen if you are 1.0.0 and would like to go down ? 0.0.9999999999999999999999999999999999999 ? I guess you never really decrement a version, rather than restore an existing one, which means you would do I m not sure why It returns a Version pointer ? I d rather return value as the instance must remain immutable. func (v *Version) IncPatch() Version {}
func (v *Version) IncMinor() Version {}
func (v *Version) IncMajor() Version {}
func (v *Version) SetPrerelease(metadata string) (Version, error) {}
func (v *Version) SetMetadata(metadata string) (Version, error) {}
type IncorrectMetadataValue error
type IncorrectPrereleaseValue error |
hmm hmm hmm
maybe this is mitigated by having three separate methods: if you have Having these semantics would also sorta strengthen the case for #19, I think.
I suspect it would be, confusing, indeed :) Which is why I was only really envisioning this as a temporary measure - that #11 has to happen before this API would really make sense. The problem with not returning a ...though, come to think of it, I guess there's no real reason we have to use pointer receivers for new methods. We could just do this: func (v Version) IncPatch() Version {}
func (v Version) IncMinor() Version {}
func (v Version) IncMajor() Version {}
func (v Version) SetPrerelease(metadata string) (Version, error) {}
func (v Version) SetMetadata(metadata string) (Version, error) {}
type IncorrectMetadataValue error
type IncorrectPrereleaseValue error And then the caller can take the address of the returned value, if they need the other methods. |
Oh yeah i guess you ve right, being still learning go, that sort of things are not yet straight to me. Yet i suspect that using value as context in Ahm, when is 2.x released ? =) |
ah, well, welcome! 😄 totally reasonable thing to not have quite straight yet.
not quite - that's part of why I suggested it. see the spec re: method sets: if you have a variable of type // v is a *Version
v := semver.NewVersion("1.0.0")
// This assigns the value returned from IncMinor() to the address from our original
// pointer, v, so v is now 1.1.0
*v = v.IncMinor()
v2 := v.IncMajor()
// v2 is a non-pointer Version, containing 2.1.0. Original value at pointer address
// is unchanged. However, none of the methods requiring a pointer are accessible,
// though our new ones are:
v2.Major() // method not accessible
v2 = v2.IncPatch() // works, v2 is now 2.1.1
v3 := &v.IncMajor()
// v3 is a new pointer to a new address, separate from v's, and has access to all
// the normal methods. So, this works:
v3.Major() So, it's still a little awkward, but at least we have our immutability guarantees encoded in the type signature. I think it'd maybe be fine to leave this for 2.x, though, with the expectation that it's only tagged for release once #11 goes in. At that point, everything's consistently using a value receiver, so there's no headache at all. Gives me some motivation to come back and wrap up things here, so we can get that tagged release out :) |
There it is! |
vNext.pre = "" | ||
vNext.patch = 0 | ||
vNext.minor = v.minor+1 | ||
vNext.original = vNext.String() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not safe to to try to preserve the original string, as it now describes the old value, not the current one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don t get your point.
After a call to Inc*, NewVersion.original is set to NewVersion.String.
It will reflect the most recent changes as if NewVersion was called.
In other words
v, _ := NewVersion("1.2.3")
vv := v.IncPatch()
fmt.Println(vv.Original()) // => 1.2.4
fmt.Println(v.Original()) // => 1.2.3
BTW,
About original
, I don t get the point of keeping it. Neither its intent.
It is the same thing in constraints.go
with the orig
field.
They exists, but they are unused within the lib.
(moving discussion out of outdated line comments)
Sorry, you're totally right - I misread the code. You do handle it correctly. However...(see below)
The purpose of it is being able to precisely recreate the user's input, given that there are multiple ways a given version or constraint could be expressed in the input string. That is, On further reflecting on these reasons, I think that it'd actually be better to either not populate the Seem sane? |
Works for me. Will do another pass later to sniff it and and keep the original v prefix accordingly. |
one stupid q i have here, the SemverRegexp won t accept I d suggest to change this if you don t foresee any problems. |
|
I keep trying ; ) Strongly disagree. The user that will face that error message It s really not obvious for that kind of end user, at best he ll think the lib he is consuming is dumb / buggy, leaving it with a bad impression of it, which the lib does not deserve at all. |
Either way, it's orthogonal to the issue we're dealing with here. Probably best to open a new issue. |
good to go, i hope |
hi @sdboyer @mattfarina this change fixes increments of prereleased version number. I also reviewed some comments here and there. |
@@ -129,6 +137,95 @@ func (v *Version) Metadata() string { | |||
return v.metadata | |||
} | |||
|
|||
// OrignalVPrefix returns the original 'v' prefix if any. | |||
func (v *Version) OrignalVPrefix() string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is OrignalVPrefix
an exported function? It only appears to be needed internally.
Note, I'd like to merge this soon and release. |
e0e1160
to
d4c6b7b
Compare
|
@mh-cbon Thanks for the contribution. A release will be coming along shortly. |
In Masterminds#11 and Masterminds#15 there were some good arguments made in favor of changing the API to use value receivers and function arguments. While Masterminds/semver apparently didn't adopt this breaking API change, this is an attempt to try it out in a hard fork.
In Masterminds#11 and Masterminds#15 there were some good arguments made in favor of changing the API to use value receivers and function arguments. While Masterminds/semver apparently didn't adopt this breaking API change, this is an attempt to try it out in a hard fork.
No description provided.