Proving the Coding Interview
17 Apr 2022 |
Applying interesting research to uninteresting interview problems
This is the first of a three part series: Part 1 | Part 2 | Part 3
Enjoying programming interview questions is one of those things that nobody should admit to in polite company. But as with so many other things in life, a good gimmick can make the most tedious chore bearable if not outright pleasant. And if you’re a grad student and can use that chore as a procrastination tactic, then all the better! (Sorry, James, if you’re reading this, I’ll get on whatever it is tomorrow!)
As it happens, it’s summer internship interview season, and with a new set of interview loops comes a need for yet another gimmick. This time, I figured: given my research interests in practical uses of formal verification, why not put my “checkable correctness proof” money where my “bug-ridden half-hearted implementation” mouth is, and try to formally verify the correctness of my practice problems’ solutions? I can’t honestly claim this will convince a company to give me a summer job, but at least I’ll have briefly entertained myself in the process.
My first task was to choose an interview problem that only formal methods could make briefly interesting.
Our problem statement
I can’t think of an interview problem that’s both as widely popular and yet wildly uninteresting as FizzBuzz. The problem’s arbitrary and divorced from any real use-cases, it’s never terribly satisfying to implement, and is fiddly enough that it’s easy to trip over an off-by-one error or incorrect if/else statement. It’s the poster child for misguided tech company interviews, and yet I’ve actually been asked it before!
Okay, here’s the Fizzbuzz problem:
Print `n` lines of output, where:
1) Every third line (unless it's a multiple of 15) reads "fizz";
2) Every fifth line (unless it's a multiple of 15) reads "buzz";
3) Every fifteenth line reads "fizzbuzz";
4) Every other line should read the number of lines previously printed out.
As any careful programmer will tell you, reasoning about side-effects like printing in unit tests is difficult, and the same is true when it comes to coming up with a good formal model for program behaviour. So, let’s break this problem into two steps: first we’ll generate an array of values and then state properties that the array has to satisfy. Then, we’ll just print that array out:
First, construct a list of strings of length `n`, such that the following hold
for all indexes `i` from 0 to n:
1) If i is a multiple of 3 (but not of 5) then the `i`th element equals "fizz";
2) If i is a multiple of 5 (but not of 3) then the `i`th element equals "buzz";
3) If i is a multiple of 15 then the `i`th element equals "fizzbuzz";
4) Otherwise, the `i`th element equals the string representation of `i`.
Then, print out, separated by a newline, every element in the list.
The Dafny programming language
There are many new languages that make formally verifying real software easier than ever before: One such language is called Dafny, originally designed at Microsoft Research and now under the stewardship of folks at AWS. Dafny is a great gateway to software verification because the style of programming looks so similar to what you’d write in an OOP language like Java or C#, and does its best to automate most of the correctness-proving mechanics away from you.
Let’s write some Dafny code! Don’t worry too much about every individual piece of syntax. The broad strokes should feel familiar-ish to you and I’ll explain the weird stuff as we come across it.
Here’s the skeleton of our program that calls our Fizzbuzz()
implementation
and prints out the array it returns. In Dafny, a method may or may not be
associated with an object - since it doesn’t make sense to use classes for this
particular problem these ones won’t be.
method Fizzbuzz(n: int) returns (ret: array<string>)
{
ret := new string[n]; // the `ret` variable is the one returned to the caller.
}
method Main()
{
var n := 100;
var fb := Fizzbuzz(n);
var i := 0;
while i < n
{
print(fb[i] + "\n");
i := i + 1;
}
}
Already the Dafny compiler complains about two potential problems. See if you can spot one of the issues before continuing - I’ll give you a hint: it involves allocating the array of strings…
Adding pre- and post-conditions
Preconditions require things to be true
The first error might feel familiar to any C programmer who’s ever passed a
signed integer to malloc()
:
$ Dafny Fizzbuzz.dfy
...
Fizzbuzz.dfy(157,22): Error: array size might be negative
...
Dafny program verifier finished with 8 verified, 2 errors
Even though our program only calls Fizzbuzz()
with a positive argument, Dafny
tells us that there could exist a call to fizzbuzz with a negative argument,
in which case instantiating the array will go awry.
We can solve this problem in two ways: one way would be to simply change the
argument type from int
to nat
, another built-in Dafny datatype. In that
case, the typechecker would ensure that i
will never be negative and the
error would disappear. Let’s fix this in a more general way, though: Let’s add
a precondition assertion to Fizzbuzz()
: a precondition is a logical
statement (which you can think of as a boolean expression) that must be true
when the function is called:
method Fizzbuzz(n: int) returns (ret: array<string>)
requires n >= 0 // On entry, the caller of Fizzbuzz() needs to know that n is nonnegative
{
ret := new array[n];
}
The requires
keyword makes this a precondition. This states the caller of
Fizzbuzz()
needs to be able to prove to Dafny that its argument is
nonnegative. In our case, this is easy: in Main()
, we simply assigned 100 to
a local variable and then passed it to Fizzbuzz()
. If we tried to, say, pass
-1 to it, the Dafny compiler would complain because that boolean expression
would no longer be true.
Postconditions ensure that things are true
The second error is more subtle: Back in our Main()
method, our while-loop
indexes into the array returned by Fizzbuzz()
on the interval [0, n)
(that
is, from 0 to but not including n
). The other error that Dafny gives us is:
$ Dafny Fizzbuzz.dfy
...
Fizzbuzz.dfy(170,16): Error: index out of range
...
Dafny program verifier finished with 8 verified, 2 errors
This isn’t saying that the index is necessarily out of range, but rather that
Dafny isn’t sure how the length of the array relates to the loop variable.
Thinking about it a bit deeper: We’ve implicitly made an assumption with the
array that Fizzbuzz()
returned: namely, that it’s long enough for us to loop
over from 0
to n
! Think about all the times you’ve assumed something about
the length of an array only to find you were wrong! Dafny won’t let you make
that mistake here.
But it’s not hard to stare at this code for a minute and convince ourselves
that the returned array is of length n
and to conclude that, as before,
Dafny’s not (yet!) smart enough to figure that fact out without some help from
us. So, let’s write a postcondition assertion on Fizzbuzz()
that states
that tells Dafny that whatever n
we supply to Fizzbuzz()
will correspond to
the output array’s length.
method Fizzbuzz(n: int) returns (ret: array<string>)
requires n >= 0 // On entry, the caller of Fizzbuzz() needs to know that n is nonnegative
ensures n == ret.Length // On return, Fizzbuzz() promises the caller that the length matches the input
{
ret := new array[n];
}
If we screwed up and returned, say, an array of length n-1
or n+1
or 42
Dafny would notice and report a compilation error. But with this
postcondition, Dafny now understands that accessing elements on [0, n)
is
safe to do, and stops complaining to us.
If we were programming in Python, we could imagine doing these sorts of checks
at runtime through assert
statements:
$ python3 -q
>>> def Fizzbuzz(n: int) -> list[str]:
... assert n >= 0, "we were given a negative input"
... ret = [""] * n
... assert len(ret) == n, "we would have returned a list of the wrong length"
... return ret
...
>>>
Of course, in this case, we won’t actually know until the program is running that something went wrong. It’s far better to know beforehand than hope for the best and wait for your program to crash in production!
We can start to see that Dafny is doing something similar but at compile-time:
we thread facts through our program using requires
and ensures
statements,
which it uses to prove to itself, and us, that there are no situations where
something could go wrong.
We also saw that just because something is “obviously true” to the programmer doesn’t mean it’s obviously true to Dafny, so sometimes we need to “state the obvious” to guide the compiler to understanding our intention.
Converting our specification into pre- and post-conditions
Because the whole point of using Dafny is to ensure our Fizzbuzz()
implementation satisfies a specification, we need to convert our
problem statement into Dafny. Since our problem statement is all about facts
concerning the returned array (i.e. “if j
satisfies some property then
ret[j]
will satisfy some other property”), it makes sense to use
postcondition assertions to express this.
Here’s my first attempt at this: notice how similar this looks to our English
specification from above. (==>
is logical
implication, a boolean
operator like ||
or &&
that you may not have seen in other programming
languages.)
// 1) If i is a multiple of 3 (but not of 5) then the `i`th element equals "fizz";
ensures forall i :: 0 <= i < n ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz"
// This property ensures all integers i in the range of 0 to n for which i mod
// three is zero and i mod five is not zero, ret[i] shall be "fizz"
// 2) If i is a multiple of 5 (but not of 3) then the `i`th element equals "buzz";
ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz"
// This property ensures all integers i in the range of 0 to n for which i mod
// five is zero and i mod three is not zero, ret[i] shall be "buzz"
// 3) If i is a multiple of 15 then the `i`th element equals "fizzbuzz";
ensures forall i :: 0 <= i < n ==> i % 15 == 0 ==> ret[i] == "fizzbuzz"
// This property ensures all integers i in the range of 0 to n for which i mod
// fifteen is zero, ret[i] shall be "fizzbuzz"
// 4) Otherwise, the `i`th element equals the string representation of `i`.
// TODO: Will Dafny complain about the expression `ret[i] == i`?
ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i
// This property ensures all integers i in the range of 0 to n for which i mod
// three and i mod 5 is zero, ret[i] shall be i
Just like before, our specification will sit at the top of the Fizzbuzz()
function, just after the method signature. There’s an issue with the fourth
and final postcondition: The problem is, of course, that str[i]
is a string
but i
is an int. The Dafny standard
library is still a work in progress
and doesn’t yet have a conversion method, so we’ll have to eventually write
one. For the moment, though, let’s just put that on the back burner and only
treat parts 1-3 of our problem statement as our specification. Let’s take
stock of our Fizzbuzz()
specification and “skeleton implementation”, and
see what Dafny tells us when we try to compile it:
method Fizzbuzz(n: int) returns (ret: array<string>)
requires n >= 0
ensures n == ret.Length
ensures forall i :: 0 <= i < n ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz"
ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz"
ensures forall i :: 0 <= i < n ==> i % 15 == 0 ==> ret[i] == "fizzbuzz"
//ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i // TODO
{
ret := new string[n];
//TODO: return a list of strings `ret` such that
//our problem statement is satisfied...
}
$ Dafny Fizzbuzz.dfy && dotnet Fizzbuzz.dll
Fizzbuzz.dfy(161,0): Error: A postcondition might not hold on this return path.
Fizzbuzz.dfy(157,8): Related location: This is the postcondition that might not hold.
Fizzbuzz.dfy(158,8): Related location: This is the postcondition that might not hold.
Fizzbuzz.dfy(159,8): Related location: This is the postcondition that might not hold.
Dafny program verifier finished with 10 verified, 3 errors
As you might expect, the three postconditions that Dafny can’t prove the
correctness of are the three ensures forall
postconditions we just added.
This of course makes sense: we haven’t actually implemented the algorithm yet.
Just like how in test-driven development we’d expect our unit tests to
initially all fail, we similarly expect our specification to not match the
implementation. And just like in the TDD model, we’ll know when we’ve got
a working Fizzbuzz()
solution: when Dafny verification begins to succeed!
Loop invariants and our first implementation
Let’s implement the core Fizzbuzz()
logic. Since Dafny’s a straightforward
imperative programming language, we can implement it just like we would in Java
or C#. There are many different possible implementations but I’m going to stick
with one that very closely resembles the specification: since the spec gives us
four distinct cases to handle, I’ll have four distinct if/else statements.
method Fizzbuzz(n: int) returns (ret: array<string>)
requires n >= 0
ensures n == ret.Length
ensures forall i :: 0 <= i < n ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz" // 1.
ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz" // 2.
ensures forall i :: 0 <= i < n ==> i % 15 == 0 ==> ret[i] == "fizzbuzz" // 3.
//ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i // 4.
{
ret := new string[n];
var j := 0;
while j < n
{
if j % 3 == 0 && j % 5 != 0 {
ret[j] := "fizz"; // Part 1. of the spec
} else if j % 3 != 0 && j % 5 == 0 {
ret[j] := "buzz"; // Part 2. of the spec
} else if j % 15 == 0 {
ret[j] := "fizzbuzz"; // Part 3. of the spec
} else if j % 3 != 0 && j % 5 != 0 {
ret[j] := "TODO: convert j to a string"; // Part 4. of the spec (not yet handled)
}
j := j + 1;
}
}
At this point we might think that Dafny will have nothing to complain about -
after all, as before, we can alternate staring deeply at the implementation and
staring deeply at the specification until we have convinced ourselves we did it right.
Each time through the loop, we modify the j
th element in the array based on
our loop counter’s value in a way that precisely matches what the spec tells
us. All should be well.
However, Dafny complains with exactly the same errors as before: A postcondition might not hold
! The reason is also the same as before: we
haven’t told Dafny enough facts about our implementation for it to understand
why our postconditions are true. In general, loops are hard for automated
tools to reason about; in particular, it’s hard for Dafny to know precisely how
many times a loop will be executed. If Dafny doesn’t know for sure then it can’t
reason about actually got stored in the output array’s elements. Maybe the
loop skipped over some elements, or overwrote previously-written values! We,
the humans, know that neither of those things happened, but we have to explain
that to Dafny.
To do so, we’ll introduce a new kind of logical clause called a loop
invariant. This is, like an ensures
or requires
clause, another logical
statement that must be true at the top of each loop iteration (i.e. before the
loop’s guard condition, j < n
, is checked).
What can we say about the values in the array when the loop’s guard is true and
we (re-)enter the loop body? Well, all the elements from 0 to j
are filled
in with the right value from previous loop iterations. For example, when j == 5
, we know that elements 0, 1, 2, 3, and 4 have their correct values. And of
course, at the end of that iteration, so too will element 5, which sets us up
for the next time ‘round when j == 6
.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
ret | x | x | x | x | x | | | | | | | | | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
\------------------/^
fizzbuzz values |
are filled in j == 5: this element will be filled in by the
end of the current loop iteration...
And what can we say when we exit the loop body for the last time? Well, j == n
, so by a similar argument, all elements from 0 to n
are filled in with
the right value, from previous loop iterations. Hey, that’s the entire array,
which is the thing we wanted!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
ret | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
\--------------------------------------------------------------/^
fizzbuzz values are filled in! |
j == 16
So, our loop invariants will actually match very closely our postconditions:
instead of saying “for all elements indexed from 0 to the size of the list, the
following statements are true”, we only have to say “the following statements
are true for all elements from 0 to our loop counter”: With one more
tiny assertion to remind Dafny that our loop counter will never exceed n
, our
final specification and implementation looks as follows:
method Fizzbuzz(n: int) returns (ret: array<string>)
requires n >= 0
ensures n == ret.Length
ensures forall i :: 0 <= i < n ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz"
ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz"
ensures forall i :: 0 <= i < n ==> i % 15 == 0 ==> ret[i] == "fizzbuzz"
//ensures forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i
{
ret := new string[n];
var j := 0;
while j < n
invariant 0 <= j <= n // Another one of those "obvious" facts that Dafny needs to be told explicitly
invariant forall i :: 0 <= i < j ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz"
invariant forall i :: 0 <= i < j ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz"
invariant forall i :: 0 <= i < j ==> i % 15 == 0 ==> ret[i] == "fizzbuzz"
//invariant forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i
{
if j % 3 == 0 && j % 5 != 0 {
ret[j] := "fizz";
} else if j % 3 != 0 && j % 5 == 0 {
ret[j] := "buzz";
} else if j % 15 == 0 {
ret[j] := "fizzbuzz";
} else if j % 3 != 0 && j % 5 != 0 {
ret[j] := "TODO: convert j to a string";
}
j := j + 1;
}
}
This is enough for Dafny to be convinced that our postconditions are satisfied:
after the final loop iteration, j == n
, which perfectly matches our actual
specification! We have, at long last, an implementation that Dafny has proven
satisfies our specification, and so we’re able to generate executable code
from our program!
$ Dafny Fizzbuzz.dfy
Dafny program verifier finished with 11 verified, 0 errors
Compiled assembly into Fizzbuzz.dll
$
What if we’d gotten the implementation wrong?
I’ve never been a good test-taker and one of my favourite things about grad
school is that, generally, the only classes that would make me write exams
are the ones I wouldn’t want to take anyway. Let’s imagine my nerves had
gotten the better of me and that I’d made a silly mistake in my implementation:
here’s a mistake I make figuratively-literally every time I write a
while
-loop: I simply forget to bump the loop counter so my program hangs
forever. If I take out that j := j + 1
statement and recompile, Dafny tells
us that it isn’t sure that our loop will ever actually terminate:
$ Dafny Fizzbuzz.dfy
Fizzbuzz.dfy(165,4): Error: cannot prove termination; try supplying a decreases clause for the loop
Dafny program verifier finished with 10 verified, 1 error
$
A decreases
clause helps Dafny understand how, at each iteration of the
loop, the loop counter is getting “closer” to the loop’s termination condition.
For us, our clause would be decreases n - j
, since at each iteration that
expression becomes smaller (since j
becomes bigger!), and the loop never
iterates when n - j <= 0
. Dafny has become good enough at infering this
clause out for simple loops that we usually don’t need to state it explicitly.
In this case, the problem isn’t that we’ve not explained to Dafny why our loop
eventually terminates - we wrote the loop wrong and it doesn’t!
Already we can see that Dafny is checking more powerful program properties than our business logic: it will complain if it isn’t convinced that our program will ultimately terminate! (And, no, this doesn’t circumvent the Halting Problem: deciding termination in general-purpose programming languages is an area of active research.)
Counterexample generation
Let’s introduce a more annoying bug. imagine that I’d written the following loop body instead - can you spot the error?
method Fizzbuzz(n: int) returns (ret: array<string>)
...
{
if j % 3 != 0 && j % 5 == 0 {
ret[j] := "fizz";
} else if j % 3 == 0 && j % 5 != 0 {
ret[j] := "buzz";
} else if j % 15 == 0 {
ret[j] := "fizzbuzz";
} else if j % 3 != 0 && j % 5 != 0 {
ret[j] := "TODO: convert j to a string";
}
j := j + 1;
}
Here I simply flipped the conditions where “fizz” should be appended versus “buzz”. Could you honestly say you might not make the same mistake in a job interview? If you were interviewing me and I wrote this out, would you even notice?
This is one of the reasons why Fizzbuzz is a terrible interview question: it’s super easy to invert a silly conditional, and if your interviewer is sharp-eyed and in a mean mood that day, you might get written up for it. Dafny catches this for us, though, and tells us that loop invariants 1 and 2 are no longer satisfied:
$ Dafny Fizzbuzz.dfy
Fizzbuzz.dfy(92,14): Error: This loop invariant might not be maintained by the loop.
Fizzbuzz.dfy(92,14): Related message: loop invariant violation
Fizzbuzz.dfy(93,14): Error: This loop invariant might not be maintained by the loop.
Fizzbuzz.dfy(93,14): Related message: loop invariant violation
Dafny program verifier finished with 10 verified, 2 errors
$
We can even do one better: Dafny can show us potential values that violate
the invariant! By adding the /extractCounterexample
flag (or, if you’re
using the Visual Studio Code plugin, directly in your editor), you can ask
Dafny to give us a specific counterexample that violates the specification.
It’s definitely easier to understand in an IDE, but here’s the relevant
output on the command-line:
...
Counterexample for first failing assertion:
Fizzbuzz.dfy(84,0): initial state:
n : int = 6
ret : ? = ()
...
Fizzbuzz.dfy(88,4): after some loop iterations:
Fizzbuzz.dfy(106,18):
n : int = 6
@1 : seq<char> = ['f', 'i', 'z', 'z']
ret : _System.array?<seq<char>> = (Length := 6, [5] := @1)
In this example, Dafny’s underlying solver has found a code path where
Fizzbuzz()
is called with n == 6
; at some point during program execution,
index 5 of our ret
array holds the string “fizz” (which the prover assigned
to a temporary variable named @1
; however, since 5 is divisible by 5, this
violates the specification’s requirement that it be equal to “buzz”!
Remember: at no point did our implementation ever actually execute: Dafny found
this counterexample symbolically, by exploring the space of program executions
and validating that whatever values are assigned to ret
match our invariants
and postconditions.
A verifiable “optimisation”
When I was asked to implement Fizzbuzz on the whiteboard the interviewer
followed up with, “…and how could we make this more efficient?”. After
some frustrating back-and-forth about what that could possibly mean, it turns
out they were slightly unhappy with the number of branches in my conditional
and wanted me to reduce the number of else if
s in my solution. That this
company was the darling of my regional tech scene and that they thought this
would “make fizzbuzz run faster” did not inspire much confidence in me!
Anyway, sigh, fine, with a bit of refactoring this isn’t difficult to do: we’ll have a mutable string variable to build up “fizz”, “fizzbuzz”, or “buzz” incrementally. It might not be faster to execute, or easier to read, but at least the garbage collector will get more of a workout when it runs!
The body of our loop now looks like this:
var j := 0;
while j < n
invariant 0 <= j <= n
invariant forall i :: 0 <= i < j ==> i % 3 == 0 && i % 5 != 0 ==> ret[i] == "fizz"
invariant forall i :: 0 <= i < j ==> i % 3 != 0 && i % 5 == 0 ==> ret[i] == "buzz"
invariant forall i :: 0 <= i < j ==> i % 15 == 0 ==> ret[i] == "fizzbuzz"
//invariant forall i :: 0 <= i < n ==> i % 3 != 0 && i % 5 != 0 ==> ret[i] == i
{
var curr : string;
curr := "";
var curr_modified : bool;
curr_modified := false;
if j % 3 == 0 {
curr := curr + "fizz";
curr_modified := true;
}
if j % 5 == 0 {
curr := curr + "buzz";
curr_modified := true;
}
if curr_modified {
ret[j] := curr;
} else {
ret[j] := "TODO: convert j to a string";
}
j := j + 1;
}
Here, our implementation really no longer looks like our specification. To
mutably build the string when j is divisible by 3 and/or 5 means we never
explicitly check divisibility by 15. Also, we created a new curr_modified
piece of state, but our spec says nothing about it existing. This sort of code
refactoring is always a dangerous process - there’s always a chance that your
modifications actually changed the program semantics, and if your test suite
isn’t comprehensive then maybe you’ve introduced a bug.
The good news is that Dafny can still prove that this gnarlier implementation
satisifes our existing set of straighgforward postconditions and loop
invariants! And if we got it wrong – say, if I wrote else if j % 5 == 0
or
swapped the order that the two modulus checks execute – then the compiler
would tell us that we broke our solution and we could ask it for a
counterexample!
The home stretch…or are we?
We never actually ran the executable that Dafny proved correct for us, so why don’t we do that now? Dafny, by default, generates .NET assemblies but can also extract to Java .class files and even Golang code, which is pretty neat. But despite being nominally “verified”, the output isn’t quite what we want:
$ dotnet Fizzbuzz.dll | head -n 20
fizzbuzz
TODO: convert j to a string
TODO: convert j to a string
fizz
TODO: convert j to a string
buzz
fizz
TODO: convert j to a string
TODO: convert j to a string
fizz
buzz
TODO: convert j to a string
fizz
TODO: convert j to a string
TODO: convert j to a string
fizzbuzz
TODO: convert j to a string
TODO: convert j to a string
fizz
TODO: convert j to a string
$
This is because our implementation isn’t complete! We still need a way of converting our loop counter into a string, remember? And because our specification is not complete, either - remember that we commented out the final postcondition - Dafny had no way of knowing that our implementation is wrong. It’s important to remember that software verification isn’t a panacea; if our specification can’t be trusted to validate the right thing, we have no business assuming that it means anything that our implementation conforms to that specification.
Don’t worry, though, we’ll close the loop on this gimmick by completing
Fizzbuzz()
in the next
post, where we’ll have to
implement a (verified!) helper method to convert a natural number to a string.
Hey, that’s not a bad interview question in its own right!!
Your turn - modify the spec
Eagle-eyed readers may notice that our specification of fizzbuzz slightly
deviates from the standard one: typically, Fizzbuzz(n)
iterates on [1, n]
,
so the beginning lines read “1”, “2”, “fizz”, and so on. We instead iterate
from [0, n)
, so our final implementation will begin with “fizzbuzz”, “1”,
“2”, … Why not try installing Dafny (there’s a good installation guide
here)
and try your hand at changing first the specification, then the implementation?
If you do, let me know how it went for you.
Thanks
Thanks to Scott Andreas, Rustan Leino, Brian Lin, Kelly Shortridge, Paul Vanetti, and Leif Walsh for their feedback on early drafts of this post!