2

So I have a problem I'm trying to solve in clojure. It's a bit of a programming exercise I try to do in many languages to "learn" the language..

This is my first immutable-functional language and I'm having some challenges.

The exercise reads from a file a number of "instructions" in the form of PLACE X, Y, CARDINAL_DIRECTION (e.g. PLACE 0,0,NORTH)

And then can move forward one space using MOVE

The player can rotate left and right using LEFT, RIGHT.

finally REPORT will print out the x, y, and cardinal direction of the player

The challenge I have at the moment is I'm not sure where to store the players position. In a class based language I'd have a player class that holds the co-ordinates.

Below is my solution so far

(ns toyrobot.core
  (use clojure.java.io))

(defstruct player :x :y :face)
(def ^:dyanmic robot nil)

(defn file->vec
  "Read in a file from the resources directory"
  [input]
  (with-open [rdr (reader input)]
    (into [] (line-seq rdr))))

(defn parse-command [line]
  "Parse command"
  (clojure.string/split line #"\s|,"))

(defn on-the-board? [co-ordinates]
  "Check if the x & y co-ordinates are on the board"
  (let [x (Integer/parseInt (first co-ordinates))
        y (Integer/parseInt (second co-ordinates))]
    (if (and (>= x 0) (>= y 0) (<= x 5) (<= y 5))
      true
      false)))

(defn place [co-ordinates]
  "Place the robot at the co-ordinates"
   (if (on-the-board? co-ordinates)
     co-ordinates
     nil))

(defn -main []
    (doseq [command (file->vec "resources/input.txt")]
      (case (clojure.string/upper-case (first (parse-command command)))
        "PLACE" (place (rest (parse-command command)))
        "MOVE" (println "move")
        "LEFT" (println "left")
        "RIGHT" (println "right")
        "REPORT" (println "report")
        "")))
mark
  • 833
  • 8
  • 21
  • I didn't quite understand the question. If I'm correct, you want to find a way to store the coordinates and the "state"s of the players like the way you do in a object-oriented programming language when you're creating an object? – albusshin Feb 28 '14 at 12:30
  • That's right, so I want to store the co-ordinates. Basically, it starts with the place command, and then moves a few times and the reports... How should i be storing the x,y,direction ? – mark Feb 28 '14 at 12:39
  • In functional language you shouldn't think in the way you're doing in object-oriented programming. Why not simply using a map – albusshin Feb 28 '14 at 12:41

2 Answers2

4

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 evaluate 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]}
Community
  • 1
  • 1
A. Webb
  • 26,227
  • 1
  • 63
  • 95
3

You are well in control of parsing the instruction file, so let's concentrate on the internal computation.

Clojure structs are essentially immutable, so that the history of the robot is a sequence of distinct robot state objects instead of a sequence of states of a single robot object. So it is natural to represent the robot commands as functions that, given a robot state, return another robot state.

We need

  • commands to turn-left, turn-right, and move-forward and
  • some way of creating a robot state in a given place and pointing in a given direction.

How should we represent robot state? Let's keep it simple.

  • Places are simply pairs of numbers, x before y. So that the origin is [0 0] and [5 0] is 5 along the x axis.
  • Directions are movement vectors, so that [1 0] stands for east and [0 -1] stands for south. This makes movement easy to do.
  • The robot state is a record (easier to handle than a struct) with :place and :direction fields.

Let's first deal with functions to change direction left or right.

We start by defining a helper function to generate a cycle from a sequence:

(defn cycle-map [fs]
  "maps each element in the finite sequence fs to the next, and the last to the first"
  (assoc (zipmap fs (next fs)) (last fs) (first fs)))

... which we use to generate a map that takes each direction to the one to its left:

(def left-of (cycle-map (list [0 1] [-1 0] [0 -1] [1 0])))

We can invert this to yield a map that takes each direction to the one to its right:

(def right-of (clojure.set/map-invert left-of))

We will use these maps as functions to modify robot states (strictly speaking, to return modified robot states).

Now we define our Robot state:

(defrecord Robot [place direction])

... and some of the functions we need to manipulate it:

(defn turn-left [robot] (assoc robot :direction (left-of (:direction robot))))

defn move-forward [robot] (assoc robot :place (mapv + (:place robot) (:direction robot))))

Let's try them:

toyrobot.core=>  (Robot. [0 0] [1 0])
{:place [0 0], :direction [1 0]}

toyrobot.core=>  (turn-left (Robot. [0 0] [1 0]))
{:place [0 0], :direction [0 1]}

toyrobot.core=> (move-forward (Robot. [0 0] [1 0]))
{:place [1 0], :direction [1 0]}

toyrobot.core=> (take 10 (iterate move-forward (Robot. [0 0] [1 0])))
({:place [0 0], :direction [1 0]}
 {:place [1 0], :direction [1 0]}
 {:place [2 0], :direction [1 0]}
 {:place [3 0], :direction [1 0]}
 {:place [4 0], :direction [1 0]}
 {:place [5 0], :direction [1 0]}
 {:place [6 0], :direction [1 0]}
 {:place [7 0], :direction [1 0]}
 {:place [8 0], :direction [1 0]}
 {:place [9 0], :direction [1 0]})

It works!

Thumbnail
  • 13,293
  • 2
  • 29
  • 37
  • I can't quite wrap my head around why you're creating a new Robot record with each call, could you explain? – georgek Feb 28 '14 at 16:07
  • You have to create a new record every time. Records are immutable values: once made, never altered (see http://clojure.org/datatypes). You can do things the Java way (see http://clojure.org/java_interop). I deliberately took a Clojure approach, as this is what the questioner wants to get to grips with. – Thumbnail Feb 28 '14 at 17:44
  • A record is just a glorified map. You don't need to create a new one from scratch for each change, but rather assoc or update-in the changes to get the revised record back. – A. Webb Feb 28 '14 at 18:33
  • I've changed the answer to use assoc, which is neater. However, according to http://stackoverflow.com/questions/4575170/where-should-i-use-defrecord-in-clojure, the assoc effectively creates a new object: there is no structural sharing, so that the adapted object is created from scratch. – Thumbnail Feb 28 '14 at 19:26
  • I made an error in editing move-forward, now corrected. – Thumbnail Mar 01 '14 at 00:45