Selected work · 09 / 10Programming languages

Advanced Programming in Java, Haskell & Python

Four exercises on what happens behind a programming language: components that cooperate only through events, a test runner driven by annotations, a Haskell type that keeps its own invariant, and decorators that make Python check its type hints. Every output the assignments expect is reproduced byte for byte.

Pairs found 0 / 4Moves 0Best –
Matching Pairs, live. Click to play.
face downface upexcludedvetoed move
4exercises, one idea of the course each
2 / 2RunTests outputs identical to the expected ones
3 / 3Python logs reproduced byte for byte
43game rules checked through the real interface
76,494multiset cases checked exhaustively
01 / The course

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.

Java Beansbound and constrained properties

The Matching Pairs card game, built from components that cooperate only through events.

Reflection and annotations@Testable · @Specification

A tool that loads a class at run time and tests the methods its annotations describe.

Haskell multisetsEq · Foldable

A collection type with its own invariant, its type-class instances and a test on anagrams.

Python decoratorsinspect.signature

Three decorators that compare the type hints of a function with the arguments of every call.

02 / Java Beans

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 ShuffleEvent with 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 changeAllowed whenTypical vetoed request
face down → face upfewer than two cards are face upa third card during the delay
face up → excluded or face downthe matched event is being delivereda click on a face-up card
excluded or face up → face downa new round is startinga click on an excluded card
Card.java · setState() · Controller.java · vetoableChange()
// 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);}
Excerpts from the two beans, with two guard checks omitted. Swing's JComponent already implements bound and constrained properties, so the beans rely on it instead of keeping their own listener lists.

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.

03 / Reflection

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:

MathOpsTests.java · testMin()
@Testable@Specification(argTypes = {"int", "int"}, argValues = {"7", "3"}, resType = "int", resVal = "3")public int testMin(int a, int b) {    return (a < b) ? a : b;}
A method of one of the test classes provided with the assignment: called with 7 and 3, it must return the integer 3.

Every method is reported exactly once, with one of four outcomes. In the first two the method is never invoked:

OutcomeWhenExample from the provided tests
wrong argumentsthe arguments do not fit the parametersone argument for two parameters
wrong result typethe expected result does not fit the return typean int from a void method
failedanother result, or an exception7 as the minimum of 7 and 3
succeededthe expected result3 as the minimum of 7 and 3
RunTests.java · runTest()
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);}
The arguments are checked first, then the expected result, and only then is the method invoked.

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.

2 / 2outputs identical to the expected ones
11methods tested in the larger provided class
17kinds of constant-pool entries parsed
04 / Haskell

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.

MultiSet.hs · addMany, union, Foldable
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
From the module, with the comments condensed. Every signature in the module is exactly the type that GHC infers.
Why it cannot be a Functor. 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:

MultisetFileWordsDistinct anagram classes
m1anagram.txt870245
m2anagram-s1.txt450208
m3anagram-s2.txt420198
m4margana2.txt1,740245

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.

05 / Python

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.

print_typesevery call

Prints the hint and the actual value, with its type, of every argument and of the result.

type_checkonly mismatches

Prints only the disagreements between values and hints, identifying parameters by position.

bb_type_check(n)bounded blocking

Also blocks a wrongly typed call, returning None, at most n times; then lets it run and checks the result.

Unions and Noneint | str · -> None

Union hints and functions that return nothing are handled; a hint that cannot be checked is skipped.

type_checking_decorators.py · bb_type_check()
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
A decorator factory: the signature is read once, when the function is decorated, and the counter of remaining blocks lives in the closure of the decorated function.
06 / Verification

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.

ExerciseWhat was checkedResult
Java Beans43 rules played through the real Board, with N = 4 and N = 6; each archive loaded on its ownall passed
ReflectionRunTests on the two provided classes, against the expected outputs2 / 2 identical
Haskellcompilation with all warnings as errors; the anagram facts; 17 laws on 76,494 casesall passed
Pythonthe three test programs against their logs3 / 3 identical
Checked again for this page. Compiled with JDK 21 and GHC 9.10 and run with Python 3.14, the two RunTests outputs and the three Python logs still match the expected files exactly, and the anagram test confirms both facts.

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.

Next case study · 10 / 10Concurrent Programming in Pure C