Thumbnail's answer is good, and I didn't really want to distract from it, but your little exercise also gives opportunity to play with Clojure's lisp-nature that I couldn't resist.
;Agree that unit vectors are more convenient to work with than cardinal directions
(def north [0 1])
(def east [1 0])
(def south [0 -1])
(def west [-1 0])
;Just for a taste of macros
(defmacro name-value-map [& xs]
`(zipmap ~(mapv name xs) (vector ~@xs)))
(def direction->heading (name-value-map north east south west))
(def heading->direction (clojure.set/map-invert direction->heading))
;Robot commands just return an updated structure
(defn left [robot]
(update-in robot [:heading] (fn [[dx dy]] [(- 0 dy) dx])))
(defn right [robot]
(update-in robot [:heading] (fn [[dx dy]] [dy (- 0 dx)])))
(defn move [robot]
(update-in robot [:position] (partial mapv + (:heading robot))))
;Report and return unchanged
(defn report [robot]
(println "Robot at" (:position robot)
"facing" (heading->direction (:heading robot)))
robot)
;Create
(defn place [x y heading]
{:position [x y] :heading heading})
Now with those in place you already pretty much have your language in a mini-DSL via the threading macro
(-> (place 3, 3, north) report move report left report move report)
;Printed Output:
;Robot at [3 3] facing north
;Robot at [3 4] facing north
;Robot at [3 4] facing west
;Robot at [2 4] facing west
;Return Value:
;{:position [2 4], :heading [-1 0]}
Indeed, if you had a file that you trust with contents
(def sample-file-contents "(place 3, 3, north) move left move")
You could read in the data as a form
user=> (read-string (str "(" sample-file-contents ")"))
((place 3 3 north) move left move)
Interleave some reporting (at the REPL *1
is the previous value)
user=> (interleave *1 (repeat 'report))
((place 3 3 north) report move report left report move report)
Tack on the threading macro
user=> (cons '-> *1)
(-> (place 3 3 north) report move report left report move report)
And eval
uate for the same output as above
user=> (eval *1)
;Printed Output
;Robot at [3 3] facing north
;Robot at [3 4] facing north
;Robot at [3 4] facing west
;Robot at [2 4] facing west
;Return Value:
;{:position [2 4], :heading [-1 0]}
To set this up a bit more properly doesn't require too much extra effort with a good parsing library
(require '[instaparse.core :as insta])
(require '[clojure.tools.reader.edn :as edn])
(def parse (insta/parser "
<commands> = place (ws command)*
<command> = place | unary-command
place = <'place '> location <', '> direction
unary-command = 'left' | 'right' | 'move' | 'report'
number = #'[0-9]+'
location = number <', '> number
direction = 'north' | 'east' | 'west' | 'south'
<ws> = <#'\\s+'> "))
(defn transform [parse-tree]
(insta/transform
{:number edn/read-string
:location vector
:direction direction->heading
:place (fn [[x y] heading] #(place x y heading))
:unary-command (comp resolve symbol)}
parse-tree))
(defn run-commands [[init-command & more-commands]]
(reduce (fn [robot command] (command robot)) (init-command) more-commands))
And now that we've dropped the requirement for parenthesis (and with some sample newlines included), redefine the sample file
(def sample-file-contents "place 3, 3, north report
move report
left report
move report")
And, again at the REPL to show intermediate results
user=> (parse sample-file-contents)
([:place [:location [:number "3"] [:number "3"]] [:direction "north"]] [:unary-command "report"]
[:unary-command "move"] [:unary-command "report"]
[:unary-command "left"] [:unary-command "report"]
[:unary-command "move"] [:unary-command "report"])
user=> (transform *1)
(#< clojure.lang.AFunction$1@289f6ae> #'user/report
#'user/move #'user/report
#'user/left #'user/report
#'user/move #'user/report)
user=> (run-commands *1)
;Printed Output:
;Robot at [3 3] facing north
;Robot at [3 4] facing north
;Robot at [3 4] facing west
;Robot at [2 4] facing west
;Return Value:
;{:position [2 4], :heading [-1 0]}