Are we there yet?

Continuing with the work on tooling support for interactive & fun development with Python.

A while ago, I wrote an article about my current attempts to make development in Python more interactive, more "test" driven and more fun.

My North Star is the developer experience in Clojure, where you have everything at your fingertips. Evaluating expressions (i.e. code) is a common thing for Lisp languages in general. I've found that it is sometimes difficult to explain this thing to fellow developers with no experience from Clojure development. The REPL is in most languages an external thing you do in a terminal window, detached from the work you usually do in your IDE. But that's not the case with REPL Driven Development (RDD).

Along the way, I have learned how to write and evaluate Python code within the code editor by using already existing tools and how to configure them. Here's my first post and second post (with setup guides) about it. You'll find information about the basic idea with RDD and guides on how to setup your IDE or code editor for Python development.

Can it be improved?

This has been the way I have done Python development most of the time, described in the posts above. But I have always wanted to find ways to improve this workflow, such as actually see the evaluated result in an overlay right next to the code. Not too long ago, I developed support for it in Emacs and it has worked really well! You can read about it and see examples of it here.

What about AI?

While writing Python code, and doing the RDD workflow, there's often a manual step to add some test or example data to variables and function input parameters. Recently, I got the idea to automate it using an LLM. I wrote a simple code editor command that prompts an AI to generate random (but relevant) values and then populate the variables with those. Nowadays, when I want to test Python code, I can prepare it with example data in a simple way by using a key combination. And then evaluate the code as before.

Are we there yet?

One important thing with RDD, that I haven't been able to figure out until now, is how to modify and evaluate code from an actual running program. This is how things are done in Clojure, you write code and have the running app, service, software constantly being changed while you develop it. Without any restarts. It is modified while it is running. Tools like NRepl does this in the background, with a client-server kind of architecture. I haven't dig that deep into how NRepl works, but believe it is similar to LSP (Language Server Protocol).

The workflow of changing a running program is really cool and something I've only seen before as a Clojure developer. So far I have used IPython as the underlying tool for REPL Driven Development (as described in the posts above).

A solution: the Kernel

In Python, we have something similar to NRepl: Jupyter. Many developers use Notebooks for interactive programming, and that is closely related to what I am trying to achieve. With a standard REPL session, you can add, remove and modify the Python code that lives in the session. That's great. But a sesssion is not the same as an actual running program.

A cool thing with Jupyter is that you can start a Python Kernel that clients can connect to. A client can be a shell, a Notebook - or a Code Editor.

jupyter kernel

By running the jupyter kernel command, you have a running Python program and can interactively add things to it, such as initiating and starting up a Python backend REST service. Being able to connect to the Jupyter kernel is very useful. While connected to the kernel, you can add, remove, modify the Python code - and the kernel will keep on running. This means that you can modify and test your REST service, without any restarts. With this, we are doing truly interactive Python programming. You will get instant feedback on the code you write, by evaluating the code in your editor or when testing the endpoint from a browser or shell.

"Dude, ever heard about debugging?"

Yes, of course. Debugging is closly related to this workflow, but it is not the same thing. Debugging is usually a one way flow. A timeline: you run code, pause the execution with breakpoints where you can inspect things, and then continue until the request is finalized. The RDD workflow, with modifying and evaluating a running program, doesn't have a one-way timeline. It's timeless. And you don't add breakpoints.

REPL Driven tooling support

I have developed tooling support for this new thing with connecting to a Jupyter Kernel, and so far it works well! Python is different from languages like Clojure: namespaces are relative to where the actual module is placed in the folder structure of a repo. This means that those connected to a Kernel need to have the full namespace (i.e. the full python path) for the items to inspect. This is what I have added in the tooling, so I can keep the RDD flow with the nice overlays and all.

I am an Emacs user, and the tooling I've written is for Emacs. But it shouldn't be too difficult to add it to your favorite code editor. Have a look at the code. You might even learn some Lisp along the way.


Resources

Top Photo by David Vujic

Permalink

Practice working with Clojure vectors

Vector

A vector in Clojure is an ordered, indexed, immutable collection that allows efficient random access and fast additions at the end. They are immutable.

Vectors are defined using square brackets [] or by using the vector function:

;; Creating vectors
[1 2 3 4]          ;; Literal syntax
(vector 1 2 3 4)   ;; Using vector function

Key Characteristics of Vectors:

  • Indexed (O(1) access time): You can retrieve elements using their index.

  • Ordered: Elements maintain the order in which they were inserted.

  • Immutable: Any modification creates a new vector instead of mutating the original.

  • Efficient addition/removal at the end: Adding elements to the end is fast (conj).

Usage & Operations:

Access Elements by Index

(def v [10 20 30 40])
(nth v 2)      ;; => 30
(get v 1)      ;; => 20
(v 3)          ;; => 40 (Vectors can be used as functions for indexing)

Adding Elements

(conj [1 2 3] 4) ;; => [1 2 3 4] (adds to the end)

Updating Elements

(assoc [1 2 3] 1 99) ;; => [1 99 3] (Replaces index 1 with 99)

Removing Elements

Vectors do not have direct remove operations, but you can filter or use subvec:

