2014-02-28 57 views
2

所以我有一個問題,我試圖在clojure解決。這是一個編程練習,我嘗試在許多語言中「學習」語言。設置位置(x,y,主方向),並允許clojure更新

這是我的第一個不可變函數語言,我遇到了一些挑戰。

演習從文件中讀取 部位X,Y,CARDINAL_DIRECTION形式的一些 「指令」(如PLACE 0,0,NORTH)

然後可以使用MOVE

向前移動一個空間

玩家可以使用左右方向左右旋轉。

最終報告將打印出的X,Y,和主方向球員

的我此刻所面臨的挑戰是我不知道在哪裏存儲球員位置。在基於班級的語言中,我會有一個擁有座標的球員類。

下面是我的解決辦法,到目前爲止

(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") 
     ""))) 
+0

我也不太UND知道這個問題。如果我是正確的,那麼當你創建一個對象時,你想要找到一種方法來存儲座標和球員的「狀態」,就像你在面向對象的編程語言中所做的那樣? – albusshin

+0

沒錯,所以我想存儲座標。 基本上,它從地方命令開始,然後移動幾次和報告... 我應該如何存儲x,y,方向? – mark

+0

在函數式語言中,您不應該像在面向對象編程中那樣思考。爲什麼不簡單使用地圖 – albusshin

回答

4

Thumbnail's answer很好,我也不想分散注意力,但是你的小練習也給了我玩Clojure那種我無法抗拒的口齒不清的機會。

;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 [email protected]))) 

(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}) 
與那些在地方

現在你已經幾乎有一個小型的DSL語言通過線程宏觀

(-> (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]} 

事實上,如果你有一個文件你與內容

信任
(def sample-file-contents "(place 3, 3, north) move left move") 

你可以在數據作爲一種形式

閱讀

交織一些報告(在REPL *1是以前的值)

user=> (interleave *1 (repeat 'report)) 
((place 3 3 north) report move report left report move report) 

粘性在穿線宏

user=> (cons '-> *1) 
(-> (place 3 3 north) report move report left report move report) 

而且eval審視你們對於相同的輸出如上

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]} 

要這樣設置一個更恰當不需要具有良好的解析庫

(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)) 

現在太多額外的努力,我們已經放棄了要求的括號(以及包含一些樣本換行符),在REPL重新定義了樣本文件

(def sample-file-contents "place 3, 3, north report 
          move report 
          left report 
          move report") 

並再次顯示中間結果

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) 
(#< [email protected]> #'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]} 
3

你充分的解析指令文件的控制,讓我們專注於內部計算。

Clojure結構本質上是不可變的,所以機器人的歷史是一系列不同的機器人狀態對象,而不是單個機器人對象的一系列狀態。因此,將機器人命令表示爲功能是很自然的,在機器人狀態下,返回另一個機器人狀態。

我們需要

  • 命令turn-leftturn-rightmove-forward
  • 在給定的地方建立一個機器人狀態,並在 給定的方向指向的一些方法。

我們應該如何表示機器人狀態?讓我們保持簡單。

  • 地方僅僅是對數字,X Y之前。因此,原點是 [0 0][5 0]沿x軸是5。
  • 方向是運動矢量,所以[1 0]代表東部的 ,代表南方的[0 -1]。這使得運動很容易做到。
  • 機器人狀態是一個記錄(比結構更容易處理)與 :place:direction字段。

我們先來處理左右改變方向的函數。

我們首先定義一個輔助函數從一個序列生成周期:

(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))) 

...我們用它來生成地圖拍攝每一個方向到一個其左:

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

我們可以顛倒這產生一個地圖,需要每個方向的一個其右:

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

我們將使用這些地圖作爲功能來修改機器人狀態(嚴格來說,要返回修改後的機器人狀態)。

現在我們定義機器人狀態:

(defrecord Robot [place direction]) 

......以及一些功能,我們需要對其進行操作:

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

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

讓我們嘗試一下:

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]}) 

有用!

+0

我無法完全理解爲什麼每次打電話都要創建一個新的機器人記錄,你能解釋一下嗎? – georgek

+0

您必須每次創建新記錄。記錄是不可變的值:一旦創建,永遠不會改變(參見http://clojure.org/datatypes)。你可以用Java的方式來做事情(參見http://clojure.org/java_interop)。我故意採取了Clojure的方法,因爲這是提問者想要掌握的。 – Thumbnail

+0

記錄只是一張榮耀的地圖。您無需爲每次更改從頭創建一個新的更新,而是更改關聯或更新以獲取修訂的記錄。 –

相關問題