Hacker News new | past | comments | ask | show | jobs | submit login

For the record, I'm not the author, nor do I agree with him. But I posted this because there was no comment section and I was curious if anyone would either

1) Agree with the article (virtually no one has) 2) Point out that the breaking changes Rich Hickey pointed out might be worth it, or not a big deal

A few people have responded with sentiments of #2. Not because it's not annoying to change these things, but that it's a smaller price to pay for relatively good type systems (I'm thinking more F# than Haskell)

Unfortunately, the talk Rich Hickey gives explicitly says Clojure spec doesn't have a solution for a flexible type system either, and the solution they proposed is still in alpha 4 years later. So while I appreciate the insights he offered, I'm torn between the problem of refactoring with the extra work of a finicky type system, or the work of hoping my tests are good enough to refactor my Clojure code.

Types in Clojure is the number one requested feature in their surveys, year over year, and despite the reputation he has, Rich recognizes that it can really help refactoring a code base. Good variable names aren't going to help you with this. Pushing Spec2 along would be a big win without having to implement a full blown type system.




My issue is that the author doesn’t seem to know what a breaking change is. Spec itself is annoying to me for a couple reasons, but this isn’t some novel insight of Rich Hickey’s: the claim about relaxing preconditions and strengthening postconditions is in Bertrand Meyer’s _Object Oriented Software Construction_ and is a relatively basic claim in texts about what it means for software to be correct.


Extending a function to accept null, under dynamic typing, can break a program which depends on the documented behavior of that function having thrown an exception for that case.

  (defun strlen (s)
    (assert (stringp s))
    (len s))

  (defun map-len-test ()
    (mapcar (lambda (arg)
              (ignerr (strlen arg)))
            '("abc" nil "defg")))
  

  (pprinl (map-len-test))

  (defun strlen (s)
    (if (null s)
      0
      (len s)))

  (pprinl (map-len-test))
Output:

  (3 nil 4)
  (3 0 4)
The caller had one idea of substituting a value in the null argument case. The function changed behavior, implementing a different idea.

Only in a statically typed language in which the call with nil is impossible, and which provides no dynamic access to the compiler, can we be 100% confident in saying that extending the function to support nil is a non-breaking change.

If the is impossible, then the function has no behavior for that case; a program invoking that case doesn't exist in a translated, executable form and so there is nothing to break as far as the program is concerned.

It's possible for another program to break: a compilation test case which validates that code which calls that function with nil cannot be compiled. If the language supports dynamic access to the compiler, then an application can be written which can behave differently due to the change: an application which dynamically compiles a call to the function and now gets a valid result (compiled code) rather than an error.

In the dynamic world, it's a non-breaking change for applications that wisely don't rely on exceptions being thrown on bad inputs, and instead handle those themselves.

If you're changing the function, you cannot know that is not the case; at beast you can decide not to care.

Not caring is not the same as knowing.


> Extending a function to accept null, under dynamic typing, can break a program which depends on the documented behavior of that function having thrown an exception for that case.

This just means that the preconditions are “the value is not null” even if there are no types to capture that precondition. I was pretty careful not to use the word “type” in the comment you’re replying to.


The problem is that in fact there are no preconditions; the function can be called unconditionally using any value as an argument. It throws for some values, and any change in which ones is a visible change.

Programmers will make code dependent on anything that is visible in an API, even if the specification tells them not to, an ISO standard tells them not to and even if their mother tells them not to.

Here, the spec may even be saying: if you call this function with a non-string, it throws an exception.


> The problem is that in fact there are no preconditions; the function can be called unconditionally using any value as an argument.

There are no _checked_ preconditions. It’s only meaningful to talk about the correctness of a program relative to a specification of its behavior and a specification needs to specify preconditions and postconditions. So, if there are no preconditions, any behavior of the function being called is as correct as any other behavior.

The fact that some programming languages allows the callers of a function to pass values that don’t satisfy the spec is irrelevant to the point I’m making.


The function checks for a type with assert: (assert (stringp arg)). But that check is a documented behavior which generates a predictable exception that the caller can detect and so can tell when that is suddenly missing.

No matter what consequence you give to the failed check, something can break when the check is taken away.

We could have a True(r) Scotsman's checked precondition like this

  (unless (stringp arg)
    (abort)) ;; bail the entire process image with abnormal status
If we check it like that, nobody will try to depend on the behavior within an application, in the way that my test program did.

It can still be a breaking change if the function is written that way, and then changed so the abort is taken away. Because the application can do this:

  (defun my-stupid-abort ()
    (strlen nil)) ;; this calls abort!
So now if the program relies on (my-stupid-abort) to abort, that will break; suddenly that function call returns zero and the process keeps going.

You really need:

  ;; ... ffi definitions here ...
  (unless (stringp arg)
    (acpi-power-off))
But again, dumb program can depend on (strlen nil) to power of the machine, which could be part of some critical system that breaks as a result of it not powering off.

Only a compile time check can do it, by preventing the program from existing. A program that never compiled and therefore was never deployed to its installation will never break.


> So, if there are no preconditions, any behavior of the function being called is as correct as any other behavior.

Umm, no. If the program is supposed to unconditionally produce the output "Hello", but it instead produces "Goodbye", then it is incorrect. Unconditionally means not that there are no preconditions but that the precondition is T (true). If the precondition is constant truth, it means that there are no circumstances under which the program can be excused for not satisfying the postcondition (e.g. producing "Goodbye" rather than "Hello").

Correctness is almost irrelevant here, because the question is about breakage. It is in fact the change in specification which is behind the breakage. The function is correctly implementing its specification at all times; the correctness of my example strlen is never being called into question. What it means for it to be correct has been changed, and the function's definition followed suit.


A breaking change is one that breaks code that uses it correctly. This means that the code that uses it uses it according to its specification. Breaking code that uses your code incorrectly should happen early and often because using code incorrectly is a bug in the using code, not the code being used.


That's just a blame game that is neither here nor there to the user who can't open his ten-year-old word processing document with the latest version.


It makes all the difference in the world for that problem. It’s how you build programs that can be maintained and evolved in a consistent fashion long-term.


I would agree the author was wrong, and I would hazard a guess that you're a lot more well read than most of us on this kinds of stuff, because this talk is widely seen as an insightful.

Is there a language that you feel solves this problem better than Spec could ever hope to?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: