Loop invariants can give you coding superpowers
yourbasic.org
yourbasic.org
For example, in the typical sum example, the mutated variable is usually called "sum" or "total". But you could also call it sum_from_0_to_i, and then a reader can immediately see that the result is the full sum because i equals the array length. It's like a proof-by-variable-name.
The same trick tends to work as well with less trivial invariants. You get long variable names, but not horribly so.
You can do the same for the accumulator in a reduce/fold operation. Way too often variables in reduce calls (and other recursive functions) have horrible names, making the entire reduce needlessly hard to understand. Naming the accumulator properly solves this:
Eg
const total = array.reduce(
(totalUptoPrevious, current) => totalUptoPrevious + current,
0
);
So much easier to follow! I've often had to look up, when reading a reduce, which argument is the accumulator in this particular language or library. When you use the invariant for the accumulator name, you fix this entirely.But I still really like your naming, it seems to bridge the gap in making reduces (which are conceptually cleaner) understandable to people who are used to iterative code.
I believe this is incorrect. Feel free to show me a counterexample.
>>> 0.1+0.2+0.3
0.6000000000000001
>>> 0.2+0.3+0.1
0.6
It's an associativity problem and not a commutative one, however, that is relevant to the original point about folding floating point operations over lists.[1] https://www.quora.com/Is-floating-point-addition-commutative...
(small number)+(small number)+ ... +(small number)+(much bigger number)
may not give the same result as (much bigger number)+(small number)+(small number)+ ... +(small number)In mathematics, commutativity is always about two operands. My textbook on floating point arithmetic[1] (probably the most famous one) states that addition and multiplication in floating point is commutative. They use examples similar to yours as an example of associativity being violated, and point out that commutativity is preserved.
Putting FP aside: In mathematics, no one disputes that addition is commutative. Yet, they all agree that for infinite sums, if you rearrange the order of the terms, you can get different results. They don't say that addition is commutative only for finite sums. They say it is commutative for all addition, and that associativity is the property that fails for infinite sums. See the discussion here[2] for example:
>The commutative property of addition is that a+b=b+a. Technically, it applies only to sums of two numbers!
>Actually, commutivity does hold for infinite sums. Glossing over a few technicalities, commutivity says that you get the same answer whenever you interchange any two terms in a sum.
Although I'll grant that when I look at some other answers in StackExchage, they do generalize to "repeated use of commutativity", and that if commutativity is applied a finite number of times, the sum is preserved, but not if applied an infinite number of times. The common refrain in both cases, though, is that commutativity applies only to two operands.
[1] https://www.springer.com/us/book/9780817647056
[2] https://math.stackexchange.com/questions/646665/why-does-com...
https://onlinebooks.library.upenn.edu/webbin/book/lookupid?k...
This is a very practical semantics of commutativity when programming. It’s unfortunately lost in most modern math expositions.
+ + +
/ \ / \ / \
+ + a + a +
/ \ / \ / \ / \
a b c d + d b +
/ \ / \
b c c d
and others as well. There would be practically no point in trying to find a definition of "commutative" for these structures that don't involve associativity. -6-7-8 == -8-7-6 == -7-8-6 == ...
How does this rely on associativity?
It does not rely on parenthetical associativity. The operator is of course associated with an operand, but that’s not what is meant by associativity.It's exactly the combination of commutivity of addition, associativity of addition, and equivalence of subtraction with addition of the additive inverse, that allows the general rearrangement of symbols you are discussing.
Above, "associativity" was used in the sense of "the property that any re-association produces the same answer". It's used that way when talking about properties of an operation, often in an algebraic context.
I think you're using it in the sense of "an understanding of how operands should be associated". It's often used that way when describing a language in practice, like "(+) is right-associative".
If we're going to be working with expressions like (a + b + c), then whenever + is not (sense 1) associative we clearly need some understanding about what that expression means. But we can restrict ourselves to dealing with fully-parenthesized expressions and not need any sort of "this associates to the left", and still properties like associativity and commutativity can be interesting.
In the case of your trees, associativity means any trees with the same ordering in the leaves (with an in-order traversal) must be equivalent. Commutativity means any trees that differ by swapping the left/right children of a parent node must be equivalent. You can have neither property, either property, or both properties.
(((b + s) + s) + s) + s
Can be different to b + (s + (s + (s + s)))
This re-parenthesising is related to associativity, not commutivity.That’s standard, I think.
(((b + s) + s) + s) + s
==
s + (s + (s + (s + b))) NaN + 1 = NaN
1 + NaN = NaN
but we also have: NaN ≠ NaN
, so that’s an example where a + b ≠ b + aI do know that with the floating point standard, NaN ≠ NaN returns True. But what does it mandate for NaN == NaN? Is this always false? If so, you're right. If not, I'd argue I'm still right, as that means that NaN + 1 == 1 + NaN would still be true.
I recognize that in a codebase where every 10 lines there's a reduce or something similar this may not be necessary, in the exact same way that nobody names a C style loop index variable "current_index". But I find that, even in many functional languages, reduces and custom recursive functions aren't that common because the libraries include so many batteries.
I do normally call this sort of thing acc or accumulator though. I feel like an understanding of what that specific term means comes with the field expertise, sort of like we can say "LinkedList" and not "ObjectWithPointerToNextObject" and know exactly what that pattern should look like. A lot of CS skill comes down to expanding simple standard names.
That's not to say that your approach doesn't have value. It might just be that the toy example doesn't show it off very well. But if you were juggling that 3-way partition, for example, your naming scheme starts making a lot of sense for naming the boundary variables.
However, I've looked over the page a couple times and still can't figure out the use of this, let alone what "superpowers" it may grant. It's just comments? The best parts I can see are the mostly-prose ones farther down that just say what the loop's supposed to do, but even those are just repeating the "input" and "output" info, really, which are sort of also comments I think. Some of this might be useful if you could express it in types or some other machine-checkable fashion, but in comments they're just kind of redundant and, like any comments, must be treated with suspicion anyway.
What am I missing? I just have no idea what I'm looking at here, or rather I think I do but I'm entirely missing why it should be in any sense exciting which leads me to think maybe I don't.
So I thought I must be missing something.
You could put some of these into asserts or similar. Like for the three way partition. Create an assert that at each iteration the array[:low] values are all less than p, array[high:] values are each greater than p, and array[low:mid] values are each equal to p. If it doesn't hold then you know your code is wrong. This does induce a performance hit, but asserts can be turned off for deployed code.
I think this is as close to the answer you can get in the paradigm in which you're asking. It is useful in the formal but non-machine-checkable realm to design the algorithm. Every invariant you can encode into the type system is a double win, but invariants you can't specify in the type system are still a win.
FWIW, direct exposure to abstract concepts like this is a benefit of a CS education. They're eminently learnable outside of a classroom (bottom-up), but you have to work to keep an open mind to recognize the trail of breadcrumbs that leads past your practicality-tailored preconceptions. Think of yourself as learning math, not programming.
In the same way, using the tool on the page you can prove that 1. the while-loop terminates. 2. the invariants hold, meaning that your program is correct. Yes, you can encode the proof in a machine-checkable format but that is missing some of the point. Just like with mathematical proofs, the major use of the proof is to communicate with humans.
Also the same techniques used to prove invariants by hand can be used by compilers to implement smart optimizations. In the sum example, the compiler could deduce/prove that sum = \sum_{i=1}^n i and replace the loop body with n(n+1)/2. I don't know if any compilers do such advanced optimizations but theoretically they could (in trivial cases like these - in general, proving properties about code is as hard as solving the halting problem).
Other proof techniques involve finding upper and lower limits of variables which are used to store values in more efficient types. For example, storing an integer as an int32 instead of int64.
To really understand what's going on read https://en.wikipedia.org/wiki/Hoare_logic. The article's proof uses Hoare logic but implicitly, without the notation, so understanding how the proofs work is hard.
Not quite. Proving that a while loop terminates is usually done by establishing a loop variant - intuitively, a claim that the inputs to the loop become "smaller" in some sense, i.e. closer to some exit condition/base case, with each iteration of the loop. Loop invariants don't suffice on their own.
For most of us, this isn't a big deal. But let's say you're writing a subroutine that will control part of a pacemaker, rocket, fighter jet, satellite- anything where bugs are deadly or expensive- then this is a Very Big Deal.
In the first example, author didn't even indicate what the loop invariant is in the code, before moving on to a second example.
The Invariant Principle was formulated by Robert W. Floyd at Carnegie Tech in 1967. (Carnegie Tech was renamed Carnegie-Mellon University the following year.) Floyd was already famous for work on the formal grammars that transformed the field of programming language parsing; that was how he got to be a professor even though he never got a Ph.D. (He had been admitted to a PhD program as a teenage prodigy, but flunked out and never went back.)
In that same year, Albert R. Meyer was appointed Assistant Professor in the Carnegie Tech Computer Science Department, where he first met Floyd. Floyd and Meyer were the only theoreticians in the department, and they were both delighted to talk about their shared interests. After just a few conversations, Floyd’s new junior colleague decided that Floyd was the smartest person he had ever met.
Naturally, one of the first things Floyd wanted to tell Meyer about was his new, as yet unpublished, Invariant Principle. Floyd explained the result to Meyer, and Meyer wondered (privately) how someone as brilliant as Floyd could be excited by such a trivial observation. Floyd had to show Meyer a bunch of examples before Meyer understood Floyd’s excitement — not at the truth of the utterly obvious Invariant Principle, but rather at the insight that such a simple method could be so widely and easily applied in verifying programs.
There's a tutorial + web interface at: https://rise4fun.com/Dafny/tutorial/Guide. The official repository is here: https://github.com/Microsoft/dafny -- you might want to switch to a local installation once the online tutorial whets your appetite. A good initial challenge, once you've gotten past the baby stuff in the tutorial, is implementing insertion and deletion on a binary search tree with appropriate pre- and post-conditions.
Dafny puts this stuff front and center, it's all well and good to think about invariants, but if they're just expressed in a comment and not actually verified, they might as well be filler text!
If there's anything I hope the mainstream adopts at some point, it is some variation of what Dafny offers here.
Invariants like this allow one to prove the correctness of their code. That may not seem like a big deal most of the time, but imagine you're working on a satellite that's going to Jupiter, where a bug in the code could mean 10 years from now a billion dollar project gets scuttled. Imagine you're building subroutines that will go into 10,000 pacemakers next year. Imagine you're writing the navigation logic for a cruise missile.
There are many times where proving your code correct is taken very, very seriously. Invariants are a tool to do so more easily.
There could always be another rare edge case.
"Program testing can be used to show the presence of bugs, but never to show their absence!"
Dijkstra (1970)Imagine trying to unit test all possible cases of Pythagorean theorem being correct vs a single proof in a few lines of descriptive logic.
Cornell:
Invariants Playlist: https://www.youtube.com/playlist?list=PLTD_NtzzD4VC6l2uLdzbsm9wW21mMaWwj
http://www.cs.cornell.edu/courses/cs2110/2017sp/online/loops/01aloop1.html
https://www.cs.cornell.edu/courses/cs1110/2018sp/materials/loop_invariants.pdf
CMU:
https://www.cs.cmu.edu/~15122/handouts/01-contracts.pdf
http://www.cs.cmu.edu/%7Efp/courses/15122-s11/recitations/recitation02.html
(former CMU prof) https://www.youtube.com/watch?v=lNITrPhl2_A
Of course if you really wanted to understand the challenge of finding loop invariants, there's the software foundations series of books which even if I don't understand all of it, has been still worth my time to go through https://news.ycombinator.com/item?id=19565365 and Cornell has some good introductory Coq material https://www.cs.cornell.edu/courses/cs3110/2018sp/a5/coq-tact...For the first example, consider:
go (n, i, sum): given sum = 1 + 2 + ... + i-1 returns (1 + 2 + ... + n) :
per cases of (i <= n)
assume (i <= n) {
note: (sum = 1 + 2 + ... i-1 -> (sum + i) = 1 + 2 + ... + i)
return go (n, i + 1, sum + i);
} else assume (i > n) {
note: thus i = n + 1
then: sum = 1 + 2 + ... + i - 1 = 1 + 2 + ... + n
return sum;
}
sum n: returns 1 + 2 + ... + n = {
note: 0 = 1 + 2 + ... + (1 - 1)
// ^^^ Need to prove our precondition for go!
return go (n, 1, 0);
}
The other examples are a bit more complex for sure, but structurally similar. It's nonetheless easy to see how even very limited changes in complexity (such as the choice of loop indexing) can create quite a few pitfalls for truly rigorous proof!Sum(0..X)=Sum(0..i)+sum(i+1..X) with the moving i. So it is true at first because the unsummed part is empty and the missing part is the goal. Then we take away an i from the solution to-go pile and put it on the done pile and when the loop has done X the to-do pile is enpty and per invariant the done pile equals the goal.
Including the goal in the invariant sometimes gives you nicer properties. The equivalence relation often also implies an algorithm.
For max I would say the equivalence is: Max(0..X)=Max(Max(0..I),max(I+1..X)) This of course only easier if the max of empty is negative infinity. Then you can move the i from 0 to X.
The author's invariant is Sum = 1+2+..I Mine is Sum+(I+1)+...+X = 1+2+..I+(I+1)+...+X
That's both fine and tells us about I. I think the version that contains the value we want to obtain is easier to prove correctness for.
Which doesn't mean the article is _wrong_ so much as it demonstrates one needs to prove the invariants are actually true.
I know this is common practice but I haven’t seen it mentioned here and it might help someone.
edit: Jonsen's reply question, for which I don't have an answer, is instructive: these loop invariants are often impossible or impractical to achieve in assertions. I modified the above to be less wrong.
Another great example of exploiting a (not-really-loop) invariant to design an algorithm is Sean Parent's implementation of a generic 'gather' algorithm, which takes advantage of the fact that the set of objects below/above the gathering point is unchanged, allowing you to split the problem into two easier sub-problems. Here's his explanation and implementation (video should be linked to 16:50):
(I guess I am a bit surprised by the short length of this article because when I write code, I tend to think of it in those terms already, so I expected it to go further; but I guess not everyone has been exposed to that idea or taught to program like that in the first place so I realize that that's my own bias).
What should I check out if I want to be able to formally check invariants for e.g. my Python or Swift code?
I ran into loop invariants in my CS undergrad where they were teaching "formal logic" to prove correctness of programs. At the time, it was a bit above my head and i struggled with it. i'm just beginning to understand what it was that we were studying at the time. cool stuff. wish i could go back and re-learn that stuff. Also google TLA+ - there's a book on it by Leslie Lamport.
http://se.ethz.ch/~meyer/publications/methodology/invariants...
Also see Meyer's Design By Contract book, which you should read even if you will never use Eiffel - it's a gem.
I wonder, are there such techniques tailored to functional programming? (immutability, recursion,...)
“Sure, I’ll just implemment a loop according to this invariant ... “
Would be nice to have the invariant ready then.