In Software Engineering it is considered bad form to specify a function by pseudocode or by a reference implementation; a proper specification is supposed to be declarative. This blanket preference for the declarative assumes that such specifications are always better: easier to understand and to write. This is indeed the case with prime numbers, but not always. In the case of the Reef Knot, the Bowline, or any other kind of knot, a declarative definition, though perhaps possible, is likely to be less useful than being shown how to make one, which is procedural. Here I consider an example where it is easy to compare the merits of the Good (declarative), the Bad (procedural), and an even less respectable third approach.
The Codebreaker tries to replicate the code on his side of the board. Each of these attempts takes the form of a probe, a sequence of n code pegs. On the Codebreaker's side there is room for several of such probes. For each probe the Codemaker determines a score, which indicates how close the probe is to the code. The score consists of a set of "key pegs", each of which is black or white. We are here concerned with specifying the scoring function, a function that maps a pair consisting of a code and a probe to the appropriate set of key pegs.
Suppose now that I want to write a program to compute the score from a given code and a given probe. Proper Software Engineering requires me to write a specification first so as to make it easier to write the program. Below you find a C program, which I find easy to write and to read. I have also made an attempt to write a declarative specification, which I find to be neither easy to write nor to read. This does not, of course prove that the Software Engineering method is wrong: I may just be singularly inept at writing specifications. All I can say is that the one shown below is the best I could come up with.
What is the basis of both of the program and of the specification? All we have to go on are the directions to prospective players given by Parker Bros., the manufacturer of the game:
Black key pegs are placed by the Codemaker in any of the key peg holes for every code peg placed by the Codebreaker which has the colour and is in exactly the same position as one of the code pegs behind the shield.Consider the following situation:White key pegs are placed by the Codemaker in any of the key peg holes when any hidden code peg behind the shield matches the Codebreaker's code pegs in colour only but not in position.
Codemaker B Y G B Codebreaker Y W Y YHere the Codebreaker has three yellow (Y) code pegs, all of which "match the Codemaker's code pegs in colour only, but not in position". Or is it only one?
The Parker Bros. are apparently aware of the difficulty, but have given up on trying to formulate an adequate description, because they add, rather lamely,
Example: If one red code peg is behind the shield and the Codebreaker places two red code pegs in the [sic] position, ONE white key peg is used.The rule as formulated by Parker Bros has a declarative flavour. I will try harder than they have done in attempting to give an adequate declarative description. For comparison, I will also give procedural descriptions, in English as well as in the C programming language. All of these will be more complex and harder to read than the inadequate one by Parker Bros.
Finally, I will address the phenomenon that the customers of Parker Bros have no trouble in scoring. This is remarkable: Parker Bros does not tell them, so how do they know? Why do I think I know? Although it is not in the given description, we all seem to agree, and I bet Parker Bros also agrees.
That's a mystery.
typedef enum{white, green, yellow, red, black, blue} colour; typedef enum{false, true} bool; typedef struct{int blacks; int whites;} score;score scoreFn(colour code[], colour probe[], int n) { score s = {0, 0}; bool cc[n]; // characteristic function for subset of code[] bool pp[n]; // characteristic function for subset of probe[] for(int i=0; i < n; i++) { if (code[i] == probe[i]) { s.blacks++; cc[i] = pp[i] = false; // this pair is counted; remove it } else cc[i] = pp[i] = true; // check this pair later for white scoring peg } for(int i=0; i < n; i++) { for(int j=0; j < n; j++) { if (cc[i] == true && pp[j] == true // both pegs present && code[i] == probe[j]) { s.whites++; cc[i] = pp[j] = false; // remove both pegs } } } return s; }
A strong match between c and p is a pair [ci, pi] such that ci = pi.
Two pairs [ci, pj] and [ck, pl] are disjoint iff i and k are not equal and j and l are not equal.
A weak match between c and p is a pair [ci, pj] such that it is disjoint from any strong pair and such that ci = pj.
b(c,p) is the number of strong matches between c and p.
Lemma: All maximal sets of mutually disjoint weak matches have the same size.
w(c,p) is the size of a maximal set of mutually disjoint weak matches.
It may be objected that examples can never be more than a hand-waving substitute for rules, justified only in situations where those whose job it is to emit the knowledge are too stupid to write rules or where those who need to absorb the knowledge are too stupid to understand them. It may be that examples can unambiguously determine the simplest function that is exemplified by the given examples.
I can imagine that we order all descriptions compatible with a given set of examples and find that the one with least Kolmogorov complexity is much simpler than the next simplest. Then we take this as the one that is defined by the examples. I can also imagine that our brains are so wired that we jump to the conclusion that this is the intended description.