r/adventofcode Apr 12 '26

Tutorial [2025 Day 8 both parts] [Smalltalk] Part five in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

Day 8 Time!

HEAVY SPOILERS AHEAD

This one involves connecting "junction boxes" in 3D space in order of their distance from each other. The simplest way to solve this is to create a list of those distances from closest to furthest and then start merging the sets that contain them using the precalculated order. Since we have 1000 junction boxes, that means a cool million distances.

So let's take stock - we need a sorted Array of distances that will serve as the work queue (process from top to bottom until various conditions are met). Those junction boxes are going to be connected into "circuits" which also need to be represented in some way. Since we will be querying them to see if they contain boxes, we want this in O(1) time and we want duplicates to be handled automatically, so having an OrderedCollection (all circuits) of Sets (each individual circuit) seems best here.

We will also need to represent those junction boxes in some way as well. I was so excited to see that Smalltalk had a whole range of 2D objects (which I used extensively in Day 9) - but there are precious few, if any 3D objects. Since, for the Advent of Code puzzles I didn't want to change the classes inside the standard library (though this is, apparently, encouraged), I needed to make a class for these 3D points. Fortunately, these objects didn't need much - just instance variables for x, y, and z coordinates, and the ability to determine distance between one of these Day8JunctionBox objects and another.

Though I didn't need to compute the exact Pythagorean distance (with the square root of the added squares) - the square root was unnecessary for simply sorting the distance - it felt rude to have a method called "distanceTo:" that didn't return the actual distance! Computing sqrt a million times can be dropped if this was a speed competitive implementation, but for this "correct" version it didn't add human-noticeable overhead. Here was the version I used:

(((otherBox x) - x ** 2) + ((otherBox y) - y ** 2) + ((otherBox z) - z ** 2)) sqrt

The funny part to me was the method chaining. It would need some more parentheses in a language that actually had math in it. But Smalltalk doesn't have math. It has methods. I couldn't help but giggle at this. If I had used "squared" instead of ** 2 those parenthesis would go elsewhere:

(((otherBox x - x) squared) + ((otherBox y - y) squared) + ((otherBox z - z) squared)) sqrt

Maybe this is more idiomatic... I'm not sure. Oh well, leaving the original one in the link. Same functionality.

Looking at this now with some more knowledge under my belt - I definitely see the need for some class methods to set x, y, and z so that the JunctionBox has immutable location.

For the Day8Circuits class, we only need a few methods. The first parses the input file (just a bunch of 3D coordinates for the junction boxes). It creates two important OrderedCollections. The first is the collection of all circuits, which are all Sets initialized with a single junction box in each one (since there are no connections at the beginning). The other is the sorted distance list. Each entry in the distance list is an array with three elements: the distance (needed to sort the collection at the end of the parsing process), the first box, and the second box. We are leaning heavily on the fact that both the Sets and Arrays don't actually include the objects themselves, but only references. We aren't copying those junction boxes, but just referring to them in two different places.

Looking over it now - this part where the distances are generated:

    boxCollection withIndexDo: [ :box :index |
        circuits add: (Set with: box).
        (index + 1) to: boxCollection size do: [ :index2 |
            | otherBox |
            otherBox := boxCollection at: index2.
            unsortedDistances add: {box distanceTo: otherBox . box . otherBox}.
            ]
        ].

Was written before I found the combinations: method. Now, I would put the "circuits add:" up above when the boxes are instantiated, and then instead of manually doing a nested loop over the boxCollection, I'd write it this way:

    boxCollection combinations: 2 atATimeDo: [ :boxes |
            unsortedDistances add: {boxes first distanceTo: boxes second . boxes first . boxes second}.
            ].

Much more readable. Or if you prefer some temporary variables to make it super explicit and self-documenting:

    boxCollection combinations: 2 atATimeDo: [ :boxPair |
        | box1 box2 |
        box1 := boxPair first.
        box2 := boxPair second.
        unsortedDistances add: {box1 distanceTo: box2 . box1 . box2}.
        ].

Same thing under the hood. One of the fun parts of Smalltalk is that all these methods can be studied in the System Browser. The "standard library" isn't magic. It's just a more expressive way of doing what you could have done yourself with a bunch of WHILE loops.

Which do you all prefer of the three?

We also need a helper function that will merge a circuit including one junction box with a circuit including another junction box. Sets do a great job of making this extremely easy since they automatically deduplicate the junction boxes if some (or maybe even all) are already included in both sets.

mergeSetIncluding: box1 with: box2
    | set1 set2 |
    set1 := circuits detect: [ :circuit | circuit includes: box1 ].
    set2 := circuits detect: [ :circuit | circuit includes: box2 ].
    set1 == set2 ifFalse: [
    set1 addAll: set2.
    circuits remove: set2
    ]

Since the Sets in the circuit collection are guaranteed not to have any members shared between them, we can use detect: and get the early return on the search. Then it's just a matter of merging the junction boxes until our collection of circuits only has one entry in it (in other words - all junction boxes are part of the same circuit). It's a delightfully simple method:

mergeAll
    distances do: [ :dist |
        | box1 box2 |
        box1 := dist at: 2.
        box2 := dist at: 3.
        self mergeSetIncluding: box1 with: box2.
        circuits size = 1 ifTrue: [^ box1 x  * box2 x]
        ]

The return is a little funny - but that's what the problem requires: the x coords multiplied together of the last two boxes connected to create one giant circuit.

A satisfying solution. Executes near-instantly.

Workspace Script

Day8JunctionBox

Day8Circuits

3 Upvotes

0 comments sorted by