1. 19
  1. 7

    Elm enforces packages like this and so there are no exceptions. This is the way to do it and this idea should spread imo.

    The problem with adding OOP with Moose to Perl, types to Ruby, contracts to Python or any other retroactive guarantee is that it creates a red/blue ecosystem. We could instead use Pact to test and verify behavior of messages while not caring about a red/blue ecosystem. But Pact’s approach is heavy on network messages and not internal memory messaging so it might not work for all situations.

    1. 3

      I had no idea that Elm ties major version numbers to API signatures. Fascinating. It sounds like the implication is that backwards-incompatibility should always be accompanied by an API-level change: that would be a quite narrow definition of backwards compatibility. On the other end of the spectrum, we have Hyrum’s Law, and a classic xkcd. But however you interpret it, I’d agree that Elm’s decision to impose uniformity on it is admirable.

      Which I suppose brings us to red/blue ecosystems: indeed, most languages have packages that vary in their versioning schemes. I would like to think that much (but not all) of the benefit of adopting Contractual SemVer is realized even if you only wrote natural language contracts, and didn’t use any 3rd party contract library at all. (clients understand what behaviors they can depend on, and providers understand when to change what numbers) Again, though, it’s clearly all much better if a single version scheme is adopted across the platform.

      1. 3

        I am not sure how similar Elm is to Haskell, but Haskell types (including function signatures) are usually more than just data types, and tend to correspond to “contracts.” (This kind of idea has been recently widely adopted in other languages too, in the name of making illegal states unrepresentable, however IMHO it’s more norm and further advanced in pure functional languages.)

        1. 1

          Oh! Have you seen Liquid Haskell? IMO, this gets even closer to the expressivity of contracts.

          1. 1

            I might have heard of its name once or twice, but I’ve never tried to take a look on it. It looks really cool! If I understand correctly, it’s sort of proof assistant, right?

            1. 1

              I know it’s focused on proving properties of programs, perhaps unlike a general theorem prover like Coq.

              I think refinement types are ~close to dependent types (I only sort of follow this discussion about the differences). As a user of the type system, I find refinements types way more grok-able.

      2. 1

        I think Pact uses contracts in a different way from design-by-contract contracts.

        1. 3

          You’re correct (hi, I wrote the initial FFI for Pact’s Rust reference implementation!). Pact describes a “contract” between a provider and a consumer. A “contract” in Pact is a set of interactions. If you’re dealing with HTTP APIs, then the interaction defines an expected request and a minimal expected response. If you’re dealing with messages, the interaction describes the minimal parts of the message the consumer expects from the provider. This description of interactions is then used to test both the consumer and the provider.

          For the consumer, Pact assumes the provider returns the expected response to a request, and tests whether the consumer correctly 1) generates the expected request, and 2) handles the assumed response.

          For the provider, Pact assumes the consumer is sending the expected requests, and tests whether the provider returns data that is consistent with the minimal data expected for the response.

          In either case, Pact basically lets you define the flow of interactions, and it then creates tests which mock out the “other side” of the side being tested, so that you 1) test both consumers and providers, and 2) neither relies on having a live instance of the other. Interactions are also isolated, so there aren’t dependencies where one interaction establishes state on the provider side which needs to be used by a subsequent interaction. Instead, interactions may be set up to assume specific provider state up-front.

          At any rate, all of this is in the end about generating tests, not performing formal verification of consistency with some rigorously defined policy in a static way, or validating pre- and post- conditions a la contracts in something like Racket. Same word, different meaning.

          Edit: Clarified my contribution to the Pact project.

          1. 1

            Yeah, Pact in theory not coupled to HTTP. https://github.com/reevoo/pact-messages is in-memory. Same paradigm. This ruby gem is out of date though, I’d like to bring this up in their Slack. I wonder if there’s some internal API to use. I’ve looked, can’t get it.

            Are contracts in Racket only in Racket?

            The cool thing about Pact is that is works for mobile, web, desktop, any language that has bindings. Just put it in your CI pipeline and you get a can-i-deploy check. You just need to be honest and put in the work of writing the tests like other testing levels. It integrates really well.

      3. 6

        Design-by-contract is new to me here, but I generally like this idea. Stumbling on the ~concrete examples in https://crosshair.readthedocs.io/en/latest/case_studies.html#contractual-semver also helped. The question of how to set a sane version constraint on a dependency is a nice synecdoche for the uncertainty/unreliability folded into best-human-effort SemVer.

        It rhymes a little with two ideas I had several years back while I was picking at a testing framework ~inspired by working a little with the Jasmine BDD framework for JS.

        One of these was just a general curiosity about whether a very different test-driven versioning schema would end up creating a better ~signal. It shared at least the goal of saving humans from worrying about what version to cut.

        The other was about pre-building version-earmarked tests that encode similar assertions about future behavior.

        1. 3

          Golly, I had a feeling lobste.rs was the right place to get feedback on this!

          Indeed, especially tdver feels exactly like the test-based equivalent; e.g. adding a test can only constrain behavior. I’ll certainly need to link to it from this repo as a related idea. You haven’t implemented any tooling around tdver yet, right?

          1. 1

            I did start work on a naive git-based approach implemented in Python. I recall being a little miffed that we hadn’t been using <short-form-of-version-scheme-instead-of-v>1.0.1 all along, so that version semantics could be a bit more evident at a glance (and support would exist across many more toolchains/ecosystems).

            I could’ve sworn I had published it, but I don’t see it up anywhere. I don’t know how far/close it was from usable. It’s about 400 lines, so it might be close… I’ll take a look this evening and see if it’s coherent enough to feel comfortable putting up and report back. :)

            1. 1

              Didn’t have as much time as I hoped tonight. I did take a look, but it didn’t work off the bat. I’ll still check back in, but it may slip to this weekend :)

              1. 1

                No rush! But I am still curious :)

                1. 1

                  I’ve got it up at https://github.com/abathur/tdverpy.

                  The git “api” it was using (really just a CLI wrapper) had gone stale and I was having trouble getting it working, so I took a little more time than I wanted to swap it out for pygit2.

                  I also pulled a little demo together as a near-test, and to help clarify how I was thinking it might be held. You can run it yourself with Nix, though it’s easiest to just peek at the CI output: https://github.com/abathur/tdverpy/runs/6045813570?check_suite_focus=true#step:4:438

                  1. 2

                    Great stuff! Linked from related work.

                    In a fairly unrelated way, pygit is interesting to me: maybe I can use it to automate some worktree management when using the diff_behavior command.

                    1. 1

                      From limited experience with updating tdverpy and writing the first draft of lilgit (a minimal, opinionated bash git prompt module):

                      It is generally less annoying than shelling out, but occasionally I’ll want to shell out anyways because:

                      • the underlying libgit is ~abstracted in a way that sometimes aligns well with the git CLI and sometimes not. Some things you want will be very straightforward and others have to be cobbled together (it can be frustrating to figure out a dozen lines to reimplement something you know you could do from the CLI with a flag)
                      • some things just aren’t performant compared to the CLI.
                  2. 1

                    I also stumbled on some working notes I was making as I plotted things out. I haven’t read them all so IDK if there’s general utility–I put them up on a separate branch @ https://github.com/abathur/tdverpy/blob/working_notes/working_notes

          2. 4

            This feels like it reinforces my main irritation with how SemVer is applied in a lot of languages: A semantic version number is a property of an interface not of an implementation. A single implementation can provide multiple interfaces. Windows does this very well with COM (modulo the limitations of COM, which doesn’t allow extension at all once an interface exists): a single library may expose ISomething, ISomething2, ISomething3, or whatever. As a consumer of a library, I shouldn’t need to care what the version of the implementation is, I should care about whether it exports the interface that I need.

            If you tie SemVers to the implementation then you have no mechanism for doing graceful deprecation. Consider a library that exports interface A, then exports interface B and deprecates A in the next version, then removes interface A in the third version. With SemVer on implementations, I have no mechanism for expressing this:

            • I can use 1.0, 2.0, and 3.0, but code linked against 1.0 will work with 2.0 and code linked against 2.0 but not using interface A will work with 3.0, so I’m advertising more breaking changes than necessary.
            • I can use 1.0, 1.1, and 2.0, but code using 1.1 and not using deprecated APIs will work with 2.0, so again I’m advertising more API breaks than actually happen.

            In contrast, if I version the interfaces, I have three library versions as tuples of SemVers:

            • {1.0}
            • {1.0.1, 2.0}
            • {2.0.1}

            The third version may just be {2.0} if it’s simply the second version with the legacy-compat layers compiled out. I can even have sequences like {1.0}, {1.1}, {1.2, 2.0}, {1.3, 2.1}, {2.2} and extend each interface, giving people a longer window to migrate. A library that exposes a long tail of backwards compatibility interfaces may have a very large tuple of interface versions that it exports and that’s something that, as a consumer of the library, I want to encourage.

            1. 2

              Not knowing much about COM, I don’t feel like I have much to contribute about the idea of an artifact supporting multiple interfaces - it sounds neat. I feel like NPM has a similar solution whereby each library has its own, versioned copy of its dependencies.

              I will talk about something tangentially related, but important. I think it’s worth thinking about what we mean by “interface”. Traditionally, it’s a hunk of functions and types. But type systems are becoming more and more expressive. Contracts bear a lot of similarities to dependent and refinement types. If you put a type system lens on “backwards compatibility” / “strengthening contracts”, you get something like the “Liskov Substitution Principle.” It feels like it’s all the same thing.

              So, critically, I’d want to submit for your consideration the idea that the contract is part of the interface, not the implementation. Hopefully, at a minimum, it’s an interesting idea to explore.

              1. 3

                I put together a longer document about how to handle this in a language with a structural and algebraic type system last year. The fun cases are when you want to do something like:

                • I have a JSON parser library A.
                • I have a JWT library B that uses A.
                • I have an RPC library C that also uses A.
                • My program uses A and passes the results to B and C.

                Now A releases a new version that provides an option for a different point in the speed / memory trade-off space that C wants to use. I want that, so I upgrade C and A. I want B to use the new version of A, but not require any source-code modification and so use the same interfaces as the old version of A. Can I do this? Only if I have a way of exposing types that appear as one thing to B and another thing to C.

            2. 1

              I’m not convinced that this is a meaningful improvement for the semver breaks people argue about. Of course it’s always better to have more precisely defined interfaces, but I’ve found that semver gets broken regardless of that:

              • For users it only matters whether their spacebar-heater client code broke or not. Explaining that they’ve relied on something not covered by the contract is not making them any happier.

              • It’s incredibly difficult to have “full coverage” of behaviour in the contract. Especially for complex cases like “function A will not call function B”. If you check all of these things at run time it’s a performance hit. If you don’t, you may be violating the contract and have users rely on the buggy behavior!

              • People disagree whether a bug fix that changes behavior is a semver break, and don’t want to bump semver even when it’s objectively a change, because they didn’t mean for the change to exist.

              • Once libraries have a large user base, authors are under pressure not to bump major version, so they will even intentionally and knowingly break semver in cases they think it won’t break much in practice. This “in practice” part is important and contacts don’t answer it. People can still argue the contract was needlessly strict/lax or can be ignored in that case.

              1. 2

                I’m not convinced that this is a meaningful improvement for the semver breaks people argue about.

                I am not convinced either! You bring up very valid objections. Nevertheless, I am pretty curious to try it and see what happens in practice.

                Explaining that they’ve relied on something not covered by the contract is not making them any happier.

                Agreed. Hopefully, it does cut down on discussion about whether the change should be reverted. It’s a small win.

                It’s incredibly difficult to have “full coverage” of behaviour in the contract.

                Again, agreed! As the primary maintainer for CrossHair, I am very keen to try and make it less difficult. I think we live in an exciting time for software.


                Backing up a bit: I wouldn’t wish this approach on people who don’t actively want it. Whether you’re on the producer or consumer side, I think contracts appeal to a more (formal?, pedantic?) subset. Perhaps such folks are less likely to be influenced by an internal monologue about whether they “want” to bump versions. And perhaps not! I am curious.