In Richard Gabriel's essay “Worse is Better ”, Gabriel contrasts the “MIT approach” of designing for correctness and completeness with the “New Jersey approach” of designing for simplicity of implementation. He argues that the MIT approach, which is more principled, is likely to be overtaken by the New Jersey approach, which is easier to reproduce. While writing the prompt for the Advent of Code problems, I noticed that my prompt was reminiscent of Gabriel's characterizations. I decided to get the LLM to compare the two approaches by running it on each type of prompt and seeing how the output differed.
Prompts
The salient part of the MIT prompt is
As an Elite Common Lisp Developer, your unwavering and paramount mission is to design and meticulously craft Common Lisp programs that are not only correct, but also simple, efficient, and robust. Your programs are not mere instructions; they are masterpieces of clarity and precision, firmly grounded in these foundational, non-negotiable principles:
- Correctness Your program must be correct in all observable aspects. Incorrectness is simply not allowed.
- Consistency Your program must be consistent. Consistency is as important as correctness. Simplicity may be sacrificed for consistency and correctness.
- Simplicity Your program must be as simple as possible, but no simpler. It is more important for the interface to be simple than the implementation. Simplicity is not allowed to overly reduce completeness.
- Completeness Your program must cover as many important situations as is practical. All reasonably expected situations must be handled.
The salient part of the New Jersey prompt is
As an Expert Common Lisp Developer, your primary and overriding objective is to create Common Lisp programs that are simple. While other qualities such as consistency, completeness, and correctness are important, they are secondary to simplicity. Your programs should be designed with the following prioritized principles in mind:
- Simplicity Your program must be simple, both in interface and implementation. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in the design.
- Correctness Your program must be correct in all observable aspects. It is slightly better that your program be simple than correct.
- Consistency Your program must not be overly inconsistent. Consistency can be sacrificed for simplicity in some cases, but it is better to drop those parts of the design that deal with less common circumstances than to introduce either implementational complexity or inconsistency.
- Completeness Your program must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must be sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.
Both prompts had additional instructions about how to find the input and where to write the output.
Results
I had Gemini analyze the solution pairs to the Advent of Code problems. In almost every case, Gemini preferred the MIT approach to the New Jersey approach. Here is an example analysis of day 10:
Excellent. I have the contents of both files. I will now proceed with a detailed analysis and comparison, as requested.
Analysis of day10_right_thing_solution.lisp
This solution presents a very clean, structured, and pragmatic approach. It reads as a piece of code written by a developer who values clarity and standard algorithmic patterns.
Structure:
-
The code is well-organized into distinct functions, each with a single, clear purpose:
parse-input
,find-trailheads
,get-neighbors
,calculate-score
(for Part 1),calculate-rating
(for Part 2), and a mainsolve
function to orchestrate the process. - This separation of concerns makes the code easy to read, test, and debug. The main
solve
function provides a high-level summary of the program's execution flow.
Algorithms & Style:
-
Part 1 (
calculate-score
): This function uses a classic iterative Breadth-First Search (BFS). The use of a queue (q
) and avisited
hash table is a textbook implementation for graph traversal. This approach is highly efficient and guarantees finding the shortest path in terms of steps, though that is not a requirement here. It is robust and not susceptible to stack depth limits. - Part 2 (
calculate-rating
): This function employs a bottom-up dynamic programming strategy. It builds apath-counts
table, iterating through the heights from 1 to 9. For each cell, it calculates the number of paths to it by summing the paths from its valid, lower neighbors. This is a very efficient and elegant method that avoids the combinatorial explosion of a naive search. - Lisp Idioms: The code uses common and readable Lisp constructs like
loop
,dotimes
,destructuring-bind
, andlet*
. The use of hash tables forvisited
sets is appropriate and performant.
Strengths:
- Clarity and Readability: The code is exceptionally clear. Function names are descriptive, and the logic is straightforward and easy to follow.
- Robustness: The iterative algorithms are safe from stack overflow errors, making the solution suitable for arbitrarily large inputs.
- Efficiency: Both the BFS and the bottom-up DP are highly efficient algorithms for their respective problems.
Weaknesses:
- Minor Environmental Dependency: It relies on
a placeholder
readFileLines
function, making it slightly less portable out-of-the-box compared to a solution using a standard library like UIOP.
Analysis of day10_new_jersey_solution.lisp
This solution uses a recursive approach, which can be very elegant but also carries certain trade-offs. It feels more like a direct translation of a mathematical recurrence relation.
Structure:- The overall structure is similar, with parsing and solving functions. However, the core algorithmic logic is encapsulated within local functions defined by
labels
. - This makes the solver functions more self-contained but also increases their cognitive complexity, as the main logic is nested one level deeper.
- Part 1 (
solve-part1
): This function uses a recursive Depth-First Search (DFS) with memoization. Thefind-reachable-nines
local function explores paths recursively. Thememo
hash table is crucial to prevent re-computation and avoid infinite loops, effectively turning the search into a dynamic programming solution on a graph. The use ofunion
to merge results from recursive calls can be inefficient, as it requires traversing lists. - Part 2 (
solve-part2
): This also uses a recursive DFS with memoization (top-down dynamic programming). Thecount-paths
function is a classic example of this pattern. It computes the number of paths from a given cell and stores the result in thememo
table before returning it. - Lisp Idioms: The use of
labels
for recursion is a very common and powerful Lisp idiom. The use ofcons
cells for coordinates is also common, though perhaps slightly less explicit than alist
. It correctly usesuiop:read-file-lines
for better portability.
- Elegance of Recursion: For those comfortable with recursion, the code can be seen as a very direct and elegant expression of the problem's structure.
- Portability: The use of
uiop
for file reading is a good practice.
- Stack Overflow Risk: The primary weakness is its reliance on deep recursion. For a sufficiently large or complex input grid, this solution could exceed the call stack limit and crash, whereas an iterative solution would not.
- Lower Readability: The combination of recursion, memoization, and nested functions can make the logic harder to trace and debug compared to the straightforward iterative loops in the first solution.
- Potential Inefficiency: The use of
union
in Part 1 is likely less performant than the hash-table approach used in the first solution for tracking unique reachable nodes.
Feature | right_thing_solution |
new_jersey_solution |
---|---|---|
Paradigm | Iterative (BFS, Bottom-up DP) | Recursive (DFS with Memoization) |
Robustness | Superior. Immune to stack overflow. | Weaker. Vulnerable to stack overflow. |
Clarity | Superior. Clear, flat structure. Standard algorithms. | Good, but more complex. Nested logic can be harder to follow. |
Performance | Excellent. Efficient, standard algorithms. | Good, but recursion overhead and union may be slower. |
Style | Pragmatic, clear, maintainable. | Elegant, concise, mathematical. |
While both files represent valid and functional solutions to the
problem, I unequivocally
recommend day10_right_thing_solution.lisp
as the better overall solution.
Justification:
- Robustness is Paramount: The most critical advantage of the "Right Thing" solution is its immunity to stack overflow errors. This makes it fundamentally more reliable and suitable for general-purpose use where input constraints may not be known. In a professional software engineering context, this is a decisive factor.
- Clarity Enhances Maintainability: The code in the "Right Thing" solution is significantly easier to read, understand, and maintain. A developer can grasp the BFS and bottom-up DP algorithms quickly, making debugging and future modifications simpler. The recursive nature of the "New Jersey" solution, while elegant, requires more mental effort to trace.
- Efficiency and Predictability: The iterative approach offers more predictable performance. The BFS algorithm and the systematic table-filling of the DP solution have well-understood and excellent time complexities, free from the overhead of function calls inherent in recursion.
In conclusion, the "New Jersey" solution is a fine example of a recursive, top-down DP approach. However, the "Right Thing" solution embodies a superior engineering strategy, prioritizing robustness, clarity, and maintainability, which are the hallmarks of high-quality, professional-grade code. It is the solution I would write and the one I would want to inherit in a team setting.
No comments:
Post a Comment