Advent of Code

Table of Contents

Each year I participate in Advent of Code. I'm not much of a programmer. Scripts and reading code, I do regularly. Actually writing code from scratch, not so much. As such, Advent of Code is a nice change of pace.

In the past, Advent of Code was made up of 24 days of two-part puzzles. This year it's only 12 days long, but it doesn't feel like the scaling of the difficulty has been adjusted to compensate for that.

As an example - Sunday's puzzle (day 7) was a fair jump in difficulty (for me). I put down part 2, and only returned to solve it later in the day with a fresh approach. Prior to day 7, I did every puzzle in the span of one cup of hot coffee.

Day 8, today's puzzle, is a step-up in difficulty, and figuring out how to translate the solution in my head into emacs lisp is just too much work for me to do while I have a full-time job. So this is where I stop.

I suspect that for the career programmers this year is a bit of a let-down. We're past the half-way-point, and on the far-side of the two non-work days, and it's only (from my perspective) beginning to ramp-up. Presumably the difficulty of each day is now going to be exponential? I think it would have been better if day 1 this year was around the difficulty of day 4.

Still, I'm sure there'll be some people happy to finish it over a week before Christmas. And it was a welcome excuse to look at Emacs Lisp.

1. Lisp

In 2024 I used Ruby, in 2023 I used OCaml. Ruby before that, and going further back there's a bit of Python or Perl mixed in. I'm not precocious about programming languages, and would happily try any language I felt may be useful to me.

Although - I don't know if I'd subject myself to doing advent of code in shell script.

This year I wanted to use a lisp. But didn't know which to use. As such, I started working through the first three days of 2024 in a grab-bag of languages. This isn't a "I tried N lisps and this is what I found" post, so I'll just say Guile and Elisp floated to the top.

I like both languages, and intend to spend time on both in the future. I may also write about common lisp.

1.1. GNU Guile Scheme

Guile is nice. A previous post looked at Guix, so I had some familiarity in the back pocket. Going through the first few puzzles wasn't terribly difficult once I'd figured out turning input into lists.

Getting to that point was not immediate - I understand but dislike the ports system when applied to filesystem access.

I ran up against minor issues caused by my usage of Fedora Kinoite. For example, the most recent version of Guile is packaged in Fedora as 'guile30', and provides a guile3.0 binary (but no guile symlink). Geiser was not amused.

Guile does have TCO, because it's a proper Scheme. I feel like if you could ask me for the top three things that define a lisp - TCO would probably be in there. With that said, coming from Ruby, writing recursive functions isn't something that comes naturally.

To me, TCO is more of a thing I stumble into. Like "Oh, I could avoid another function by tweaking this one to have an accumulator".

I found writing Guile in Emacs with Geiser to be fun. The in-editor-eval / C-x C-e process is to Ruby's binding.pry as vaping is to chewing Kendal Black Irish XXX Extra Thick Twist Rope Tobacco. It feels like a fully realised process, rather than an enhancement to printing variables at certain points in code.

Below is Guile for day 1, 2024. It is a naive implementation, that I would write quite differently now.

(use-modules (ice-9 rdelim)
             (ice-9 string-fun)
             (srfi srfi-1))