(vec (remove #(= % 2) [1 2 3])) ;; => [1 3] (Removes 2)
(subvec [1 2 3 4] 1 3)          ;; => [2 3] (Slices vector from index 1 to 2)

Iterating Over a Vector

(map inc [1 2 3]) ;; => (2 3 4)
(filter even? [1 2 3 4]) ;; => (2 4)

Sorting a Vector

(sort [3 1 4 2])  ;; => (1 2 3 4)
(sort-by count ["apple" "kiwi" "banana"]) ;; => ("kiwi" "apple" "banana") (Sort by length)

Task: Managing a Task List with Vector

Scenario:

You are building a simple task management system that stores tasks in a vector. Each task is represented as a map with the following keys:

  • :id (unique task identifier)
  • :title (task description)
  • :priority (one of :low, :medium, :high)
  • :completed? (boolean indicating if the task is done)

Goal:
Implement functions that allow you to manage tasks in the system:

  • Initialize Task List – Create a vector of sample tasks.

  • Add a Task – Insert a new task into the task list.

  • Remove a Task – Remove a task by its :id.

  • Mark a Task as Completed – Update a task’s :completed? status to true.

  • List All Pending Tasks – Return only tasks where :completed? is false.

  • Find Tasks by Priority – Retrieve all tasks with a specific priority (:low, :medium, or :high).

  • Sort Tasks by Priority – Sort the vector so :high priority tasks come first, followed by :medium, then :low.

  • Get Next Task – Return the highest-priority pending task that has not been completed.

Solution

;; Managing a Task

;; Initialize Task List
(defn init-task-list []
  (hash-map {})
  )

(defn valid-priority [priority]
  (or (= :low priority) (= :medium priority) (= :high priority))
  )


(defn create-task [id title desc priority]
  (when (valid-priority priority)
    {:id id :title title :description desc :priority priority :completed? false}
    )
  )

(defn task-not-exist? [task-list title]
  (= 0 (count (filter #(= (:title %)  title) task-list))
  ))

;; Add a Task
(defn add-task [task-list title desc priority]
  (when (task-not-exist? task-list title)
    (let [last-task-id (:id (last task-list) 0)
          next-task-id (inc last-task-id)
          task (create-task next-task-id title desc priority)]
      (conj task-list task)
      )
    )
  )

;; Remove a Task
(defn remove-task [task-list task-id]
  (remove #(= (:id %) task-id) task-list)
  )

;; Mark a Task as Completed
(defn mark-as-completed [task-list task-id]
  (let [task (first (filter #(= (% :id) task-id) task-list))
        index-of-task (index-of task-list task)]
    (when (not (nil? task))
      (assoc task-list index-of-task (assoc task :completed? true))
      )
    )
  )

;; List All Pending Tasks
(defn pending-tasks [task-list]
  (filter #(not (:completed? %)) task-list)
  )


;; Find Tasks by Priority
(defn filter-by-priority [task-list priority]
  (when (valid-priority priority)
    (filter #(= (:priority %) priority) task-list)
    )
  )

(defn priority-count [priority]
  (cond
    (= priority :high) 3
    (= priority :medium) 2
    (= priority :low) 2
    :else 0
    )
  )

(defn priority-higher-comparator [el1 el2] ()
  (let [priority1 (priority-count (el1 :priority))
        priority2 (priority-count (el2 :priority))]
    (compare priority2 priority1)
    )
  )

;; Sort Tasks by Priority
(defn sort-by-priority [task-list]
  (sort priority-higher-comparator task-list)
  )

;; Get Next Task
(defn next-task [task-list]
  (sort-by-priority (pending-tasks task-list))
  )

Permalink

An Animated Introduction to Programming in C++ [Full Course]

This post is a comprehensive introduction to programming in C++. You don't need to have any previous programming experience in order to begin. Along the way, you will learn about the flow of control, variables, conditional statements, loops, arrays, functions, structured data, pointers and dynamic memory, classes, common data structures, and working with databases.

There are practice problems in each section so that you can practice while learning from the content. These are in the 'Hands-On Practice' section in each section. Integrated Development Environments (IDEs) are tools that allow you to write your own programs. There are some great, free C++ IDEs out there like Visual Studio, Xcode, and CLion. The simplest way to get started is to use a web-based IDE. replit works great, use it if you are unable to install one of the other IDEs.

Code Playbacks

This material will not be delivered like traditional online tutorials or video series. Each section will have links to interactive code playbacks that visually animate changes made to a program in a step-by-step manner, helping you understand how it was written.

A code playback shows how a program evolves by replaying all the steps in its development. It has an author-supplied narrative, screenshots, whiteboard-style drawings, and self-grading multiple choice questions to make the learning process more dynamic and interactive.

Here's a short YouTube video explaining how to view a code playback:

Playback Press

Playback Press is a platform for sharing interactive code walkthroughs created by Mark Mahoney, a professor of computer science. These books provide interactive programming lessons through step-by-step animations, AI tutoring, and quizzes.

If you want to see the full C++ 'book', you can go here:

An Animated Introduction to Programming in C++

by Mark Mahoney

Mark also built Storyteller, the free and open-source tool that powers code playbacks.

AI Tutor

When viewing a code playback, you can ask an AI tutor about the code. It answers questions clearly and patiently, making it a helpful resource for learners. You can also ask the AI tutor to generate new self-grading multiple-choice questions to test your knowledge of what you are learning.

To access the AI tutor and self-grading quizzes, simply create a free account on Playback Press and add the book to your bookshelf. It is still free but you do need a free account in order to access the AI features.

Table of Contents

  • Part 1: Variables
  • Part 2: Selection
  • Part 3: Looping
  • Part 4: Arrays
  • Part 5: Functions
  • Part 6: Vectors
  • Part 7: Structured Data
  • Part 8: Pointers
  • Part 9: Object-Oriented Programming
  • Part 10: Data Structures
  • Part 11: SQLite Databases

Part 1: Variables

Simply watching an experienced artist paint is not enough to say that you have learned how to become a painter. Watching an experienced artist is an important part of the learning process but you can only call yourself a painter after struggling to make your own paintings first. There are a lot of similarities between learning to paint and learning to program. The only way to truly learn programming is through practice!

So, let's get started. Follow along with the code playbacks below. Click the links below to load each code playback (it may help to open them in a new tab). Click on the playback comments on the left-hand side of the playback screen to step through the code's development.

Flow of Control

The following playback explains the flow of control in a program by describing how to print to the screen from it:

Variables and Types

This next group of programs describes declaring variables to hold data in a program. All variables have a type which specifies what can be stored in them and what operations can be performed on them.

Reading from the Keyboard

This final group of programs builds on previous concepts and shows how to prompt the user for input.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a program that prompts the user for three integers- one representing an hour, one representing a minute, and one representing a second. Next, calculate the number of seconds until midnight based on the time that was input. Print the number of seconds until midnight on the screen.

Then, prompt the user for a single integer representing the number of seconds until midnight. From that value, do the reverse calculation to find the hour, minute, and second of that time. Print it to the screen in a time format HH:MM:SS.

Part 2: Selection

This section discusses altering the flow of control with if/else statements. These statements ask the computer to evaluate whether a condition is true or false and changes the flow of control based on the answer. It also explains the data type, bool, which can hold either true or false and it shows a few examples of how to use selection with if, if/else, if/else if/else, and switch statements.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, try to write a few programs:

Problem 1

Problem 1 asks you to write a program to determine if one date comes after another. The program will ask for two sets of dates. Next, the program will determine if the first date comes before, is equal to, or comes after.

Enter in the first month: 2
Enter in the first day: 21
Enter in the first year: 2012

Enter in the second month: 2
Enter in the second day: 22
Enter in the second year: 2011

The first date comes after the second.

Problem 2

Problem 2 asks you to write a programs that prompts the user for a date and determines if that date is valid. For example, 9/19/2017 is a valid date but these are not valid dates:

4/31/2006 (only 30 days in April)
2/29/2005 (not a leap year)
16/1/2010 (invalid month)
4/59/2013 (invalid day)

If the date is correct print it out. If it is incorrect display an error message explaining why the date is not correct.

Problem 3

Problem 3 asks you to write a program that will calculate change for a sales purchase. Your program should prompt for a sales price. Validate that the data entered is a number greater than 0. If the data entered is incorrect, display an error message and end the program. Next, prompt the user for the amount that the customer will pay to the cashier. Validate that this value is greater than or equal to the sales price. If it is not, display an error message and end the program.

If the entry is correct, your program must calculate the amount of change to return to the user. Next, calculate what bills and coins that the the cashier needs to return to the customer. The fewest number of paper bills and coins should be returned. You can make change in many different combinations, however, the only correct implementation is the one that returns the fewest paper bills and coins.

Display the number of each of the bills and coins. Here is a sample run of the program:

Enter in a sales amount: $20.38
Enter in the amount the customer pays: $30.00

The change due back is $9.62

You should give the customer this change:
0 $100 bills
0 $50 bills
0 $20 bills
0 $10 bills
1 $5 bills
4 $1 bills
1 Half Dollars
0 Quarters
1 Dimes
0 Nickels
2 Pennies

Because of the way arithmetic works with float variables, storing the monetary values as floats may cause some problems. For example, if you had a float variable that held 1.29 to represent $1.29 and you subtracted the .05 from it (to represent giving back a nickel) you would think that you would be left with exactly 1.24. Unfortunately, the computer might store that value or it might store 1.2399999 or 1.2400001 instead of exactly 1.24. These very small inconsistencies can cause a problem calculating the number of pennies to return. Consider converting the amounts into ints to solve this problem.

Part 3: Looping

This group of playbacks discusses repeatedly executing the same code over and over again in a loop. They show how to create count controlled and event controlled loops with the while keyword, nested loops, a for loop, and how to exit a loop with break and continue.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a few programs:

Problem 1

Problem 1 asks you to write a program that will calculate the sum of the squares from 1 up to and including that number. For example if the user entered in the value 5 then the sum of the squares for the numbers one through five would be (1 + 4 + 9 + 16 + 25) = 55.

Your program should repeatedly calculate this value until the user enters in a value of -99. This is the sentinel value that the program uses to determine when to quit.

Enter in an integer number(ex. 10), -99 to quit: 5
The sum of the squares up to 5 is 55

Enter in an integer number(ex. 10), -99 to quit: 4
The sum of the squares up to 4 is 30

Enter in an integer number(ex. 10), -99 to quit: -99
Have a nice day!

Problem 2

Problem 2 asks you to write a program that will determine whether a number is prime or not. A prime number is any number that is evenly divisible only by the number one and itself.

7 is prime because the only numbers that divide into it without a remainder are 1 and 7.

12 is not prime because the numbers that divide into it evenly are 1, 2, 3, 4, 6, and 12.

Your program will prompt for a number and then display whether the number is prime or not. The number entered must be a positive number. Repeatedly prompt for a number until a positive number is entered.

Problem 3

Problem 3 asks you to calculate a mortgage schedule for someone thinking of buying a new house. The inputs to determine a monthly schedule are the principal loan amount and the annual interest rate. Assume this will be a conventional 30 year loan.

Your program should prompt for these inputs and find a monthly payment using this calculation:

                                   monthly interest rate                                              
monthly payment =  ------------------------------------------------- * principal
                   1 - (1 + monthly interest rate)^-number of months

Notice that you will have to calculate the monthly interest rate (the annual interest rate divided by 12.0) and number of months (360 for a 30 year loan). The ^ in this formula means raise one number to a power. There is a function called pow() which raises one number to another and returns the result. For example, if one wanted to raise 2 to the -3rd power they would do this:

float result = pow(2.0, -3.0);

After you have calculated the monthly payment create a summary of the loan characteristics. Display the loan amount, the interest rate, the monthly payment, the total amount paid for the loan, the total amount of interest paid, and the ratio of amount paid over the principal.

After you have printed the summary you can begin to make the schedule. Prompt the user for the ending month to display in the schedule. The schedule should display the month number, the monthly payment, the amount paid in principal in that month, the amount paid in interest in that month, and the amount remaining in the principal (the amount paid in principle each month is deducted from the remaining principle). A month's interest amount is equal to monthly interest rate times the remaining principal. The monthly principal is the difference between the monthly payment and the monthly interest paid. Remember to update the remaining principal after every month.

After each year of the schedule has been printed display a message with the year number.

Part 4: Arrays

This batch of programs shows how to use arrays in C/C++. An array is a collection of variables (all of the same type) that have a single name and sit next to each other in memory. Loops are almost always used to go through the elements of an array. They show how to create two and three dimensional arrays and use the random number generator in C/C++.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a few programs:

Problem 1

Problem 1 asks you to write a program that displays a menu with three options.

The first option allows the user to enter in a month number (between 1-12) and a day number within that month (1-31) and calculates the day number in the year. January 1 is day 1. January 31 is day 31. February 1 is day 32. February 28 is day 59. December 31 is day 365 (don’t worry about leap year). If the user enters an invalid combination (like February 31) the program should continuously prompt the user to enter in a new value until they enter a valid date.

The second menu option allows the user to enter in a day number (1-365) and prints out the month name and day number of that month. If the user enters in 59 the program should print out:

Day 59 is February 28

If the user enters an invalid day number the program should continuously prompt the user to enter in a new value until it is in the correct range.

The last menu option allows the user to quit the program. The menu should repeatedly be displayed until the user chooses to quit the program.

Use an array of integers to hold the number of days in each of the months. Use the array to keep a running sum to help with your day calculations. You may also want to create an array of strings with the month names.

int numDaysInMonths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

Here is a sample run of the program:

1. Enter in a month and day
2. Enter in a day number
3. Quit
Enter in a menu option: 1

Enter in a month number: 2
Enter in a day number: 1

February 1 is day 32

1. Enter in a month and day
2. Enter in a day number
3. Quit
Enter in a menu option: 2

Enter in a day number: 59
Day 59 is February 28

1. Enter in a month and day
2. Enter in a day number
3. Quit
Enter in a menu option: 3

Problem 2

Problem 2 asks you to create a program that will find the number of days in between two dates.

For example, if one would like to know how many days are in between two dates (1/1/2000) and (3/19/2030). The program must find out how many whole days are in between these two dates (include the starting and ending date).

If the two dates are in the same year then the algorithm to find the number of days may be different than if the dates are in different years. Your program must handle each case.

Here is a sample run of the program:

Enter in a start month: 1
Enter in a start day: 1
Enter in a start year: 2000

Enter in an end month: 3
Enter in an end day: 19
Enter in an end year: 2030

There are 11035 days in between 1/1/2000 and 3/19/2030

Part 5: Functions

This group of playbacks describes another flow of control altering mechanism called functions. A function is a named block of code that can be called and the flow of control will jump to it.

When calling a function some data can be passed into it (called parameters) and the function can return a piece of data when it is complete (called a return value). These playbacks discuss passing in data 'by value' versus 'by reference'. They show that every variable has a limited lifetime that it sits in memory, or scope.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a few programs:

Problem 1

Problem 1 asks you to extend the prime number problem from a previous section. Write a program that includes a function that will print all the prime numbers in a range. The program will ask for a lower bound and an upper bound and print all the primes in that range.

Problem 2

Problem 2 asks you to write a function that takes an integer year number and returns a bool whether that year is a leap year or not. The calculation for leap year is as follows:

  • most years evenly divisible by four are leap years
  • if a year is divisible by four and is a century year, like 1800 or 1900, then it is NOT a leap year
  • if a century year also happens to be divisible by 400, like 2000 or 2400, then it is a leap year

The function should look something like this:

bool isLeapYear(int year)
{
    //calculate if it is leap year or not and return true or false
}

Problem 3

Problem 3 asks you to write a program that will print a calendar for a whole year given the day that January 1st falls on. Your program will prompt for the day that January 1st falls on and the year. You must print the calendar for all 12 months of that year. The options for the first day of the year will be entered with the first three letters of each of the seven days of the week from Sunday ('sun') to Saturday ('sat').

The year does not need to be validated, but the day of the week does. You should not allow the user to enter in a value other than 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', or 'sat'.

The format of your calendar should be very similar to the example below. You must use at least one function in addition to the main function. Pass data between functions, do not use global variables.

The final requirement is that your program should only display one month at a time and then wait for some user input before continuing.

It is very important that you come up with a plan to solve this program before you begin coding (as it is with every program). Think about how you would print a single month's calendar given the day of the month that it begins on.

Sample Output:

What year do you want the calendar for?
2003

What day of the week does January 1st fall on (sun for Sunday, mon for Monday, etc..)?
s
Invalid Entry- please enter the first three letters of the day

What day of the week does January 1st fall on (sun for Sunday, mon for Monday, etc..)?
wed

    January 2003
 S  M  T  W  T  F  S
---------------------
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Do you want to continue? y

   February 2003
 S  M  T  W  T  F  S
---------------------
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28

Do you want to continue? y
...
...

Part 6: Vectors

C++ comes with the Standard Template Library (STL) which is a collection of different containers. This section will cover vectors. A vector is an array-based container that holds data very much like an array does but it is smarter. It knows how many elements are in it and it can grow as the program is running. It also shows how to read and write data from a file, efficiently search through an array-based container, sort values, and more.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a program to read in whole words from a file and store them in a vector of strings. Create a simple text file (.txt) using your coding editor. Then, add a few sentences of text to it. Strip any punctuation marks from the words and make them all lowercase letters. Only store a word in the vector if it is not already present.

Part 7: Structured Data

This section describes how to use structured data types. Structured data types allow you to group related data together so that it can be passed around easily.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a program that has a struct to represent a cell phone. Every cell phone has a model name and manufacturer along with a camera with a mega-pixel resolution. For example, one may want to store information about an Apple IPhone X with a 12 MP camera or a Google Pixel 4 with a 16 MP camera.

Your program will create three cell phone objects, fill them, and then add them to a vector. Lastly, print out the information about each cell phone on the screen.

Write a function that takes a cell phone object by reference and prompts the user to enter in the model, manufacturer, and camera resolution. Write another function that prints information about a cell phone. Write a function that takes a vector of cell phones and prints each one.

Part 8: Pointers

This section describes pointers in C/C++. A pointer is a variable that holds the address of another variable. Pointers are important because they allow us to use a special section of memory called the 'heap'. It discusses the different types of memory that can be used in a program (global, local, and dynamic).

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a program that will read a sequence of words from the keyboard and store them in a dynamic array of strings. Use the word 'quit' as the word that terminates the input. Print the words back to the screen in the order in which they were entered each on its own line. Do not store the same word twice.

Up until now the size of an array has been determined at compile time, now that you know about pointers and about the keyword new, write a program which is not restricted to selecting an upper bound at compile time for the number of words which can be read in.

One way to do this is to use new to create arrays of strings on the fly. Each time an array fills up, dynamically create an array which is twice as large, copy over the contents of the existing array to the new array, and continue (remember to delete the original array). Start with an array of 5 elements.

Here is an example of the output:

Enter in some text and end with the word quit:
This lab asks you to write a program that will read a sequence of words from the keyboard and store them in a dynamic array of strings. Use the word 'quit' as the word that terminates the input. Print the words back to the screen in the order in which they were entered each on its own line. Do not store the same word twice. quit

Doubling Array from 5 to 10
Doubling Array from 10 to 20
Doubling Array from 20 to 40
Doubling Array from 40 to 80
1. This
2. lab
3. asks
4. you
5. to
6. write
7. a
8. program
9. that
10. will
11. read
12. sequence
13. of
14. words
15. from
16. the
17. keyboard
18. and
19. store
20. them
21. in
22. dynamic
23. array
24. strings.
25. Use
26. word
27. 'quit'
28. as
29. terminates
30. input.
31. Print
32. back
33. screen
34. order
35. which
36. they
37. were
38. entered
39. each
40. on
41. its
42. own
43. line.
44. Do
45. not
46. same
47. twice.
Press any key to continue . . .

Part 9: Object-Oriented Programming

This section discusses object-oriented programming using classes in C++. A class is like a struct except that in addition to collecting data it also collects methods that work on that data. This is called encapsulation. It also discusses inheritance and polymorphism that make it easier to reuse code.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a few programs:

Problem 1

Problem 1 asks you to create a Date class to represent a date.

There should be an int for the day number, month number, and year number declared in the private section. Instead of having a setter for each of those have one method called setDate(int m, int d, int y) that sets the date. You can have a getter method for each piece of data.

Add two constructors- one that takes no data and sets the date to 1/1/2000 and another that takes three ints to set the date.

Include a print() method that will print the date in this format: MM/DD/YYYY and a method called printLong() that prints in this format: MonthName Day, Year. For example today's date would print November 16, 2021.

Include a method to add some number of days, months, and years to a Date:

void addDays(int d)
void addMonths(int m)
void addYears(int y)

Problem 2

Problem 2 asks you to create some related classes for playing card games. The data needed to be stored for a card is a numeric value and a suit. A card is responsible for being able to display itself. For example, when we want to display a two of hearts, ten of diamonds, jack of clubs, or ace of spades the output would look like this:

2 of Hearts
10 of Diamonds
J of Clubs
A of Spades

Next, create a deck class. A deck is a collection of fifty-two cards. Each card is unique. There should be a card with a numeric value from two to the ace in each of the four suits. The responsibilities of the deck class are that it must be able to shuffle itself and deal out some number of cards from the deck. To shuffle the deck you will need to randomly move cards around in the deck. One can generate a random number in C++ using the rand() function.

When dealing out cards the user will ask the deck for some number of cards. If there are enough cards in the deck it should deal those cards out. Once a card is dealt from the deck it cannot be dealt again from the same deck. The user will pass in a vector of cards and the deck will fill it with the number of cards requested. If there aren't enough cards in the deck print an error message and then kill the program with an exit(0).

When creating a class one always has to think about the data needed for the class and the responsibilities of the class. The data for the class should be private and an interface to the class should be provided. Think about how each card and deck should be constructed. Write at least one constructor for each class. Write the card class first and test it in a driver program. Then work on the deck class. To test the deck, create a deck object, ask for 52 cards, and print each of those cards to the screen.

Next, alter the program so that it will allow a person to evaluate the probabilities of getting certain poker hands. Poker is a card game played with a deck of 52 cards. In this program you only need to handle the five card variety of poker. The program will repeatedly get groups of five cards from the deck and count how many times each hand occurs.

In most variations of poker the precedence of hands goes like this:

Royal Flush- 5 cards that are a straight (5 cards in numeric order) and a flush (5 cards that are the same suit) from the 10 to the Ace.
Straight Flush- 5 cards that are a straight (5 cards in numeric order) and a flush (5 cards that are the same suit).
Four of a Kind- any 4 cards with the same number.
Full House- three of a kind (3 cards with the same number) and a pair (two cards with the same number).
Flush- any 5 cards of the same suit.
Straight- any 5 cards in numeric order.
Three of a Kind- any 3 cards with the same number.
Two Pair- 2 sets of pairs.
Pair- any 2 cards with the same number.
High Card in your Hand- if you don't have any of the above, your high card is the best hand.

You will need to create additional classes with more responsibilities than the Deck and Card classes.

Your program needs an 'evaluator' that can look at a collection of cards and determine the best hand that can be made from those cards. In order to determine probabilities, deal a great number of hands and keep track of how many times each hand shows up. In other words, your program might create 100,000 collections of five cards to be evaluated. The evaluator will count how many times a royal flush comes up, how many times a straight flush comes up, an so on. Your program will show the probabilities as percentages of the likelihood of getting each hand.

Below is a driver to illustrate how to use the evaluator:

int main()
{
    //five card evaluator
    //create a poker evaluator for 5 card poker
    PokerEvaluator fiveCardPokerEvaluator;

    //set the number of hands to play- one hundred thousand this time
    fiveCardPokerEvaluator.setNumberOfHandsToPlay(100000);

    //play all the hands and track the statistics, then print the results to the screen
    fiveCardPokerEvaluator.playAndDisplay();

    return 0;
}

Part 10: Data Structures

This section describes how to build some common data structures: a hash table, a binary search tree, and a graph. It also describes how to use the STL unordered_map class.

Hands-On Practice

Now that you have reviewed the guided code walk-throughs, write a program that includes a class which is equivalent of the vector called SafeArray. SafeArray has a method called at that returns the element at the specified position. A SafeArray maintains a pointer to an array on the heap. Use the pointer to make the array grow and shrink with calls to push_back and pop_back. The SafeArray will have a method called size that returns the number of items in it. Include a default constructor that sets the initial size of the underlying array to hold 10 elements. Include a destructor to delete the array when the SafeArray falls out of scope.

Part 11: SQLite Databases

This section describes working with a SQLite database. SQLite is my favorite Database Management System (DBMS) because it is powerful and easy to add to any program. This section assumes that you are familiar with relational database design and SQL.

The first program shows how to use the API to write and run SQL queries in a C++ program. In the second, some of the repetitive code is abstracted into a separate class. In the third, transactions in SQLite are explained and it shows that they can be used to ensure the ACID properties of a database:

Hands-On Practice

Extend the auction program from the second playback to include a method that prints the names and email addresses of all of the users who won an auction. Then write a method that prints item followed by the names and email addresses of anyone who made a bid on the item.

Comments and Feedback

You can find all of these code playbacks in the free 'book', An Animated Introduction to Programming in C++. There are more free books here:

Comments and feedback are welcome via email: mark@playbackpress.com.

Permalink

Clojure Deref (Mar 21, 2025)

Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS). Thanks to Anton Fonarev for link aggregation.

Podcasts, videos, and media

  • [EP.05 deref-in] - clojure-br

Blogs, articles, and projects

Libraries and Tools

New releases and tools this week:

  • ClojureDart - Clojure dialect for Flutter and Dart

  • rebel-readline-nrepl 0.1.6 - A Clojure library that brings the flexibility of Rebel Readline to your terminal as an nREPL (Network REPL) client

  • gen 1.1.0 - A CLI tool to generate files and run commands from templates

  • transductio 1.1.0 - A bridge from I/O to transducers and back

  • speclj 3.8.0 - pronounced "speckle": a TDD/BDD framework for Clojure

  • vrac 0.1.0 - A frontend rendering library in Clojure, for Clojurists

  • datalevin 0.9.22 - A simple, fast and versatile Datalog database

  • pg2 0.1.34 - A fast PostgreSQL driver for Clojure

  • clay 2-beta35 - A tiny Clojure tool for dynamic workflow of data visualization and literate programming

  • calva 2.0.492 - Clojure & ClojureScript Interactive Programming for VS Code

  • noj 2-beta12 - A clojure framework for data science

  • fulcro 3.8.3 - A library for development of single-page full-stack web applications in clj/cljs

  • cherry 0.4.25 - Experimental ClojureScript to ES6 module compiler

  • flow-storm-debugger 4.2.2 - A debugger for Clojure and ClojureScript with some unique features

  • coax 2.0.2 - Clojure.spec coercion library for clj(s)

  • squint 0.8.142 - Light-weight ClojureScript dialect

  • omega-red 2.3.0 - Idiomatic Redis client for Clojure

Permalink

On Inspectability

by Laurence Chen

At the beginning of 2025, I took over a client project at Gaiwan. It was a legacy code maintenance project that used re-frame. While familiarizing myself with the codebase, I also began fixing the bugs the client had asked me to address. This process was anything but easy, which led me to reflect on what makes an effective approach to taking over legacy code.

I have yet to reach a conclusion on the general approach to inheriting legacy code. However, when it comes to taking over a front-end project built with re-frame, I do have a clear answer: install re-frame-10x, because it significantly improves the system’s inspectability.

To illustrate, let’s examine my debugging process for one particular frontend bug in the project:

I knew that pressing a certain button would trigger an event, which would then be handled by a specific handler. However, due to the high level of abstraction in the code, I couldn’t directly determine which event the button was sending just by reading the source code. The event was determined by a section of code injected via dependency injection. Since I was reading through a large amount of code without being able to answer a simple question, the total volume of code I had to track quickly exceeded my working memory limit, making it increasingly difficult to proceed. As a side note, the project’s UI was in Dutch, which consumed even more of my working memory.

After installing re-frame-10x, the frontend events became inspectable. I could now easily view, via the browser, which event was sent when I clicked the button. As a result, I was able to answer the same question while saving a significant amount of time that would have otherwise been spent reading code. I also no longer needed to insert a ton of prn statements in the code. My mental load dropped significantly, and my development efficiency improved dramatically.

Have you ever had a similar experience? This made me rediscover the importance of inspectability.

Here is a proposed definition of inspectability:

A system provides certain mechanisms that allow developers to observe its internal state, events, behaviors, and errors during runtime or development. These mechanisms should also enable developers to correlate observed errors with the relevant sections of the running code, with minimal modification to the source code or configuration, facilitating understanding and diagnosis.

Based on this definition, the benefits of inspectability include:

  • Reducing modification costs. When developers realize they need to inspect the system’s internal state to proceed, a lack of inspection mechanisms forces them to manually insert prn, tap>, or similar debugging statements and advance through trial and error.
  • Lowering mental load. Quickly viewing the system’s internal state, events, and behaviors reduces cognitive overhead. Developers no longer need to retain large chunks of code in memory to infer how the system operates.
  • Reducing the “maze effect.” In legacy codebases without inspection mechanisms, developers often rely on fragmentary knowledge to understand the system. However, this knowledge is typically unidirectional—developers work forward from the code to infer system behavior. The problem is that similar-looking code scattered across different files may trigger the same behavior, leading to a maze effect where developers face multiple branching paths, making it difficult to determine which code branch is correct.

inspectability

One paradoxical yet interesting fact: The importance of inspectability is often felt most strongly by those unfamiliar with a system. People deeply familiar with a system tend to naturally follow the happy path while developing, and even if they make occasional mistakes, they are rarely far from it. When debugging, they can quickly infer the approximate cause based on their holistic understanding of the system. In contrast, for someone taking over maintenance, inspectability becomes critically important.

With this in mind, when developing a system, consider proactively improving its inspectability—even if you currently feel no pressing need for it due to your familiarity with the system.

Below, we’ll explore several approaches to improving inspectability.

Supporting Tools

In our definition of inspectability, we mentioned that “these mechanisms require little to no modification of the original source code or configuration.” If we can design our systems to reduce the ceremony required for inspectability, we are effectively enhancing it.

When debugging, the most basic and commonly used techniques are prn and inline def. However, some code structures make inspection impossible without modifications or special tools. For example, in the following code, we cannot apply prn directly to inspect the results of (reduce + xs) and (count xs) without modifying the function.

(defn mean [xs]
  (/ (double (reduce + xs)) (count xs)))

Among the many debugging tools available, I find that hashp requires minimal code changes while being highly intuitive. In the code below, #p prints the evaluated value of the form after it.

(defn mean [xs]
  (/ (double #p (reduce + xs)) #p (count xs)))

Inspectability at Different Levels

The first step most developers take toward improving inspectability is enhancing system logs. This is an important step, as logs contribute to developer-level inspectability. However, two other levels of inspectability are also worth considering: library developer-level and user-level.

At the library developer level, Clojure provides metadata, which can be used for:

  • Tagging & Tracing
  • Function Documentation & Metadata
  • Aspect-Oriented Programming

Consider this example from EmbedKit, where metadata is used for tagging. The request’s metadata is preserved in the response using vary-meta, allowing developers to retain important context.

(defn- do-request [req]
  (vary-meta (http/request req)
             assoc
             ::request req))

Similarly, user-level inspectability is critical. End users rely on documentation rather than source code to understand a system. When a system is inspectable, users can observe internal states to verify their actions.

Common user-level inspectability mechanisms include:

  • /health API
  • /metrics API
Inspectability LevelPurposeTarget UsersTypical Mechanisms
Developer-LevelDebugging, error tracing, performance monitoringApplication developersLog (clojure.tools.logging)
User-LevelSystem monitoring, quick issue diagnosisEnd users, operations teamsRESTful API (/health, /metrics)
Library Developer-LevelBehavior control, localized observability, additional semanticsLibrary/framework developersMetadata (meta, with-meta)

Events and Behaviors

In Clojure, we use live development—most people call it interactive development, but I prefer “live” because a running program is alive—which inherently provides an inspectability experience comparable to working in a debugger in other languages. Variables in a running system are always accessible, meaning that state is generally inspectable.

However, events and behaviors are not always so easily observable, which is why they deserve special consideration.

For example, in re-frame, I only gained event inspectability after installing re-frame-10x.

What about behavior inspectability? A good example is the bin/dev script used in many Gaiwan projects. Its primary function is deployment, but you can also think of it as an admin process.

The standard design of bin/dev always includes a dry-run (--dry-run) option. Any subcommand, when modified with the dry-run option, will not actually execute but will instead display the intended actions on the screen. This makes the upcoming “deployment behavior” inspectable. When we are unsure about the effects of a subcommand, we can quickly run a dry-run to understand what will happen before executing it for real.

Improving Exception Messages

Compared to my experience developing in C++ twenty years ago, having exception messages in Clojure/Java is a huge improvement. At the very least, when an exception occurs, I can start tracing the root cause from the stack trace.

Recently, I encountered an exception message with a full stack trace that was difficult for beginners to read. The stack trace had a total of 126 lines, but most of them were just noise. Only five lines were useful in quickly identifying the root cause. These five lines consisted of:

  • The very first line: Incorrect string value: ..., which hints that the error is related to a string.
  • Four other lines containing APPLICATION_NAMESPACE, which usually indicate where the issue can be found.

Ultimately, I fixed this exception in the code located at line 45 of src/clj/APPLICATION_NAMESPACE/services/files.clj. The fix was simply changing path to (str path).

In more complex cases, the number of lines containing APPLICATION_NAMESPACE may be far greater than four. In such situations, inspectability becomes a challenge since, for a developer, any occurrence of APPLICATION_NAMESPACE could be the source of the error. If this happens, one approach is to introduce Clojure spec checks between different layers of APPLICATION_NAMESPACE. This allows exceptions to be caught earlier by spec validation. Eric Normand has analyzed this issue.

[clojure] java.sql.BatchUpdateException: Incorrect string value: '\xAC\xED\x00\x05sr...' for column 'path' at row 1
[clojure]       at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
[clojure]       at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
[clojure]       at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
[clojure]       at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
[clojure]       at com.mysql.jdbc.Util.getInstance(Util.java:387)
...
[clojure]       at APPLICATION_NAMESPACE.db.access.files$update_file_BANG_.invokeStatic(files.clj:15)
[clojure]       at APPLICATION_NAMESPACE.db.access.files$update_file_BANG_.doInvoke(files.clj:15)
[clojure]       at clojure.lang.RestFn.invoke(RestFn.java:428)
[clojure]       at APPLICATION_NAMESPACE.services.files$upload_file_handler.invokeStatic(files.clj:44)
[clojure]       at APPLICATION_NAMESPACE.services.files$upload_file_handler.invoke(files.clj:19)

Conclusion

We have explored the importance of inspectability in maintaining and developing software systems. Starting from its definition, we emphasized or extended the definition in various ways—expanding from the developer level to the user level—and proposed several approaches to enhance inspectability during software development.

Finally, don’t forget: if you want to know whether your system has good inspectability, ask the person who takes over its maintenance. I believe they can provide you with highly valuable feedback.

>

Permalink

Rearchitecting My Trello Management Tool with Claude Code using Vibe Coding/Architecting (Part 2)

In Part 1 of this blog series, I shared my experience using Claude Code to resurrect a critical Trello card management tool that I’ve relied on for 6 years but hadn’t maintained in 2 years. After Twitter API changes broke my data pipeline and deployment issues plagued the application, I used Claude Code to:

  1. Resolve persistent bugs in the card movement functionality that had frustrated me for 4 years.
  2. Create a new Twitter data pipeline to replace the broken Zapier integration.
  3. Fix deployment issues with Google Cloud Run that were causing mysterious startup errors.
  4. Add quality-of-life improvements to the UI and keyboard shortcuts.

In that post, I described my experiences using Claude Code over 2 days, highlighting both its capabilities (quickly debugging complex issues, suggesting architectural improvements) and potential pitfalls (coloring outside the lines, overly complex solutions). Rather than passive “vibe coding,” I found using Claude Code required active engagement and critical thinking—like pair programming with an extremely knowledgeable but sometimes overzealous partner.

I wrote a lot about fixing bugs that eluded me for 4 years.

In this post, I wanted to write about how Claude Code helped me think through the architectural problems in my code. Fixing this would completely eliminate these types of errors.

But despite all the books I’ve read, the hundreds of podcasts and videos I’ve watched, and the scores of hours practicing it on my code, I was at a loss to figure out exactly what to do to fix the architectural issues in my code. It was amazing to have an LLM tell me exactly how to do it in my specific codebase.

What made it so exciting was that this wasn’t about generating code. Instead, it was helping me bridge years of theory that I’ve studied and actually put it into practice.

Lessons:

  • Architectural Improvements > Bug Fixes: Fixing bugs is good, but addressing underlying architectural issues can eliminate entire categories of bugs and make future development more robust.
  • From Theory to Practice: AI can help you fix these architectural issues. What an amazing use of “vibe coding” techniques. This is the opposite of “turning your brain off.”
  • Testability as a North Star: Easy testability is a hallmark of good architecture. When components are easily testable, they tend to have cleaner interfaces and better separation of concerns.
  • Active Collaboration: Using AI effectively requires engagement and critical thinking. It’s not about blindly accepting generated code but collaboratively reasoning through solutions.

Let’s Fix The Architecture Problems!

I asked Claude Code to provide recommendations for completely eliminating these categories of errors, to put me on a path to a cleaner code base. We identified two areas for improvement that I’m eager to jump into, after book deadlines are over:

  1. Ensuring that my Clojure maps that store Trello cards are accessed correctly and consistently.
  2. Developing a better way to extract the business logic in the Move modal dialog box into pure functions, which can then be unit tested.

It was exciting to create the list and have it give me concrete patterns that I could follow.

Hardening Data Access with a Consistent Interface Layer

The bugs I encountered in Part 1 were mainly due to map keys resolving to nil because of inconsistent usage of namespaced keys. In some places, I used :idList, while in other places I used :trello-card/idList. In fact, there are probably other misspelled keys in my code too.

This inconsistent usage resulted in nil values appearing where they shouldn’t.

Claude Code recommended creating accessor functions like get-card-id and get-list-id that would handle all possible variations:


(defn get-card-id [card]
  ; creates a more forgiving way to get values from map
  (or (:id card)
      (:trello-card/id card)
      (:card-id card)
      (get-in card [:trello-card/raw :id])))

I liked this because I often forget my own key conventions in programs that I’ve worked on for a long time, I occasionally mistype keys, etc. Implementing this in functions is a great way to enforce consistency.

I’ll likely do the same thing for setting these key values so that I can put all those conventions in code rather than keeping them in my head.

(People make fun of class-getters and setters, but at a certain point, it sure is nice to make those explicit!)

Claude Code also suggested adding data validation to catch issues early:


(defn validate-move-operation [{:keys [card-id list-id target-list-id] :as move-op}]
  (cond ; this is like a case statement

    (nil? card-id) 
    {:valid false :error "Missing card ID"}

    (nil? target-list-id) 
    {:valid false :error "Missing target list ID"}

    :else
    {:valid true :data move-op}))

That’s also a great idea, and I can add those as post-condition validation check-in functions that modify card states or use cards. Huzzah.

Improving Testability for the Card Moving Interface

I then asked Claude Code the following:

Help me create a plan to make the movemodal dialog box and associated components more testable. It is currently painful to test all the types of errors that we’ve encountered over the past couple of days. Please enumerate those types of problems and come up with a way to rewrite the code to enhance testability. This would help to eliminate the need for manual testing of pulling up the MoveModal box and testing the escape, enter, and tab keys. Additionally, I want to check to ensure that the history links function correctly and that clicking on the links triggers the correct moves. The moves must direct to the appropriate location

What it came up with was terrific:

Refactor Component Architecture: Replace the current global atom-based state with the component local state to make the flow of data more explicit and testable. Pass explicit callbacks for all actions instead of relying on a shared state. Create pure functions for all logic, which can be tested independently of the UI components.

Separate UI Logic from Event Handling: Isolate pure render functions that only handle UI presentation from event handlers that process keyboard and click interactions. Extract state management logic into separate testable functions that can be verified without rendering components.

Create Explicit Test Scenarios: Develop unit tests for all pure functions with comprehensive input variations. Build component tests that use mock data and callbacks to verify correct behavior. Implement integration tests to ensure complete workflows function as expected.

Implement Testing Utilities: Create event simulators for keyboard and mouse interactions that allow testing without manual input. Build state inspectors to easily verify component state at different points in time. Develop mock callbacks that record their invocations for validation.

Create Manual Test Harness: Build a dedicated test component with controls for simulating different user interactions. Include state visualization to make the current component state visible during testing. Add event simulation buttons to trigger specific behaviors without manual keyboard input.

Improve History Links Testing: Isolate the history display logic into pure functions that can be tested independently. Create mock data generators to produce test history entries. Implement verification functions to ensure clicking history links triggers the correct moves.

What was so fantastic for me was that all these recommendations seemed spot-on. But what was even better was that I could ask it to “show me exactly how I would implement tests for keystroke handling and what the UI component would look like.”

Changes to MoveModal Component Architecture

  • Move validation logic from component into standalone function validate-move-operation
  • Create get-effective-target function to handle target selection logic
  • Extract history transformation logic to prepare-history-display function
  • Create separate handle-move-button-click function for move button logic
  • Implement handle-cancel-button-click for modal closing
  • Add handle-reload-lists-click for list reloading functionality
  • Create handle-dropdown-selection for dropdown changes
  • Add handle-history-click for history item interaction
  • Remove nested conditional logic from component render function
  • Eliminate duplicate validation code that appears in two places
  • Move logging statements to a specific debug section
  • Change component to call handlers instead of containing logic
  • Remove direct state manipulation from render functions
  • Consolidate identical validation blocks into single implementation
  • Unify the move operation execution code
  • Remove redundant effective target calculation
  • Simplify button onClick handlers to call extracted functions
  • Change dropdown callbacks to use handler functions
  • Move alert/error display logic out of component

Implementation Priority – Start by creating pure functions for all logic currently embedded in components. Next, implement event simulation utilities for testing. Then refactor components to use the pure functions and explicit callbacks. Add comprehensive unit tests for all functions and components. Finally, create a test harness for manual verification of complex interactions.

Conclusion

While fixing bugs that annoyed me for 4 years felt great, what’s truly great is how AI can recommend how to fix architectural problems.

I love architecture. In fact, the entire Idealcast podcast series was my multiyear quest to better understand what good software architecture is, which has everything to do with the learnings that went into the Wiring the Winning Organization book that I coauthored with Dr. Steve Spear. (Download the entire Part I here!)

But despite all the books I’ve read, the hundreds of podcasts and videos I’ve watched, and the scores of hours practicing it on my code, it was amazing to have an LLM tell me exactly how to do it in my specific codebase. By recommending patterns like consistent data accessors and how to extract the logic from my UI components, I was well on my way to fixing these problems.

This will make the testing so much easier. Instead of tediously clicking through the UI to test keyboard interactions (which I’ve been doing for years), I can now verify these functions in isolation.

A change that would have required hours of manual testing can now be validated in seconds through automated tests. This creates a tighter feedback loop, allowing me to catch issues before they reach production.

The post Rearchitecting My Trello Management Tool with Claude Code using Vibe Coding/Architecting (Part 2) appeared first on IT Revolution.

Permalink

Rama, the 100x developer platform, is now free for production use

I’m excited today to announce we’re ending our private beta and making Rama free for production use! You can download Rama here. Rama is a platform for developing any backend at any scale that unifies computation and storage, supports infinite data models, and greatly reduces the amount of infrastructure and code needed to build a backend. Rama is deployed as a cluster, and on that cluster you deploy any number of Rama applications programmed in Java or Clojure.

We demonstrated Rama’s power when we announced it 1.5 years ago by rebuilding the entirety of Mastodon to be Twitter-scale (mostly the same as the Twitter consumer product circa 2015). Mastodon/Twitter consists of a ton of different interactive and asynchronous features, all with completely different indexing and processing requirements. Our implementation is 100x less code than Twitter wrote to build their 2015 consumer product at scale and 40% less code than the backend portion of the official Mastodon implementation. The performance and scalability of our implementation is as good or better than Twitter’s published numbers, and no other infrastructure besides Rama was needed. I’m excited now to make this power available to everyone.

The free version of Rama can run clusters up to two nodes, enough to comfortably run small or medium-scale applications. A license can be purchased to run larger clusters and have access to enterprise features. One-click deploys are available for AWS and for Azure.

The effect of Rama on the development speed and operational overhead of our private beta users has been profound. The past couple weeks we’ve published case studies from Multiply and from AfterHour exploring this, and we’ll be publishing more case studies from more of our users soon. Here are quotes from those first two case studies:

“With databases, the conversation always started with “what are we able to do?“. I rarely find myself asking what Rama is able to support, and rather “how?“. The requirements of the application dictate how we utilise the platform, not the other way around. Rama as a tool allows us to think product first, while still delivering highly optimised and scalable features for specific use cases, something that would not have been possible without a much larger team.”

William Robinson, senior engineer at Multiply

“Rama provides exactly the type of persistence, routing, and data locality for processing that is required to handle chat as scale. We would have easily spent 3-6 months building an inferior backing to our chat system if it were not for Rama. We are running many rooms with thousands of concurrent users in each and do not see any reason why it won’t continue to scale as we add more users, more features, and more rooms.”

JD Conley, Head of Engineering at AfterHour

I’m also excited to announce that we’ll build your entire backend for you using Rama at 75% less cost than anyone else will charge for a comparably high-quality implementation. So if someone else quotes you $40k, we’ll only charge $10k. We can do this with huge profit margins because Rama reduces the cost of development so much (and we’re pretty good at using it!). As a bonus, your application will be fully scalable as well. We’re offering free consultations if you’d like to discuss using Rama or our services.

Releasing a free version of Rama was my goal ever since I started working on it. The mission of Red Planet Labs is to dramatically improve the economics of software development. Rama accomplishes the technical side of that – what previously required large companies can now be done by individuals or small teams. The next step is to teach the world how to leverage this new way of thinking of building software. Making Rama free to use is a major milestone towards that goal. As more and more developers adopt Rama, we’ll have more and more examples of how Rama applies to every domain.

We used the private beta as an opportunity to learn from early adopters and improve Rama with their feedback. Here’s just a few of the ways Rama has improved since starting the private beta 1.5 years ago:

  • Added instant migrations, enabling schema changes with arbitrary transformation functions to be deployed and fully migrate instantly no matter how large the datastore
  • Added incremental backups, enabling Rama applications to be frequently backed up to any external store (e.g. Amazon S3)
  • Added a REST API, enabling reads and writes to be done with HTTP
  • Added yielding selects, enabling massive queries to be performed with high efficiency
  • Added node labeling, enabling Rama applications with different hardware requirements to co-exist in the same cluster
  • Added streaming ack returns, enabling arbitrary information about processing to be communicated back to appenders

These are besides the innumerable telemetry, logging, performance, and minor feature improvements we’ve made.

If you’d like to use Rama please don’t hesitate to get in touch! We’d love to talk to you even if you’re only planning to use the free version.

Permalink

Messy domains have a core

Our last Apropos was with Sean Corfield. Check it out. Our next episode is with Bobbi on March 25. Please watch us live so you can ask questions.

Have you seen Grokking Simplicity, my book for beginners to functional programming? Please check it out or recommend it to a friend. You can also get it from Manning. Use coupon code TSSIMPLICITY for 50% off.


The presentation was chaotic. My slides weren’t working. My daughter was sitting on my lap. And, what’s worse, the audience did not believe what I had to say. My main point was that you didn’t have to live in a nest of if statements. You could find a model in any domain. But they weren’t buying it.

That presentation was my first step toward the idea of domain modeling as the heart of software design. That was years ago. And it seems obvious now (so obvious that it seems weird to need to say it), but here’s where I eventually arrived: A domain model is judged by how well it corresponds to the domain. The model and the domain must have similar (ideally: identical) structure. The closeness of correspondence between model and domain is where the power of abstraction comes from. How do we get computers to do useful work? Correspondence.

But correspondence doesn’t sound great when you’re looking at a messy domain. In the intimate audience of that presentation, someone mentioned their domain had lots of government regulation. He believed there was no way to find a clean model. The only choice was building up spaghetti code to match the spaghetti of laws that made up their domain. And here I was trying to sell an answer to complexity by finding a better model.

I empathized with him. I’ve faced similar domains. But I’ve also been surprised. I came away from that presentation with a new awareness of the difficulty of the challenge I had taken on. You see, many times in my career, when we were struggling to find a simple model, we’ve eventually found a one (though sometimes too late). Well, not just a simple model—a much better model than what we currently had. The new model was so good, in fact, that when we did find them in time to build them into our code, they drastically reduced the complexity of our codebase. At the same time, they added dramatically to the business value.

I’ll tell a story that I’ve told before. But it’s very relevant. I worked at a company that helped Americans register to vote. In the US, each state determines its own voting registration requirements. A co-founder of the company had studied all of the laws of all the states, and she presented us with a big mess of overlapping categories, showing how crazy the logic was. It was clear she’d tried to organize the chaos but came up short.

We all struggled with the rules. Some states let you register the day of. Some you didn’t need to register. Some you had to register two weeks before. Some required you to live in the state for two years, unless the state you moved from didn’t allow you to vote there. Some just wanted you to vote where you lived. Ugh. It really was a mess. There were no clear seams that we could pull apart—which, in retrospect, makes sense when each state is acting mostly independently.

Eventually, I gave up. Let’s just do a big if statement! Why not? The first level of nesting will be by state. If you’re in Nebraska, then follow this logic. If you’re in New Jersey, follow this logic. We divide it up, and the task is 50x easier. We just have 50 new if statements to write, one for each state.

It’s sometimes useful to look at that worst case scenario. The worst case is that you have 50 small messes instead of one giant mess. Ask yourself: How would you do it in a straightforward way? It doesn’t mean you have to commit to going that way, but it helps jog the mind. And jog my mind it did.

Once it was a set of conditionals, I saw something we had overlooked: The question we were trying to answer was really simple: Can person x register in state y? That can be written in a nice function signature:

function canRegister(x: Person, y: State): boolean;

And each of those 50 branches of the conditional was then a question of this signature:

function canRegisterInNebraska(x: Person): boolean;

One of those functions for each state. That’s just a simple predicate on Person. And when you look at the rules of Nebraska (or any state), they’re of a similar form:

function isMajorityAge(x: Person): boolean;
function isResident(x: Person): boolean;
function isCitizen(x: Person): boolean;

We can easily combine those smaller, easy to implement questions into one bigger question with some boolean operations:

function and(f1: (x:Person)=>boolean, 
             f2: (x:Person)=>boolean): (x:Person)=>boolean;
function or (f1: (x:Person)=>boolean, 
             f2: (x:Person)=>boolean): (x:Person)=>boolean;

It turns out that isMajorityAge() is very reusable. Most states have that rule. So we were getting plenty of reuse. And we were able to build up rules that had a very similar structure to how the laws worked in each state. Each state was custom, but it was built out of modular, reusable parts.

In essence, we looked beyond the mess to find structure underneath. The structure was at a level of higher generality than we were looking at. We wanted to find a hierarchy to fit the states into. Instead, we needed to accept that each state was complicated and build a platform of atomized decisions that could be composed to mirror the structure of the state’s laws. We build a platform on which to model the states’ laws. I sometimes call such a platform a core abstraction.

I think of that story every time someone claims their domain has no structure. There’s always a structure. Humans build things with structure, even if it’s very messy and complicated. It’s never random. You can find the structure.

However, I don’t think it’s merely a mindset shift. I don’t really believe mindset is enough. Over the years, I’ve realized that it takes a very trained eye to spot that structure. How did we see that there was Boolean logic at the base? Lots of practice? How did I come up with that function signature? Years of working in Haskell. A part of my brain is always asking “what is the type?” And the composing functions? Luckily, this was a Clojure company, so we were used to thinking about higher-order functions. Would the average Java programmer have thought of that? I would guess not because they’re not used to thinking that way.

We should judge a model by how well it corresponds to the domain. The better it corresponds, the simpler our code that uses that model. But many domains are messy. Their structure is entangled. Does that mean that our model should be messy as well? I say no. It needn’t be messy, but you have to look for structure at a different level of generality than you’re currently looking.

But this a bittersweet synthesis. On the one hand, it’s hopeful. There is a core model that structures the mess. I’ve always found one, eventually. On the other hand, it’s not easy. I think of a passage from The Early History of Smalltalk (emphasis mine):

It started to hit home in the Spring of '74 after I taught Smalltalk to 20 PARC nonprogrammer adults. They were able to get through the initial material faster than the children, but just as it looked like an overwhelming success was at hand, they started to crash on problems that didn't look to me to be much harder than the ones they had just been doing well on. One of them was a project thought up by one of the adults, which was to make a little database system that could act like a card file or rolodex. They couldn't even come close to programming it. I was very surprised because I "knew" that such a project was well below the mythical "two pages" for end-users we were working within. That night I wrote it out, and the next day I showed all of them how to do it. Still, none of them were able to do it by themselves. Later, I sat in the room pondering the board from my talk. Finally, I counted the number of nonobvious ideas in this little program. They came to 17. And some of them were like the concept of the arch in building design: very hard to discover, if you don't already know them.

The connection to literacy was painfully clear. It isn't enough to just learn to read and write. There is also a literature that renders ideas. Language is used to read and write about them, but at some point the organization of ideas starts to dominate mere language abilities. And it helps greatly to have some powerful ideas under one's belt to better acquire more powerful ideas.

The challenge I’ve taken on in my book is to give people a handful of powerful ideas that help people model. I think the biggest idea is to judge code complexity not by how hard it is to read, but on how well it expresses the model. Then there are others, like to use total functions. And total functions is what we’ll talk about next week.

Do you have examples of finding core abstractions in an otherwise intractable domain? Is there a domain you gave up on and resorted to spaghetti code?

Permalink

Resurrecting My Trello Management Tool and Data Pipeline with Claude Code using Vibe Coding (Part 1)

Introduction

What do you do when you have a critical book deadline and need to use a tool you wrote that hasn’t worked in two years? It doesn’t deploy anymore because of some obscure error at startup in Google Cloud Run. And you haven’t touched the code in two years and don’t remember how any of it works.

Oh, and by the way, the entire data pipeline that made it so useful stopped working two years ago when Twitter limited access to their API and Zapier deprecated their integration. 

It seems like you’d be completely out of luck, right?

That’s the situation I found myself in last week. But I’ve routinely been building impossible things thanks to GenAI. In fact, I’m working on a book with Steve Yegge on how everyone can achieve the impossible!  (The book title we’ve finally converged upon: The Vibe Coding Handbook: How To Engineer Production-Grade Software With GenAI, Chat, Agents, and Beyond.)

In this blog post, I’ll share how I used Claude Code over two days two weeks ago to resurrect a critical tool I needed, fix bugs that have bothered me for four years (!!), and, oh, by the way, create a new Twitter data pipeline that I’ve been dreaming of for two years. 

Using Claude Code was an exhilarating experience, and it cost $80 in tokens (and 6 to 7 hours of work).

Steve started using Claude Code the same day I did. The next day, he told me, “I literally couldn’t sleep. I was tossing and turning all night long because all I wanted to do was get up and use it to fix things. I’ve got all this pent-up anger at 30 years of bugs that have been piling up in this game I wrote, and now I’ve got this machine that can just fix them.”

In a tweet, he wrote, “I’ve been using Claude Code for a couple of days, and it has been absolutely ruthless in chewing through legacy bugs in my gnarly old code base. It’s like a wood chipper fueled by dollars. It can power through shockingly impressive tasks, using nothing but chat.”

That feeling resonated with me. As Dr. Erik Meijer says, using Claude Code is addictive. You put in money, it fixes your code, it builds features, and you’re powerfully motivated to give it more money to do more of it!

In this post, I’ll describe why solving this problem was so important, an experience report using Claude Code, and some lessons learned.

(PS: for Clojure fans out there: so many of the posts I’ve written were about using languages I don’t normally use: Python for data analysis, JavaScript for building a Google Docs Add-On. This time, it’s all about my normal weapon of choice: Clojure and ClojureScript.)

Lessons:

  • “Vibe coding” often has the connotation of turning your brain off or only being for prototyping, but here’s an example of using Claude Code (and any coding assistant) to do the opposite. I describe a session where I felt like I was in a murder mystery, trying to reproduce and fix a bug that has been bugging me for 4 years. I iterate through specific usage scenarios, add more logging, lean on Claude Code to diagnose what is going wrong—and finally fix the problem
  • I describe how I hadn’t touched this project in 2 years. Despite that, under time pressures, coding assistants gave me confidence that I could resurrect this program and get an entire new data pipeline going over a weekend.
  • On the one hand, Claude Code can do amazing things, but it also requires continuous vigilance and a clear mental model of the boundaries of what you want changed. When you see changes being made where you don’t want them, they make it very easy to hit the ESC key, and it instantly stops the changes.
  • Despite being vigilant, you can still accidentally wake up in a room full of AI-generated horrors. I give a couple of examples where this happened to me, and yet decided, “It’s fine for now.”

Project Overview and Purpose

The program I needed to resurrect was a Trello card management tool, which had been at the center of my daily workflow for nearly 6 years. I built it for myself to manage my to-dos and notes for writing books. 

For many things, Trello serves as my to-do list and research fact file. Most of these Trello cards are from liked tweets that came in from Zapier. Other Trello cards were generated from starred Gmails, starred articles in Feedly (my RSS reader), and some other sources, all via Zapier. I had Trello boards for my to-dos and major projects, such as conference planning, book projects, things to research, and so forth. (It was an evolution of the David Allen “Getting Things Done” method I’ve been using for decades.)

I had first written this app in Clojure and ClojureScript in 2016, and rewrote it in 2019 using the Fulcro and Fulro RAD framework. The primary goal was to make it super-easy to move cards between boards and lists. It has a ton of keyboard accelerators (like vim), easy ways to make new lists, a “most recently used” list of move targets, which I could repeat with just a click, and all sorts of affordances that I made to make life easier for myself. And most importantly, it made it easy to deal with super-wide boards, something that is quite cumbersome in the Trello interface.

I was shocked today to discover that since 2016, I have processed almost 12,000 Trello cards with this app. Nearly 6,000 cards have been archived, and over 5,000 cards have been moved, which means I had read the cards, maybe pondered them, maybe even written about them, or just archived them. To me, this speaks volumes about how much I used this tool.

(In the screenshot below, you can see the boards or lists on the left, and the Trello cards on the right, with the list of all cards on the bottom.)

I pulled some stats from git (actually, Claude Code did). It’s 13K lines of code. And since 2019, I’ve added/modified/deleted 30K lines of code across 300 commits, adding various things I needed over the years. This shows how much functionality I added to help make my life easier.

Here was the data pipeline pre-2023, which was populating cards into Trello.

All that came to a crashing end when Twitter turned off most of their API in 2023 unless you paid $200/month. Zapier deprecated their Twitter integration, which turned off my biggest data source to my Trello boards. That’s when the system I had been using for a decade started to fall apart, if not cease entirely.  

I mostly stopped using this tool except for the rare cases when I had to do mass Trello card operations. I hadn’t pushed any code updates in two years.  

But with The Vibe Coding Handbook book deadline looming, and countless valuable AI coding examples appearing on Twitter daily, I needed this system operational again. I needed to get the hundreds (or thousands?) of liked Twitter posts out of my Twitter account and come up with a way to get them on a continual basis. 

I also needed to figure out how to deploy my Trello app again. As I mentioned, it hadn’t been deployed in 2 years. New deployments resulted in an uncaught exception and a mysterious error. I also wanted to fix a bunch of bugs, but I didn’t remember much (or anything) about the code.

A year ago, I probably would have looked at this and given up even before starting. Just imagining all the problems I could have encountered, I would have immediately concluded that the “juice wasn’t worth the squeeze.”  

But then I remembered how many things I’ve built in the last year that I could never have imagined doing before GenAI. Heck, I had even written a Google Docs Add-On a month ago!  Encouraged by these successes, I started mentally priming myself for tackling this.

Below, I show the new data pipeline I created over two days. In a future post, I’ll describe how I created the new data pipeline (despite every GitHub repo and gist describing how to do this disappearing).

In this post, I’ll focus on how Claude helped me fix the Trello card management app.

My First Session With Claude Code: What It’s Like Using It

I had signed up for Claude Code shortly after it was released but was waitlisted. But when Steve texted me saying that he had started using Claude Code, I was like, what? How?

That’s when I discovered that Anthropic just enabled anyone to start using it. I installed it and started using it right away. Here are instructions on how.

Claude Code is very different than the coding assistants you see in IDEs. When you start it in a terminal window shell with “claude,” it starts in a terminal window. You see a program that’s basically an interactive curses-style application. You can use up/down arrows to get history, there are live elements at the bottom of the screen.  

But it’s a revelation. Instead of focusing on the UI, the Anthropic team seemed to have focused on what really matters: stripping away all unnecessary affordances to explore how an LLM can solve problems with and for you.

These are some notes I took at the end of my first day:

In the beginning, Claude Code doesn’t have permission to do anything. I found myself giving it more permission to do things on its own. I was happy to give it access to sed, grep, and head. Of course, it asks to make modifications to source code files.

I was more reluctant to let it actually run the program itself (clj -M:m-run xxx), but I decided to let it on Day 2. It had the benefit of enabling it to detect compile-time errors on its own, which was awesome.  

Things got really weird when I let it do git add and git commit by itself about one hour into my first session. I feel like this is where it went totally off the rails.

I had given it a small task, and as a side effect of running the program, it realized it couldn’t start a non-relevant web server in a different namespace due to a port collision. Then it started making changes to the web server. I was like, “Whoa, whoa, whoa. Stop, stop, stop. This is way beyond what I wanted you to do.”

I learned from Steve that at any time, you can always hit ESC, and it will immediately stop what it’s doing and give control back to you. It’s far more immediate than I originally thought. You hit ESC, and you instantly have control again. (It must “kill -9” whatever active Claude Code tasks are operating.)

This dynamic makes it very pleasant and confidence-inspiring. You give Claude Code tasks, probably leaf nodes in your task tree, that are reasonably bounded and defined. It grinds away, and it shows you code diffs. You look at them and just hit Enter when you think, “Looks good to me!”

And far from turning your brain off, which is what people associate with “vibe coding,” your brain stays constantly engaged. You need it to make sure the AI is “coloring inside the lines.”

Claude Code has the notion of “context remaining.” I’m guessing this is the context window of everything it has discovered about your codebase, your objectives, and tasks. Eventually, you’ll see a notification at the bottom like “10% context remaining. /compact your session.” (They recently renamed the message.  It now reads “Context left until /compact: 22%.” I think this means it will automatically compact your session for you when exhausted.)

(Steve also mentioned in a conversation that this means the entirety of this context is likely being sent to the Anthropic API for each interaction, which drives up your costs. So there may be real benefits to compacting more frequently: faster inference times and cheaper.)

One time, I made the terrible mistake of typing /clear instead of /compact, which was absolutely awful. Maybe I was too tired after working on trying to deobfuscate the Twitter DOM. But when I realized what I had done, I had an “OMG, rm -rf /” level of horror.  And indeed, I had to explain what I was trying to do again, what methods we’ve already tried and failed, etc.  It started going down rabbit holes from hours ago.  After a long day, it was just too difficult to reestablish all the context again.  I quit for the day.)

(Note to Claude Code team: because context is so important, can “/clear” require explicit confirmation, and maybe even be renamed “/newsession”.  And how do you “save game”?  I’d love to have a “/savecontext” command, that outputs the entire context to a .txt file.)

Ah, from my notes that first day: after I accidentally cleared the context, it started creating new endpoints on the web server that it shouldn’t even be touching, brought in a whole bunch of libraries that I didn’t want, and then wanted to commit it into git. 

I was like, “No, no, no.”  I called it quits for the day.

End of day cost: $12.  Happily spent!

Second Day with Claude Code

After a good night’s sleep, I started another coding session. Much to my delight, I got enough of a Twitter to Trello card pipeline going and managed to get 1,200 Trello cards created from my liked tweets into MySQL.  

It was time to switch to Trello card processing production mode. I needed to start reading each card and disposition them: i.e., move them to the most relevant list or create a new list. To make life a little easier, I managed to create a quick categorizer, using Google Gemini to look at the tweet text and categorize it based on the existing board lists.

After processing several hundred cards, I was very irked to run into an annoying bug that had been aggravating me for four years. Every once in a while, when I move a card, it goes to the wrong list. When I make a new list, the new list doesn’t show up in the dropdown list right away. When I click on a list in the move history, the card gets moved to the wrong place.

I hadn’t touched this code in 2 years, and I know that I’ve tried to fix this bug off and on several times, and gave up each time. (I knew that it had to do with inconsistently using Clojure key names in maps. I always run into this type of problem in larger programs, which typically tempts me to consider more strongly typed languages. More on this later.)

I had mentioned that I had a session that felt like being a detective in a murder mystery, with a helpful companion at my side every step of the way. Below, I walk through about 90 minutes of Claude Code logs, where I’m trying to fix this problem while using the program. Every time I see something work, I document it. When something goes wrap, I capture the logs in the browser console and ask Claude Code what happened and why and how, and to add more logging.

By doing this, I progress from a situation where I literally have no clue what is going on to being on the verge of a fix. Here are some of the prompts

  • “When I move a card, I’m seeing: mutation: move-card: card-id: nil mutation: move-card: list-id: nil; how is that happening?”
  •  “I’m doing it from the move modal box. here are my logs” [lots of logs]
  • “put more logging when the ‘m’ key is selected and the MoveModal becomes visible; at that point, if the values are nil, do an js/alert; and then explain how we got there”
  • “alert hit; logs…” [lots of logs]
  •  “movemodal: esc key not working”

Eventually, I got to this point where I could write a long prompt that enumerates all the cases that are working, and the cases that are broken, as well as all of the relevant log messages.

  • “- These are a bunch of usage scenarios in the MoveModal dialog box — I’ve labeled them “works” and “wrong”.
    • – WORKS: Click a list in the dropdown, Click Move
    • – WORKS: select list in dropdown. (manually clicking it)
    • – WRONG: type list name (e.g. “to watch”), tab, enter
    • – WRONG: type list name: hit ENTER (closes window)
    • – WRONG:
      • – weird: in display history of moves, clicking the list goes to correct list, but name is wrong!
    • UNKNOWN: list name, down arrow, enter works?
  • Here are the relevant logs

Less than 30 minutes later, I managed to get this modal dialog box running correctly. I think all the edge failure cases have now been hammered out. The dropdown box works, tab, enter, and escape keys finally work as expected, and the history of moves above the dropdown box works!

Claude Code made it easier to debug this. I felt that I was working at a higher level, documenting the usage paths, with the AI improving logging and making sense of what the logs were saying.

The smoking gun? As Claude explained: “There was namespace mismatch between code (:idList) and data (:trello-card/idList).” As I suspected, I had misnamed keys littered around my code, but Claude Code tracked down the incorrect usages and fixed them for me.

It was such an exhilarating feeling to use my Trello list manager, fixing problems one by one as I found them, and making it easier and easier to do what I needed to get done. Here are some of the other prompts I wrote that day:

  • When making a new list, make it so that it’s available in Move Card modal dialog box right away
  • Add new menu items: Trello > Reload Starred Lists, Trello > Reload List” (so that they’re accessible via menu, not just keyboard accelerators, which I can’t remember after two years of not using the tool)
  • Display at bottom of the screen the active Trello board-id, list-id, card-id, last move destination (giving myself more debug information without having to open up the browser console)
  • Make it so that Enter in dropdown selection text area automatically selects the matching list, and triggers the Move operation

By the end of that day, I had a bunch of cards for Steve that he could integrate into these sections!

One More Debugging Story

One problem I discovered stands out in my memory as remarkable. When selecting a Trello list in the dropdown box, sometimes the card would get sent to the wrong list. Claude Code convinced me that it was due to a delay between two stateful managed React components.

I don’t have much experience with front-end programming, so I barely know what this means. But I know I had similar issues with managed React elements. I remember having a problem a decade ago of keeping the cursor in the right place in text input boxes, which apparently continues to be a problem that people face in React—here’s a React issue someone filed in 2020!

Claude recommended changing the Semantic UI React controls, to which I responded, “Whoa, whoa. I’m not sure if I’m up for that. I just want to fix my one little problem.” I was delighted when I suggested using a global variable instead to work around this issue. It actually worked!

When You Wake Up To An AI-Generated Room Of Horrors

Several times, I was reminded why vigilance is so important while using Claude Code (and other “agentic” coding assistants). Earlier, when trying to troubleshoot this issue, Claude Code was convinced that the move target list was being corrupted somehow. It then started reloading this list from the server.

I was willing to entertain that concept (despite being deeply skeptical). But I started getting 3- to 5-second keyboard latency when typing in the search box. Claude Code then started adding debouncing and caching and tried to debug debouncing.

I pulled the plug on this, reverted back to my last git commit, and asked Claude Code to take a different approach to problem-solving.

Someone described this situation as “waking up to an AI-generated room of horrors.” I can definitely relate to this. As an engineer, sometimes you go where the LLM suggests. Other times, you use your experience and judgment to say, “Nope.”

One last story: I had mentioned that I could no longer deploy this code to Google Cloud Run. It was running on my laptop just fine, but it was failing with this error in Google Cloud Run: “Received RST_STREAM: Protocol error” when fetching secrets from Google Cloud Secrets Manager.

Claude Code wasn’t able to fix this, so I resorted to using OpenAI Deep Research to figure this one out. It turns out that this error was caused by my conversion of my Google Secrets Manager from the Google Java client library to using REST calls, and for some reason, I was getting this error because I was using HTTP/2 by default.

(Why does this HTTP call work in some of my programs, but not others? I have no idea.)

But on the way to fixing the issue, I asked Claude Code to comment all my calls to my Secrets Manager library, which it did…very thoroughly.

I was happily copying and pasting the new logging messages into Claude Code to isolate what was generating the error. But when I looked into the source file of my callers, I was shocked to find the code now had deeply nested try/catches, like 6 or 7 levels deep, when they used to be 2 or 3.  

It was “fine,” but I would really like to clean this up. Someday. But for now, I have other fish to fry. (Some might say I just added to my technical debt, but it’s just a bunch of overly nested error handling. Big deal.)

But it definitely had shades of those “waking up to AI-generated horror” feelings.

End of day cost: $30.  Happily spent!

Conclusions

Many people associate “vibe coding” with turning your brain off and doing what the LLM says. I’m hoping this experience report shows that what I did on those two days was the opposite of that. I was using AI as another engineering tool to achieve what I wanted.

That’s the point of the book Steve and I are writing: The Vibe Coding Handbook: How To Engineer Production-Grade Software With GenAI, Chat, Agents, and Beyond. There are times you want to prototype something and throw it away, there are times you want to solve a problem for yourself (and only you have the consequences when something goes wrong), and there are times when you are supporting mission-critical services that support many, many people.

There is a way to use vibe coding and engineering tactics for each scenario. So, happy vibe coding!

PS: For those of you interested in Claude Code, here’s a 1.25-hour video that Steve Yegge and I recorded last Friday. Steve used it to convert a 20-year-old, 2,500-line Ruby admin script to Kotlin/Gradle.

It was super impressive. This was something we tried 2 months ago using more traditional CHOP/vibe coding methods, but we gave up after 90 minutes. In a little over an hour, Claude Code converted over some of the most challenging tasks: the MySQL and Redis admin commands, logging into the sandbox environment. Wow! (Link: https://www.youtube.com/watch?v=HtqxI53h7zM.)

My write-up on that experience is coming soon!

The post Resurrecting My Trello Management Tool and Data Pipeline with Claude Code using Vibe Coding (Part 1) appeared first on IT Revolution.

Permalink

A Clojure Jekyll adventure: A Wet side quest (Wet 0.3.0 released)

I ventured far and wide on my Clojure Jekyll adventure right after the last check-in. Then, fatigue (and life) caught up with me, and I had to set up camp for a little while.

The other day, I went on a little side quest and officially released my fork’s changes to the Liquid templating library Wet 💧.

This is the third part of the series: “A Clojure Jekyll adventure”. If you’re curious about what happened on the previous part of my journey, check out: Getting my feet wet with Liquid.

The library is now at version 0.3.0, indicating that new features have been added, but I still don’t think it is ready to be tagged as version 1.0.0.

For a quick recap, here’s what’s new:

  • render tag with parameters, allowing to isolate reusable template parts.
  • comment tag, because not everything needs to end up in the “final product”.
  • empty type for assertions like: {% if coll == empty %}

I put some serious effort into cracking “Whitespace control” (stripping unnecessary adjacent whitespaces controlled by special Liquid tags), but it proved to be a tough nut. I suspect it will take fundamental changes to the original implementation to get it right without overcomplicating the code. However, it might just be me — I still don’t fully understand the Wet library and Instaparse. Any help would be much appreciated!

As I teased earlier, I also made significant progress on the Clojure Jekyll clone. But after being defeated by ‘Whitespace control’, I needed some time to recover before I had the energy to write a blog post.

Originally, I planned to share details about the clone’s progress in this post, but it became too lengthy and unfocused, so I decided to dedicate a separate blog post to it instead.

Stay tuned.

Plus, I get to create another awesome picture for it with Midjourney. 🖼️

Permalink

Clojure Deref (Mar 14, 2025)

Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS). Thanks to Anton Fonarev for link aggregation.

Libraries and Tools

New releases and tools this week:

  • core.async 1.8.718-beta2 - Facilities for async programming and communication in Clojure

  • honeysql 2.7.1295 - Turn Clojure data structures into SQL

  • clojure-plus 1.2.0 - A collection of utilities that improve Clojure experience

  • awesome-clojure-likes - Curated list of Clojure-like programming languages

  • contajners 1.0.7 - An idiomatic, data-driven, REPL friendly clojure client for OCI container engines

  • calva 2.0.487 - Clojure & ClojureScript Interactive Programming for VS Code

  • slim 0.3.0 - The slim way to build Clojure

  • squint 0.8.141 - Light-weight ClojureScript dialect

  • bling 0.5.2 - Rich text console printing for Clojure, ClojureScript, and Babashka

  • stripe-clojure 0.2.1 - Clojure SDK for the Stripe API

  • astro 2025-03-12 - Rich development workflow with Clojure support, using AstroNvim 4 and selected plugins

  • manifest-edn 0.1.1 - A small Clojure/Babashka library for hashing static assets

  • wet 0.3.0 - Liquid in Clojure(Script)

  • omega-red 2.2.0 - Idiomatic Redis client for Clojure

  • sketch 0.1.29 - Domain network modelling

  • c4k-keycloak 1.5.0 - k8s deployment for keycloak

  • next-jdbc 1.3.1002 - A modern low-level Clojure wrapper for JDBC-based access to databases

  • clojure-repl-intellij 2.4.0 - Free OpenSource IntelliJ plugin for Clojure REPL development

  • clojure-lsp 2025.03.07-17.42.36 - Clojure & ClojureScript Language Server (LSP) implementation

  • html 0.2.2 - Html generation library inspired by squint’s html tag

  • lazytest 1.6.1 - A standalone BDD test framework for Clojure

  • flow-storm-debugger 4.2.1 - A debugger for Clojure and ClojureScript with some unique features

  • edamame 1.4.28 - Configurable EDN/Clojure parser with location metadata

  • sci 0.9.45 - Configurable Clojure/Script interpreter suitable for scripting and Clojure DSLs

  • clay 2-beta32 - A tiny Clojure tool for dynamic workflow of data visualization and literate programming

  • noj 2-beta10.1 - A clojure framework for data science

  • rv 0.0.6 - A Clojure library exploring the application of pure reasoning algorithms

  • babashka-sql-pods 0.1.4 - Babashka pods for SQL databases

  • cherry 0.4.24 - Experimental ClojureScript to ES6 module compiler

Permalink

Repetition

Sometimes, Clojure seems to miss operations that feel like they should be in clojure.core. I'm clearly not the only one who has felt this way, as evidenced by James Reeves's medley library. These missing operations are typically simple to implement, but after rewriting them for the nth time I feel like putting them in my own library. Or maybe contributing to James's.

Today's question was how to identify duplicates in a seq. For instance, say we have a sequence of numbers with a duplicate:

(def a-seq [0 1 2 3 4 2 6])

Step 1

My first thought was that every element in the sequence needs to be compared to every other item. Indeed, if the first element is not equal to any of the sequence that follows it, then that element need not be considered again. How might that look?

I'll start with a different seq where the first element does appear again:

(def seq0 [0 1 2 3 4 2 0])

We want to avoid indexing and loops whenever possible in Clojure, as these are conceptually low-level, and can be a source of errors. Instead, we want to look in clojure.core for applicable functions whenever possible.

As with any Lisp, the 2 main functions to consider for processing seqs are map and reduce. The map operation will do something for every element in a seq, returning a new seq, while reduce will derive a single value from processing each element in a seq. In this case, we're looking for a repeated value:

(reduce (fn [result element] (or result (= (first a-seq) element))) nil (rest a-seq))
true

OK, this found the value, but which value was it?

(reduce (fn [result element] (or result (and (= (first a-seq) element) element))) (rest a-seq))
0

This could then be applied to each subsequence, of the seq, but... wow, is it clunky. The reducing function is doing a lot here, comparing if the result is true yet, and if not, then comparing the provided first element to the element being presented by reduce, returning that element if needed.

We could clean it up a little by putting reduced over the return value so that it breaks as soon as the first match is found, and using a singleton set for the equality test:

(reduce (fn [result element] (or result (and (#{(first seq0)} element) (reduced element)))) (rest seq0))

But clojure.core already has a function that does all of this, called some. The documentation even tells us that we can use a set as an "equality" test when searching a seq:

(some #{(first a-seq)} (rest a-seq))

Step 2

But now we want to do this for every sub-seq, dropping off the head each time. One way might be to use drop to remove the start of the seq, counting up:

(map #(drop % a-seq) (range (count a-seq)))
((0 1 2 3 4 2 6) (1 2 3 4 2 6) (2 3 4 2 6) (3 4 2 6) (4 2 6) (2 6) (6))

But counting the sequence, or even using map-indexed (which does the counting for you) is still getting very involved in the mechanics of processing a sequence. Is there a higher-level construct?

One mechanism is using the rest operation over and over. This might be done using reduce in some way, but fortunately the iterate function does exactly this. Except it returns an infinite series, so we just need to take the correct amount. Let's go past the end by 5 to see why we need to keep it constrained:

(take (+ 5 (count a-seq)) (iterate rest a-seq))
([0 1 2 3 4 2 6] (1 2 3 4 2 6) (2 3 4 2 6) (3 4 2 6) (4 2 6) (2 6) (6) () () () () ())

This looks good, but it's still counting the length of the seq, so we want to keep looking for a better approach.

The map function is more flexible than simply applying an operation to transform each element of a seq. It can also be used to apply an operation to a group of elements across multiple seqs. The best part about this is that it will terminate as soon as the first of the seqs finishes. So let's try using map between the original seq and the iteration. We can just return the 2 values as pairs to see what we are working with:

(map (fn [a b] [a b]) a-seq (iterate rest a-seq))

This is almost exactly what we want as the arguments for the some operation above. Each operation of the map could execute rest on its second argument, but if we start the iteration one step along, then can get the same effect without needing the extra rest every time:

(map (fn [a b] [a b]) a-seq (rest (iterate rest a-seq)))
([0 (1 2 3 4 2 6)] [1 (2 3 4 2 6)] [2 (3 4 2 6)] [3 (4 2 6)] [4 (2 6)] [2 (6)] [6 ()])

These pairs are exactly what we needed for the some operation, so let's look at that:

(map (fn [a b] (some #{a} b)) a-seq (rest (iterate rest a-seq)))
(nil nil 2 nil nil nil nil)

Step 3

This is almost done. We need to skip to the first non-nil value, which we already know we can do using some. However, some needs a function to apply whereas we don't need to do anything to the value, just use it as-is. So we can use identity here:

(some identity (map (fn [a b] (some #{a} b)) a-seq (iterate rest (rest a-seq))))
2

To look more Clojure-ey we would usually run steps like this through a threading macro, and the anonymous function can be abbreviated as well. Of course, we want this to be a function, so we can replace the use of the a-seq value with a function argument, which I'll just call s for "seq":

(defn any=
 [s]
 (->> s
      rest
      (iterate rest)
      (map #(some #{%1} %2) s)
      (some identity)))

Step 4

After all of the above, I was thinking I had something relatively nice, and maybe I should write about the process of getting to it. Hence this post. I actually included extra steps along the way, just to spell things out, though I didn't actually go through most of those steps.

While thinking of extra steps and alternatives that I could write about, I thought about comparing to Clojure's distinct function which can remove duplicates:

(distinct a-seq)

This is similar to what I needed, but instead of telling me when it found a duplicate, it skips the duplicates instead. A similarly written function would also work, but this function is a transducer, and these are probably more work than should be necessary for something relatively simple.

I'm not saying that a transducer would be difficult. They follow a straightforward template:

(defn duplicates
 ([]
  (fn [rf]
    (let [seen (volatile! #{})]
      (fn
        ([] (rf))
        ([result] (rf result))
        ([result input]
         (if (contains? @seen input)
           (rf result input)
           (do (vswap! seen conj input)
               result)))))))
 ([coll] (sequence (duplicates) coll)))

This is actually a little more flexible since it can find all of the duplicates, rather than just the first one:

(duplicates [1 2 3 4 2 4 5 1])
(2 4 1)

The transducer approach also has the benefit of only processing the seq s single time, and using memory to recall what it has seen. So instead of O(n^2) complexity, it becomes O(n.log(n)) complexity (since the hashmap insertions and lookups are logarithmic - though in Clojure they are effectively O(1)).

However, all I needed was a function that could report the first duplicate in a seq. Maybe there was some way to use distinct for that.

Let's try using map again, this time against the original seq and it's distinct version. We'll emit pairs to see what is happening:

(map (fn [a b] [a b]) a-seq (distinct a-seq))
([1 1] [2 2] [3 3] [4 4] [2 5])

It terminated at the end of the distinct version, but we can see that just at the end the two sequences differed. This will happen if there is a duplicate in the first sequence that didn't appear in the second. So we can output that one when we get to it:

(map (fn [a b] (when (not= a b) a)) a-seq (distinct a-seq))
(nil nil nil nil nil 2)

This will keep emitting values after the first duplicate is found, since the two sequences are now misaligned. Consequently we only want to return the first non-nil value from the seq:

(some identity (map (fn [a b] (when (not= a b) a)) a-seq (distinct a-seq)))
2

Giving a new version for finding the first duplicate:

(defn any=
 [s]
 (->> s
      distinct
      (map #(when (not= %1 %2) %1) s)
      (some identity)))

Wrap Up

This is a little shorter, has better complexity, and makes better use of clojure.core. I'd probably use this one instead of my first attempt.

This post also reminded me to check my assumptions. This is not the first time I've tried to explain why one approach was not as effective as another, only to show that it was better. I need to remember that.

Permalink

Migrating to LazyTest

I've been using the Expectations testing library since early 2019 -- over six years. I love the expressiveness of it, compared to clojure.test, and it exists because "Classic Expectations" was not compatible with clojure.test tooling. At work, our tests use a mixture of clojure.test and Expectations, but in my open source projects, I've mostly stuck with clojure.test for familiarity's sake for contributors.Being compatible with clojure.test comes at a price, though. Expectations uses macros to produce clojure.test-compatible code under the hood, so it is limited by the same reporting as clojure.test and the same potential problems with tooling that tries to override parts of clojure.test's behavior -- namely that multiple tools do not play well together, so I've had to avoid "improving" the expressiveness or reporting in ways that would break compatibility with that tooling.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.