elm-lang / elm-package

Command line tool to share Elm libraries

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Changing a private type to a type alias shouldn't be a major change

gampleman opened this issue · comments

If I publish this module:

module Foo exposing (Bar, baz)

type Bar = Bar (List Float)

baz : Bar -> Float
baz (Bar l) = 
     List.reduce (+) 0 l

Then I change it to:

module Foo exposing (Bar, baz)

type alias Bar = List Float

baz : Bar -> Float
baz l = 
     List.reduce (+) 0 l

elm package will indicate that this is a MAJOR change. However, this doesn't seem right as none of the code using the package will break as the constructor wasn't exposed and so all of the values must have been treated as a black box until now.


Aside about how this happened in the real world

I had a type called Curve in elm-visualization which a class of functions could consume to compute smooth lines between. The library provides a few of those, but I always wanted the user to be able to use their own. These could be used in several different scenarios, so I wasn't sure if the type would need to have cases for these scenarios or if all the scenarios could be implemented in a generic way. So I decided to not expose the constructors and punt on the decision until I had more time to investigate the problem. Now that it has become clear that indeed all of the scenarios can be represented as a simple list of coordinates, I would like to relax this constraint and simply allow these functions to operate on the laxer type.

Thanks for the issue! Make sure it satisfies this checklist. My human colleagues will appreciate it!

Here is what to expect next, and if anyone wants to comment, keep these things in mind.

A counter argument: Clients of version A of your package, where the type was hidden, may rely on some invariants that the type guarantees, but which cannot anymore be taken for granted when using version B of your package. Because even if version B of your package still preserves these invariants, some completely unrelated package could mess them up. That loss of the invariant seems a rather radical API change.

A more concrete example: Say your package provides this type Bar as one that always represents a list of even length. I am using your package and it is very important to me that the lists are always of even length. With version A of your package, I can be sure that will be the case. Even if I pass some Bar values to some unrelated package EvilPackage and get some Bar values back from that package, they will still be of even length. Now you publish version B of your package, where the type is not anymore private. Now suddenly EvilPackage can release a new patch release version that uses that new freedom of version B of your package, and returns odd length lists. If you haven't indicated a major version change with your release of version B, then my own code, on the next deploy, will silently use your version B together with the patch release of EvilPackage, and will suddenly break in bad ways. And I never got a warning that I may want to investigate an API change.

But then again I myself can decide that the invariant shouldn't be there and release a patch version that breaks the invariant without any issues.

Furthermore, the code that actually translates Bar to something useful to your code could still enforce the invariants (i.e. it could drop elements if they were odd for example).

Finally, if my code enforces invariants about the content of its types, than surely changing the type representation from what I control to something I don't would be a pretty bad idea from that point of view.

What I'm trying to say is that the way elm package enforces semver is about code breaking in the "wont compile" sense. You are still free to break other people's code in the "it's buggy now" sense. The case you present is firmly in the second category, which seems impossible to prevent anyway.

Is "the condition for semver in Elm is exactly and only about 'still compiles'" documented somewhere?

And I do think that the case I presented is not the same as "I introduced a bug in a patch release". Because that would be an issue just between me and you. But opening up the type brings a third person into the picture that can introduce bugs into my code.

My point is: When I hypothetically started to use your package version A, and looked at the API, I saw that the type Bar is controlled exclusively by you. Then with version B it silently switched to "now it is not controlled by anybody anymore". The difference between "type that is controlled by package UsefulStuff" and "type that is not anymore controlled by any specific package" is an API difference to me.

But yes, one can take the "it's only about whether stuff still compiles" standpoint.

(Aside: What if one said "it's about whether stuff that previously compiled still compiles but also stuff that previously did not compile does not suddenly start compiling"? Would that latter condition be compatible with current semver criteria for Elm? It seems a useful perspective in its own right, and it would explain why changing a private type to a non-private type requires a major version bump.)