Four ideas behind a language.
Advanced Programming studies the machinery that most courses take for granted: how components are discovered and wired at run time, how a program inspects its own classes, how a type system decides what an abstraction may promise, and how a dynamic language can be extended without touching its interpreter. The two assignments of the course turn each idea into an exercise, in the language where the idea is most at home.
bound and constrained propertiesThe Matching Pairs card game, built from components that cooperate only through events.
@Testable · @SpecificationA tool that loads a class at run time and tests the methods its annotations describe.
Eq · FoldableA collection type with its own invariant, its type-class instances and a test on anagrams.
inspect.signatureThree decorators that compare the type hints of a function with the arguments of every call.
Components that only talk through events.
Eight cards lie face down and the player turns two face up at every turn: a pair is removed from play, two different values go back down. The exercise is not the game but its architecture. The Board, the Cards, the Controller, the Counter and the Challenge are JavaBeans, and once the Board has wired them at startup they never call each other: they publish events and listen to them, following the Observer pattern.
- Shuffle. The Board fires a
ShuffleEventwith N values, each repeated twice, and every card takes the value at its own position. - A turn. Each card turned face up fires a property change: the Controller records v1 and v2, the Counter counts a move, and half a second after the second card the Controller fires a
MatchedEvent. - Best score. The Challenge follows the moves published by the Counter and the end of the round published by the Controller, so it never calls another bean.
The state of a card is bound and constrained: before every change the card asks its vetoable listeners for permission, and after the change it notifies the bound ones. That is where the rules live. A click never decides anything by itself: it only asks for a change, and the Controller vetoes the ones the rules forbid.
| Requested change | Allowed when | Typical vetoed request |
|---|---|---|
| face down → face up | fewer than two cards are face up | a third card during the delay |
| face up → excluded or face down | the matched event is being delivered | a click on a face-up card |
| excluded or face up → face down | a new round is starting | a click on an excluded card |
// Card: every change of state goes through herepublic void setState(CardState newState) throws PropertyVetoException { CardState oldState = this.state; if (newState == oldState) { return; // nothing changes, so no event is fired } fireVetoableChange("state", oldState, newState); // constrained: listeners may veto this.state = newState; updateAppearance(); firePropertyChange("state", oldState, newState); // bound: listeners are notified}// Controller: the rules of the game, written as vetoespublic void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { CardState from = (CardState) evt.getOldValue(); CardState to = (CardState) evt.getNewValue(); if (to == CardState.FACE_UP) { if (cardsUp >= 2) { throw new PropertyVetoException("Two cards are already face up", evt); } return; } if (resolving || newRound) { return; // the Controller itself is turning the card down or excluding it } throw new PropertyVetoException(from == CardState.EXCLUDED ? "The card is excluded from the game" : "A face-up card cannot be turned down by clicking it", evt);}
The half-second delay is a javax.swing.Timer, whose action runs on the Event Dispatch Thread like every click: the beans never share state with another thread, so they need no locks. Both optional extensions are in. The number of cards is a constant N of the Board, which passes it to the other beans at run time, so changing it requires recompiling the Board only; and the Challenge label keeps the best score. As the course instructions ask, each bean is also delivered in its own archive, renamed from .jar to .raj because Gmail filters out .jar attachments.
Tests written as annotations.
RunTests receives the name of a class, loads it with the Reflection API, creates an instance and invokes every non-private instance method marked @Testable, with the arguments and the expected result written in its @Specification:
@Testable@Specification(argTypes = {"int", "int"}, argValues = {"7", "3"}, resType = "int", resVal = "3")public int testMin(int a, int b) { return (a < b) ? a : b;}
Every method is reported exactly once, with one of four outcomes. In the first two the method is never invoked:
| Outcome | When | Example from the provided tests |
|---|---|---|
| wrong arguments | the arguments do not fit the parameters | one argument for two parameters |
| wrong result type | the expected result does not fit the return type | an int from a void method |
| failed | another result, or an exception | 7 as the minimum of 7 and 3 |
| succeeded | the expected result | 3 as the minimum of 7 and 3 |
private static void runTest(Object instance, Method method) { Specification spec = method.getAnnotation(Specification.class); if (spec == null) { spec = DEFAULT_SPECIFICATION; } Object[] arguments; try { arguments = convertArguments(method, spec); } catch (NotTestable e) { Report.report(Report.TEST_RESULT.WrongArgs, method.getName(), spec); return; } Object expected; try { expected = convertExpectedResult(method, spec); } catch (NotTestable e) { Report.report(Report.TEST_RESULT.WrongResultType, method.getName(), spec); return; } Object actual; try { method.setAccessible(true); // protected and package-private methods are tested too actual = method.invoke(instance, arguments); } catch (InvocationTargetException e) { // The method was invoked but threw an exception instead of returning the expected result. Report.report(Report.TEST_RESULT.TestFailed, method.getName(), spec); return; } catch (IllegalAccessException | RuntimeException e) { // The invocation itself failed, so the method could not be tested with these arguments. Report.report(Report.TEST_RESULT.WrongArgs, method.getName(), spec); return; } boolean success = (expected == null) ? actual == null : expected.equals(actual); Report.report(success ? Report.TEST_RESULT.TestSucceeded : Report.TEST_RESULT.TestFailed, method.getName(), spec);}
One detail decided whether the output would match exactly. getDeclaredMethods() returns the methods in no particular order, while the expected outputs follow the source file. Since javac writes the method table of a class file in declaration order, RunTests reads that table directly from the bytecode: it walks the constant pool, skips the interfaces and the fields, and numbers the methods as they appear.
A type that keeps its promises.
A multiset is a collection where order does not count and elements can repeat. The assignment fixes its representation, a list of (element, multiplicity) pairs, and asks every operation to keep it well-formed: positive multiplicities and no repeated element. A single helper adds k occurrences of an element, so insertion, union and mapping all preserve the invariant by construction.
data MSet a = MS [(a, Int)] deriving (Show)-- Adds k > 0 occurrences of an element: the invariant holds by construction.addMany :: Eq a => MSet a -> a -> Int -> MSet aaddMany (MS xs) v k = MS (insert xs) where insert [] = [(v, k)] insert ((x, n) : rest) | x == v = (x, n + k) : rest | otherwise = (x, n) : insert restunion :: Eq a => MSet a -> MSet a -> MSet aunion (MS xs) m2 = foldl (\acc (x, n) -> addMany acc x n) m2 xs-- Folding visits each distinct element once, ignoring the multiplicities.instance Foldable MSet where foldr f z (MS xs) = foldr (\(x, _) acc -> f x acc) z xs
fmap must work for every result type, but merging two elements that a function maps to the same value means comparing them, that is, the constraint Eq b, which the Functor class cannot express. Without the merge, mapping a two-element multiset onto a constant would return the same element twice.The test reads four word lists and replaces every word with its ciao, its letters in alphabetical order, so that anagrams become the same element:
| Multiset | File | Words | Distinct anagram classes |
|---|---|---|---|
| m1 | anagram.txt | 870 | 245 |
| m2 | anagram-s1.txt | 450 | 208 |
| m3 | anagram-s2.txt | 420 | 198 |
| m4 | margana2.txt | 1,740 | 245 |
Both facts required by the assignment hold: m1 and m4 are different but have the same elements, since every multiplicity in m4 is twice the one in m1, and m1 is the union of m2 and m3.
Type hints, enforced at run time.
Python ignores type hints when it runs. The assignment tells the story of a lecturer who wrote three decorators to check them, lost the file and kept only the tests and their logs: the job is to rewrite the decorators so that the tests print the same output again.
every callPrints the hint and the actual value, with its type, of every argument and of the result.
only mismatchesPrints only the disagreements between values and hints, identifying parameters by position.
bounded blockingAlso blocks a wrongly typed call, returning None, at most n times; then lets it run and checks the result.
int | str · -> NoneUnion hints and functions that return nothing are handled; a hint that cannot be checked is skipped.
def bb_type_check(max_block): def decorator(func): signature = inspect.signature(func) params = list(signature.parameters.values()) remaining_blocks = max_block @wraps(func) def wrapper(*args): nonlocal remaining_blocks if _check_arguments(params, args) and remaining_blocks > 0: remaining_blocks -= 1 print(f"Function blocked. Remaining blocks: {remaining_blocks}") return None result = func(*args) _check_result(signature, result) return result return wrapper return decorator
Every expected output, byte for byte.
Where an assignment provides an expected output, “essentially the same” was read in the strictest sense: the output is compared with diff, character by character. The repository keeps the files provided with the assignments, so anyone can repeat every comparison.
| Exercise | What was checked | Result |
|---|---|---|
| Java Beans | 43 rules played through the real Board, with N = 4 and N = 6; each archive loaded on its own | all passed |
| Reflection | RunTests on the two provided classes, against the expected outputs | 2 / 2 identical |
| Haskell | compilation with all warnings as errors; the anagram facts; 17 laws on 76,494 cases | all passed |
| Python | the three test programs against their logs | 3 / 3 identical |
Behind the language, byte for byte.
The four solutions are on GitHub together with the files provided by the course, a README for every exercise and the report that explains each design choice.