I am an experienced self-taught professional Python programmer working my way through the book Classic Computer Science Problems in Python to try to catch myself up on some of the things I missed by not going to school for this. I am also currently trying to learn Clojure, and I thought why not kill two birds with one stone by using this book to practice writing Clojure as well. So, I'm using Python as a reference in order to port solutions to Clojure. I have never professionally written code with a lisp or functional programming language. I am new to the Clojure and JVM ecosystems. For all these reasons I feel I would greatly benefit from submitting my early Clojure code up for peer review from more experienced Clojure developers.
What I'm primarily looking to get out of this review:
- Call me out where I'm doing something Pythonic but not Lispy/Functional.
- Point me in the direction of useful Clojure core functions that I may just not know about.
- Guide me to use common Clojure developer conventions. How should I write doc comments? How should I name my variables?
- Tell me when something I've done just looks weird to an experienced Clojure developer.
- I'd really be interested in a lazy solution to my dfs algorithm that could produce all the possible solutions rather than just the first solution.
Okay here's the code.
(ns cs-clj.ch2.generic-search)
(defn dfs
[start is-goal? get-successors]
(loop [frontier (list [start, []])
explored #{start}]
(let [[cur-loc cur-path] (peek frontier)
next-path (conj cur-path cur-loc)]
(cond
(empty? frontier) nil
(is-goal? cur-loc) next-path
:else (let [successors (remove explored (get-successors cur-loc))
new-frontier (map (fn [loc] [loc next-path]) successors)
next-frontier (apply conj (pop frontier) new-frontier)
next-explored (apply conj explored successors)]
(recur next-frontier next-explored))))))
(ns cs-clj.ch2.maze
(:require [clojure.string :as string]
[cs-clj.ch2.generic-search :refer [dfs]]))
;; TODO: Seems like a fine way to represent an Enum, but...is there a better way?
(def cells {:empty " "
:blocked "X"
:start "S"
:goal "G"
:path "*"})
(defn random-cell-value
[sparsity]
(if (< (rand) sparsity)
(:blocked cells)
(:empty cells)))
(defn create-2d-grid
[x y val-fn]
;; TODO: Is this idiomatic clojure? I'm trying to express a way to take either a
;; function or a value, and if it's a value wrap it in a function? Am I missing
;; the use of `identity` here in some way? Or perhaps an opportunity to use spec?
;; If spec, how?
(let [val-fn (if (fn? val-fn) val-fn (fn [] val-fn))]
;; TODO: Wanted to represent grid as vectors, hence the `mapv`. With my use
;; of `assoc-in` to set values in the grid later I'm not certain it would have
;; worked with normal `map` returning sequences.
(mapv (fn [_]
(mapv (fn [_] (val-fn)) (range x)))
(range y))))
(defn mark-grid-with
[grid coord-vals]
(reduce (fn [res-grid [coord val]]
(assoc-in res-grid coord val))
grid
coord-vals))
;; TODO: use spec to validate that the start/goal are inbounds of rows and cols
;; TODO: think about whether maze "objects" could be a record.
;; TODO: Is there a good Protocol opportunity here?
;; mark-path, is-in-bounds?, is-blocked?, display-maze, get-successors, and mark-path
(defn create-maze
"Creates randomly generated maze map."
[& {:keys [rows cols sparsity start provided-goal]
:or {rows 10
cols 10
sparsity 0.2
start {:row 0 :col 0}
provided-goal nil}}]
(let [goal (if (nil? provided-goal)
{:row (dec rows) :col (dec cols)}
provided-goal)
val-fn (partial random-cell-value sparsity)
rand-grid (create-2d-grid rows cols val-fn)
grid (mark-grid-with rand-grid [[[(:row start) (:col start)] (:start cells)]
[[(:row goal) (:col goal)] (:goal cells)]])]
{:rows rows
:start start
:goal goal
:cols cols
:grid grid}))
(defn is-in-bounds?
"Checks if a location is inside the bounds of the maze."
[maze loc]
(let [{:keys [rows cols]} maze
{:keys [row col]} loc]
(and (<= 0 row)
(< row rows)
(<= 0 col)
(< col cols))))
;; TODO Is it okay to use `backticks` in comments and doc strings to refer to
;; code vars and such? Is there a clojure docstrings and comments standard I should
;; know about? Like what if I wanted to autogen some API docs to a static site? Is
;; there a standard or at least popular tool like python's sphinx and restructured text?
(defn is-blocked?
"Is the location at `loc` occupied in the `maze` by a wall character?"
[maze loc]
(let [grid (:grid maze)
{:keys [row col]} loc
cell-value (get-in grid [row col])]
(= cell-value (:blocked cells))))
;; TODO: How do I get a doc comment on this function?
(def is-open? (complement is-blocked?))
(defn display-maze
"Formats the maze grid to a legible string value."
[{:keys [grid]}]
(->> grid
;; TODO: Use (map #(string/join "" %)) here or is that too terse?
;; I feel like maybe the var name "row" helps clarity slightly.
(map (fn [row] (string/join "" row)))
(string/join "\n")))
(defn get-successors
"Gets sequence of valid moves from the given location in the maze."
[maze loc]
(let [{:keys [rows cols]} maze
{:keys [row col]} loc
candidates [{:row (inc row) :col col}
{:row (dec row) :col col}
{:row row :col (inc col)}
{:row row :col (dec col)}]]
(->> candidates
(filter (partial is-in-bounds? maze))
(filter (partial is-open? maze)))))
(defn mark-path
"Mark the path through the maze with * characters. Returns altered maze."
[maze path]
;; TODO: Is this call to subvec an idiomatic clojure way to slice a vector?
;; I'm trying to do here what I would do in python as `path_locs = path[1:-1]`
(let [path-locs (subvec path 1 (dec (count path)))
cell-val (:path cells)
coord-vals (map (fn [{x :row y :col}]
[[x y] cell-val])
path-locs)]
(assoc maze
:grid (mark-grid-with (:grid maze) coord-vals))))
(defn -main []
(let [maze (create-maze)
is-goal? (partial = (:goal maze))
get-suc (partial get-successors maze)
path (dfs (:start maze) is-goal? get-suc)]
(if (nil? path)
(do
(println (display-maze maze))
(println "Could not solve."))
(println (display-maze (mark-path maze path))))))
Produces output like this when run:
S******X
X ****
X *
X*********
** X X
* XXX XX
****X X
*X X
*******
X G
Also, here is the python maze.py