Mars Rovers IX: The Grid Viz Solutions — Emacs Lisp
This is part of the Mars Rovers series.
After finishing my first implementation of The Grid Viz Problem, in Clojure, I went ahead with the same process as last time, and translated it to Emacs Lisp.
The Adaptation
My approach was exactly the same: copy-paste into new Elisp buffer, and plunge ahead changing it (defn → defun, inc → 1+, ...) — and when that’s done, debug it a few times (there’s always something you miss, isn’t there?) until it runs.
The Comments
Here’re a few things that might help navigate the code.
Vectors of vectors
Working with a vector of vectors is not as straightforward in Elisp as in Clojure, where it behaves as a regular nested map. Nested updating in Elisp would involve chaining aset, which is destructive and returns nil. It can be done, but I find it a bit awkward.
So I picked hash tables through xht. Easy choice.
I kept the abbreviation vov in the function names, but it’s a misnomer.
Instead of “vector of vectors”:
[[:a :b :c] [:d :e :f]]
it’s a “0-indexed–keyed hash table of 0-indexed–keyed hash tables”, which xht represents like this:
(h* 0 (h* 0 :a 1 :b 2 :c) 1 (h* 0 :d 1 :e 2 :f))
It’s then easy to make nested updates using keys sequences, as well as merge nested hash tables — both of which can be done either destructively or non-destructively. One example:
(-> (h* 0 (h* 0 :a 1 :b 2 :c) 1 (h* 0 :d 1 :e 2 :f)) (h-put* 1 2 :FOO)) H=> (h* 0 (h* 0 :a 1 :b 2 :c) 1 (h* 0 :d 1 :e 2 :FOO))
This h-put* is non-destructive: it returns a fresh hash table.
But Elisp isn’t Clojure, so there’s no HAMT-backed performance behind the scenes; and I’m updating hash tables while strictly let-bound, returning them at the end. So here I did it destructively, using h-put*!:
(defun rover-grid--vov-updates-from-map (map) "Convert rover input MAP to a list of updates." (-let* (((&hash :begp :id :endp :endh :Zs :Vs) map)) `(,(list begp id) ,(list endp endh) ,@(--map (list it :Z) Zs) ,@(--map (list it :V) Vs)))) (defun rover-grid--vov-update-with-rover (vov imap) "Update vector of vectors VOV with rover's input map IMAP." (let ((updates (rover-grid--vov-updates-from-map imap))) (dolist (upd updates) (-let [((x y) v) upd] (h-put*! vov x y v))) vov))
and I’m fine with that.
The alternatives were:
- Prepare
updatesas a hash table instead, and merge it withvovnon-destructively. Could be done — but more awkward, less straightforward. Or launch a
loop-recurthing betweenvovandupdates, instead of thatdolist. Something (untested) like this:
(defun rover-grid--vov-update-with-rover (vov imap) "Update vector of vectors VOV with rover's input map IMAP." (let ((updates (rover-grid--vov-updates-from-map imap))) (--fix (-let [(a . d) it] (if (null d) a (-let (((x y) v) (car d)) (cons (h-put* a x y v) (cdr d))))) (cons vov updates))))
Purist-functional-programmer-me likes it — but frankly, have a look at this and compare with that procedural-but-shorter-and-more-readable
dolist.
Other than that, I used h--hmap twice for non-destructive hash-table-to-hash-table mapping.
Rotation
Unlike Clojure, here it seemed simpler to counterclockwise-rotate the hash table and print its elements as string all in a single pass. Also procedural — but tiny and simple. See rover-grid--vov->grid-str.
Validating string dimensions — or not
My make-smap function in Clojure was initially more complicated, as I had it do the extra work of validating the replacement strings — and I ended up rewriting that in Elisp as well:
;; Ensure monospace alignment (defun rover-grid--height (s) (->> s (s-matched-positions-all "\n") length 1+)) (defun rover-grid--same-width? (l) (apply #'= (-map #'length l))) (defun rover-grid--same-height? (l) (apply #'= (-map #'rover-grid--height l))) (defun rover-grid--make-smap (smap) "Merge a given SMAP with default-smap and then validate it. Validation consists of restricting the combination of display strings to those that keep grid alignment. It does not try to verify actual pixels — so you can still mess up display alignment if you use, say, CJK, emoji, etc." (let ((sm (h-mix rover-grid--default-smap smap))) (h-let sm (if (and (rover-grid--same-height? (list .N .E .S .W .p .z .Z)) (rover-grid--same-width? (list .N .E .S .W .p .v .V)) (rover-grid--same-height? (list .m .v .V)) (rover-grid--same-width? (list .m .z .Z)) (= 1 (length .p))) ; rover-id ≤ 9, and won't be customizable sm (error "Mismatch in string dimensions: %S" sm)))))
But then I thought that instead of throwing exceptions and restricting user input, better to keep it simple. So now it’s okay to enter some odd combination of replacement strings that makes the grid look unaligned. And if that doesn’t look good — well, then try another one!
So all the above became just this:
(defun rover-grid--make-smap (smap) "Merge a given SMAP with default-smap." (h-mix rover-grid--default-smap smap))
Simpler. Better.
The Code
;;; rover-grid.el --- Mars Rovers' Grid Viz Problem -*- lexical-binding: t -*- ;; SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful> ;; SPDX-License-Identifier: GPL-3.0-or-later ;;--------------------------------------------------------------------------- ;; Author: flandrew ;; Updated: 2026-04-18 ;; Keywords: lisp ;; Homepage: <https://flandrew.srht.site/listful/software.html> ;;--------------------------------------------------------------------------- ;; Package-Version: 0.1.0 ;; Package-Requires: ((emacs "25.1") (xht "2.0") (dash "2.18") (s "1.12")) ;;--------------------------------------------------------------------------- ;;; Commentary: ;; ;; My solution to The Mars Rovers' Grid Viz Problem. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Code: ;;;; Libraries ;; Assuming rover.el is in the same directory: (load (replace-regexp-in-string "-grid" "" (or load-file-name (buffer-file-name)))) (require 'dash) ; ‘-iota’, therefore 2.18+ (require 'xht) ; by the same author (require 's) ;;;; Symbols from other packages ;; Silence "not known to be defined" compiler warnings (declare-function rover--parse-input "ext:rover" (s)) (declare-function rover--map-all "ext:rover" (m)) (declare-function rover--xy "ext:rover" (m)) ;;;; Functions ;;;;; Extract path points and sides (defun rover-grid--stretch (p) "Transform point P (x,y) → (2x,2y). Makes grid coordinates match indices of a list of lists (lol)." (-let [(x y) p] (list (* 2 x) (* 2 y)))) (defun rover-grid--midpoint (p1p2) "Given list P1P2 of two points, return their midpoint." (-let [((x1 y1) (x2 y2)) p1p2] (list (/ (+ x1 x2) 2) (/ (+ y1 y2) 2)))) (defun rover-grid--path-sides (ps) "Convert list of points PS into list of sides. A side, here, is the midpoint of two consecutive non-overlapping points. It represents a segment of the rover's track, stretched (x,y) → (2x,2y). Rotations don't create sides, but these will be removed by type sorting." (->> ps (-map #'rover-grid--stretch) (-partition-in-steps 2 1) (-map #'rover-grid--midpoint))) (defun rover-grid--side-type (p) "Sort a side P into horizontal or vertical." (pcase (--map (= 1 (% it 2)) p) ('(t nil) :Zs) ('(nil t) :Vs) (_ :NOT))) (defun rover-grid--group-by-ZV (ps) "Group by horizontals and verticals the list of sides PS." (-group-by #'rover-grid--side-type ps)) ;;;;; Build vector of vectors ;;;;;; Make new ;; NOTE: Here, in my Elisp solutions, vov is not exactly a vector of vectors. ;; It's a 0-indexed–keyed hash table of 0-indexed–keyed hash tables. (defun rover-grid--vov-new (xM yM) "Create vector of vectors given xM and yM (grid's top-right coordinate). Initialize with empty-grid values: mzvp ~ mid, horiz, vert, point. Implicitly transforms (x,y) → (2x,2y). Visually, it's a 90° clockwise rotation of the grid, making coords match." (-let* ((zm (-interpose :m (-repeat (1+ yM) :z))) (pv (-interpose :v (-repeat (1+ yM) :p))) (lol (-interpose zm (-repeat (1+ xM) pv)))) (h<-list (-map #'h<-list lol)))) ;;;;;; Update with rover (defun rover-grid--make-vov-input (imap) "Given rover's init map IMAP, produce input map to modify vov." (let* ((id (-> imap (h-get :id) h-as-string)) ; s ← rover id (amap (-> imap rover--map-all)) ; vom (lasth (-> amap last car (h-get :h) h-as-keyword)) ; k ← last heading (points (->> amap (-map #'rover--xy))) ; lol (lastxy (-> points last car)) ; v ← last coord (initxy (-> points car)) ; v ← init coord (sides (-> points rover-grid--path-sides)) ; lol (groupd (-> sides rover-grid--group-by-ZV)) ; m (Zs (->> groupd (alist-get :Zs))) ; lol ← horiz coords (Vs (->> groupd (alist-get :Vs)))) ; lol ← verti coords (h* :begp (rover-grid--stretch initxy) :Zs Zs :endh lasth :endp (rover-grid--stretch lastxy) :Vs Vs :id id))) (defun rover-grid--vov-updates-from-map (map) "Convert rover input MAP to a list of updates." (-let* (((&hash :begp :id :endp :endh :Zs :Vs) map)) `(,(list begp id) ,(list endp endh) ,@(--map (list it :Z) Zs) ,@(--map (list it :V) Vs)))) (defun rover-grid--vov-update-with-rover (vov imap) "Update vector of vectors VOV with rover's input map IMAP." (let ((updates (rover-grid--vov-updates-from-map imap))) (dolist (upd updates) (-let [((x y) v) upd] (h-put*! vov x y v))) vov)) ;;;;; Maybe customize display strings (defconst rover-grid--default-smap (h* :p "·" :z " " :N "N" :E "E" ;s is used as intergrid separator :v " " :m " " :S "S" :W "W" :s "=" :V "│" :Z "───") "The default string values to replace placeholder keywords with. Everything except rover numbers can be customized.") (defun rover-grid--make-smap (smap) "Merge a given SMAP with default-smap." (h-mix rover-grid--default-smap smap)) (defun rover-grid--vov-keywords->strings (vov &optional smap) "Replace vector of vectors VOV's keywords with strings. If no map of keywords-to-strings SMAP is passed, use the default." (-let [sm (rover-grid--make-smap (or smap (h*)))] (h--hmap* key (if (keywordp value) (h-get sm value) value) vov))) ;;;;; Make output string (defun rover-grid--vov->grid-str (vov) "Convert vector of vectors VOV to output string. Assumed to have had keywords replaced with strings. CCW-rotate it and convert to string in one pass." (let* ((xlen (h-length vov)) (ylen (h-length (h-get vov 0))) (vovr (h--hmap key (h-reverse value) vov))) (->> (with-output-to-string (dolist (y (nreverse (-iota ylen))) (dolist (x (-iota xlen)) (princ (h-get* vovr x y))) (terpri))) (string-lines) ; remove trailing \n (-map #'s-trim-right) (s-join "\n")))) (defun rover-grid--imap->grid-str (xM yM imap &optional smap) "Given xM, yM, and rover's init map IMAP, return its grid string. Optional keywords-to-strings map SMAP may be provided." (let ((sm (rover-grid--make-smap (or smap (h*)))) (vovin (rover-grid--make-vov-input imap))) (-> (rover-grid--vov-new xM yM) (rover-grid--vov-update-with-rover vovin) (rover-grid--vov-keywords->strings sm) (rover-grid--vov->grid-str)))) (defun rover-grid--strs-with-separators (xM gstrs &optional smap) "Make horizontally separated string from list of grid strings GSTRS. Uses xM. An optional SMAP updates the default." (let ((sm (rover-grid--make-smap (or smap (h*))))) (h-let sm (if (null gstrs) "" (let* ((width (+ (* (length .p) (1+ xM)) (* (length .m) xM))) (sline (substring (s-join "" (-repeat width .s)) 0 width))) (s-join "\n" `(,sline ,@(-interpose sline gstrs) ,sline))))))) ;;;;; Output grid (defun rover-grid-output (s &optional smap) "Given input string S, produce output string. Optional keywords-to-strings map SMAP may be provided. Throw an exception if any map is invalid or if there's any collision." (-let* (((xr yr . rovers) (rover--parse-input s)) (((_ xM) (_ yM)) (list xr yr)) (sm (rover-grid--make-smap (or smap (h*))))) (--> rovers (-map (-cut rover-grid--imap->grid-str xM yM <> sm) it) (rover-grid--strs-with-separators xM it sm)))) ;;;; Wrapping up (provide 'rover-grid) ;; Local Variables: ;; coding: utf-8 ;; indent-tabs-mode: nil ;; sentence-end-double-space: nil ;; outline-regexp: ";;;;* " ;; End: ;;; rover-grid.el ends here
The Tests
Once again, I used Exemplify-ERT.
;;;; Examples ;;;;; Main ;;;;;; Our sample input (defvar good-input "5 5 1 2 N LMLMLMLMM 3 3 E MMRMMRMRRM") (exemplify-ert rover-grid-output:good-input ;; The default: exactly as displayed in the problem statement. (rover-grid-output good-input) => "\ ===================== · · · · · · · · · · · · · N · · · · │ ·───1 · · · · │ │ ·───· · · · · · · · · · · ===================== · · · · · · · · · · · · · · · 2───·───· │ · · · · · · │ · · · · ·───E · · · · · · =====================" ;; Can we have different grid points and separator? ;; Sure! (rover-grid-output good-input (h* :p "•" :s "★")) => "\ ★★★★★★★★★★★★★★★★★★★★★ • • • • • • • • • • • • • N • • • • │ •───1 • • • • │ │ •───• • • • • • • • • • • ★★★★★★★★★★★★★★★★★★★★★ • • • • • • • • • • • • • • • 2───•───• │ • • • • • • │ • • • • •───E • • • • • • ★★★★★★★★★★★★★★★★★★★★★" ;; Can separators be patterned? Will width be adjusted? ;; Sure! (rover-grid-output good-input (h* :p "⋆" :s "★-☆~")) => "\ ★-☆~★-☆~★-☆~★-☆~★-☆~★ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ N ⋆ ⋆ ⋆ ⋆ │ ⋆───1 ⋆ ⋆ ⋆ ⋆ │ │ ⋆───⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ★-☆~★-☆~★-☆~★-☆~★-☆~★ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ 2───⋆───⋆ │ ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ │ ⋆ ⋆ ⋆ ⋆ ⋆───E ⋆ ⋆ ⋆ ⋆ ⋆ ⋆ ★-☆~★-☆~★-☆~★-☆~★-☆~★" ;; Can we change the track lines as well? Maybe remove grid points? ;; Sure! (rover-grid-output good-input (h* :p " " :V "┆" :Z "┄┄┄")) => "\ ===================== N ┆ ┄┄┄1 ┆ ┆ ┄┄┄ ===================== 2┄┄┄ ┄┄┄ ┆ ┆ ┄┄┄E =====================" ;; What about the headings? Could we use something more visual, ;; more universal, more language-agnostic? ;; Sure! (rover-grid-output good-input (h* :N "⮙" :E "⮚" :S "⮛" :W "⮘")) => "\ ===================== · · · · · · · · · · · · · ⮙ · · · · │ ·───1 · · · · │ │ ·───· · · · · · · · · · · ===================== · · · · · · · · · · · · · · · 2───·───· │ · · · · · · │ · · · · ·───⮚ · · · · · · =====================") ;;;;;; Some tiny cases (defvar tiny-0a "0 0") (defvar tiny-0b "1 1") (defvar tiny-1a "1 1 0 0 N MRMRMRML") ; returns to origin (defvar tiny-1b "1 1 0 0 N ") ; never leaves origin — doesn't even rotate (defvar tiny-1c "2 1 0 0 N \n MRM 2 1 N \n LLMRMM") (exemplify-ert rover-grid-output:tiny (rover-grid-output tiny-0a) => "" (rover-grid-output tiny-0b) => "" (rover-grid-output tiny-1a) => "\ ===== ·───· │ │ S───· =====" (rover-grid-output tiny-1b) => "\ ===== · · N · =====" (rover-grid-output tiny-1c) => "\ ========= ·───E · │ 1 · · ========= · · 2 │ W───·───· =========") ;;;;;; How is Mars? (defvar rover-news "3 1 0 0 N \n M 1 0 N \n MR 2 0 N \n ML 3 0 N \n MRR") (exemplify-ert rover-grid-output:news ;;;; Hey, rovers — how is Mars? Send us some news! (rover-grid-output rover-news) => "\ ============= N · · · │ 1 · · · ============= · E · · │ · 2 · · ============= · · W · │ · · 3 · ============= · · · S │ · · · 4 =============") ;;;;;; Rover ponders big questions (defvar rover-life "7 6 1 5 S MMLMMLMMRRMMMMLMMMRRMMRMMRMMLMMLMMR") (exemplify-ert rover-grid-output:life ;;;; Rover, what is the answer to life, the universe, and everything? (rover-grid-output rover-life) => "\ ============================= · · · · · · · · · 1 · · N───·───· · │ │ │ · · · · · · · · │ │ │ · ·───·───· ·───·───· · │ │ · · · · · · · · │ │ · · · ·───·───·───· · · · · · · · · · =============================") ;;;; And with this 7×6 grid we conclude our tests.
Here's what some of the internal functions do.
;;;;; Internal (exemplify-ert rover-grid--make-vov-input (rover-grid--make-vov-input (h* :id 1 :x 0 :y 0 :h "N" :cs "MMRM")) H=> (h* :begp '(0 0) :id "1" :endp '(2 4) :endh :E :Zs '((1 4)) :Vs '((0 1) (0 3)))) (exemplify-ert rover-grid--vov-new (rover-grid--vov-new 0 0) H=> (h* 0 (h* 0 :p)) (rover-grid--vov-new 1 1) H=> (h* 0 (h* 0 :p 1 :v 2 :p) 1 (h* 0 :z 1 :m 2 :z) 2 (h* 0 :p 1 :v 2 :p)) (rover-grid--vov-new 2 3) H=> (h* 0 (h* 0 :p 1 :v 2 :p 3 :v 4 :p 5 :v 6 :p) 1 (h* 0 :z 1 :m 2 :z 3 :m 4 :z 5 :m 6 :z) 2 (h* 0 :p 1 :v 2 :p 3 :v 4 :p 5 :v 6 :p) 3 (h* 0 :z 1 :m 2 :z 3 :m 4 :z 5 :m 6 :z) 4 (h* 0 :p 1 :v 2 :p 3 :v 4 :p 5 :v 6 :p))) (exemplify-ert rover-grid--vov-keywords->strings (rover-grid--vov-keywords->strings (h* 0 (h* 0 :p 1 :v 2 :p) 1 (h* 0 :z 1 :m 2 :z) 2 (h* 0 :p 1 :v 2 :p))) H=> (h* 0 (h* 0 "·" 1 " " 2 "·") 1 (h* 0 " " 1 " " 2 " ") 2 (h* 0 "·" 1 " " 2 "·")) (rover-grid--vov-keywords->strings (h* 0 (h* 0 "1" 1 :V 2 :p) 1 (h* 0 :z 1 :m 2 :Z) 2 (h* 0 :p 1 :v 2 :W))) H=> (h* 0 (h* 0 "1" 1 "│" 2 "·") 1 (h* 0 " " 1 " " 2 "───") 2 (h* 0 "·" 1 " " 2 "W")) (rover-grid--vov-keywords->strings (h* 0 (h* 0 :p 1 :v 2 :p) 1 (h* 0 :z 1 :m 2 :z) 2 (h* 0 :p 1 :v 2 :p)) (h* :p "•" :N "🠉")) H=> (h* 0 (h* 0 "•" 1 " " 2 "•") 1 (h* 0 " " 1 " " 2 " ") 2 (h* 0 "•" 1 " " 2 "•"))) (exemplify-ert rover-grid--vov->grid-str (rover-grid--vov->grid-str (h* 0 (h* 0 "1" 1 "│" 2 "·") 1 (h* 0 " " 1 " " 2 "───") 2 (h* 0 "·" 1 " " 2 "W"))) => "\ ·───W │ 1 ·")
See next
Really, now: Bash?
How am I going to update hash tables and all that — there aren’t any!
On the other hand, the output is a string. And boy, can Bash do strings.
Mars Rovers X: The Grid Viz Solutions — Bash
📆 2026-W16-7📆 2026-04-19