(define (file->lines path)
  (call-with-input-file path
    (lambda (port)
      (let loop ((line (read-line port))
                 (lines '()))
        (if (eof-object? line)
            (reverse lines)
            (loop (read-line port) (cons line lines)))))))

(define (pair->delta pair)
  (let ((first (car pair))
    (second (cadr pair)))
    (abs (- first second))))

(define (string-pair->int-pair pair)
  (list (string->number (car pair))
    (string->number (cadr pair))))

(define (day1-part1 input-file)
  (letrec ((data (map string-tokenize (file->lines input-file)))
       (pairs (map string-pair->int-pair data))
       (left (sort (map car pairs) <))
       (right (sort (map cadr pairs) <))
       (sorted-pairs (map list left right)))
    (apply + (map pair->delta sorted-pairs))))

(define (multiply-by-right n right-list)
  (* n (count (lambda (x) (= x n)) right-list)))

(define (day1-part2 input-file)
  (letrec ((data (map string-tokenize (file->lines input-file)))
       (pairs (map string-pair->int-pair data))
       (left (map car pairs))
       (right (map cadr pairs)))
    (apply + (map (lambda (n) (multiply-by-right n right)) left))))

(day1-part1 "input")
(day1-part2 "input")

1.2. GNU Emacs Lisp

Elisp is so quick to get started with. Most of this is down to how elisp is deeply embedded in emacs - any familiarity with emacs can be translated into the same with elisp.

The starting point for all advent of code challenges is to get the input into a form you can work with. You can do this in one-go, or you can do it line by line. Either way, you need to read a file. If you already know M-x insert-file, you already know you're looking for something (insert-file "./file.txt").

By executing C-h f insert-file, emacs will pop a buffer with documentation for insert-file, which will explain you want (insert-file-contents FILENAME &optional VISIT BEG END REPLACE). For functions that aren't called interactively, it's usually enough to guess. C-h f split lists a plethora of functions, including (string-split STRING &optional SEPARATORS OMIT_NULLS TRIM).

I believe that I reached day 8 in elisp solely because of Emacs' internal documentation. My past experiences with Common Lisp tend to devolve into a lot of context-switching trying to figure out precisely what form of loop to use. There's a lot of ways to iterate.

Being able to bring up documentation on any function is about the only thing that makes that constant context-switching bearable.

There is no TCO. Humorously, I forgot that, spent an age writing a solution to day 7 part 1, and then realised when I switched from test input to real input and blew the stack. Guile Emacs may change that in the long-run.

The one thing I think I'd have liked to see is cl-X functions as the default. I appreciate it's a far too late to replace vanilla mapcar with cl-mapcar - you'd probably break an unfathomable number of emacs packages. But one can't help but wish the more versatile cl- functions weren't segmented behind a prefix.

Here's the same naive solution as above, in elisp. As I figured things out, things got a bit neater, and I spent less time hoying thing into random variables:

(add-to-list 'load-path (pwd))
(require 'utils)

(setq strings (file-to-string-lists "day1-test"))
(setq data (numberize-string-lists strings))
(setq left-data (mapcar 'car data))
(setq right-data (mapcar 'cadr data))

;; Part 1
(setq sorted-left (sort left-data))
(setq sorted-right (sort right-data))
(setq sorted-joined (cl-mapcar #'list sorted-left sorted-right))
(setq distances (mapcar (lambda (pair) 
                          (abs (- (car pair) (cadr pair)))) sorted-joined))
(setq day1-part1 (apply '+ distances))

;; Part2
(setq right-occurences (make-hash-table :test 'eq))
(dolist (num right-data right-occurences)
  (puthash num (1+ (gethash num right-occurences 0)) right-occurences))

(setq day1-part2 (apply '+ 
  (mapcar (lambda (num) 
            (* num (gethash num right-occurences 0))) left-data)))

(message "Day 1 Part 1: %d" day1-part1)
(message "Day 1 Part 2: %d" day1-part2)

2. Emacs Buffer Manipulation

One thing that elisp can do, which I've not seen elsewhere, is the ability to mangle buffers in real time. When I decided day 7 was about the right place to stop, I also decided I'd have a bit of fun with it.

Just like there's C-h f for functions you can call with M-x or in eshell, there's C-h k for keys. Finding out which function is called on C-n is trivial, and the documentation is top-notch. In the case of C-n, it calls (next-line &optional ARG TRY-VSCROLL), and the documentation helpfully states This function is for interactive use only; in Lisp code use ‘forward-line’ instead.

Iteration on the theme led to this fun mess (helper-functions were written, but are not below as they're trivial):

(defun day7-part1-visual (input-file)
  (let ((buf (generate-new-buffer "*day7*")))
    (with-current-buffer buf
      (insert-file-contents input-file)
      (switch-to-buffer buf)

      (beginning-of-buffer)
      (search-forward "S")
      (backward-char 1)

      (setq timelines 1)
      (setq splitters-all    '())
      (setq splitters-todo   '())

      (cl-labels
      ((one-stream (position)
         (goto-pos position)
         (while (not (eobp))
           (sleep-for 0.05)
           (redisplay)
           (let ((c (char-after)))
         (cond
          ((eq c ?\.)
           (replace-char ?|))
          ((eq c ?\|)
           (end-of-buffer))
          ((eq c ?^)
           (setq timelines (+ timelines (line-number-at-pos)))
           (push (get-pos)   splitters-all)
           (push (one-left)  splitters-todo)
           (push (one-right) splitters-todo)
           (end-of-buffer))))
           (move-one-down))))
        (one-stream (get-pos))

    (while (< 0 (length splitters-todo))
      (if splitters-todo (one-stream (pop splitters-todo))))

    (insert (format "Part 1: %d\n" (length splitters-all)))))))

Which, when executed with the test input, results in the following animation:

3. Caveats

A small note - Emacs Lisp is old. Venerable is one way to view it. But as a result, there's a lot of "X should do Y, but A, B, and C depend on current behaviour".

I ran up against this when in need of a cyclical list. I had expected nth -1 of a cyclical list would be last element of the list before it wraps around, or something relative to that point.

i.e. nth -1 #1=(1 2 3 4 . #1#) would return (ideally) 4, (less ideally) 3, or (perfectly acceptable in lieu) - an error. It instead returns 1 for all negative nths. Which is rather the worst of all worlds. This was discussed on the mailing list last year.

Merry Christmas!

Date: 2025-12-08 Mon 12:00

Author: Patrick

Emacs 30.2 (Org mode 9.7.11)

Validate