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)
该练习从一个文件中读取一些“指令”,以X、Y、CARDINAL_DIRECTION(例如,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
最后,报告将打印出玩家的x、y和基本方向。
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.
我现在面临的挑战是,我不知道该在哪里储存球员的位置。在以类为基础的语言中,我有一个具有协调功能的player类。
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")
"")))
2 个解决方案
#1
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.
拇指甲的回答很好,我并不想分散你的注意力,但是你的小练习也给了我一个机会,让你可以玩我无法抗拒的Clojure的lispe。
;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
现在已经有了这些,你已经在一个微型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]}
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)
插入一些报告(在REPL *1中是先前的值)
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
然后在REPL中显示中间结果。
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]}
#2
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.
Clojure结构本质上是不可变的,因此机器人的历史是一个不同的机器人状态对象的序列,而不是单个机器人对象的序列。因此,将机器人的命令表示为函数是很自然的,因为给定一个机器人状态,返回另一个机器人状态。
We need
我们需要
- commands to
turn-left
,turn-right
, andmove-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. - 位置是简单的数对,x在y之前,所以原点是[0 0]而[5 0]是5沿着x轴。
- Directions are movement vectors, so that
[1 0]
stands for east and[0 -1]
stands for south. This makes movement easy to do. - 方向是运动矢量,所以[1 0]代表东方,[0 -1]代表南方。这使得动作很容易做。
- 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!
它的工作原理!
#1
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.
拇指甲的回答很好,我并不想分散你的注意力,但是你的小练习也给了我一个机会,让你可以玩我无法抗拒的Clojure的lispe。
;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
现在已经有了这些,你已经在一个微型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]}
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)
插入一些报告(在REPL *1中是先前的值)
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
然后在REPL中显示中间结果。
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]}
#2
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.
Clojure结构本质上是不可变的,因此机器人的历史是一个不同的机器人状态对象的序列,而不是单个机器人对象的序列。因此,将机器人的命令表示为函数是很自然的,因为给定一个机器人状态,返回另一个机器人状态。
We need
我们需要
- commands to
turn-left
,turn-right
, andmove-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. - 位置是简单的数对,x在y之前,所以原点是[0 0]而[5 0]是5沿着x轴。
- Directions are movement vectors, so that
[1 0]
stands for east and[0 -1]
stands for south. This makes movement easy to do. - 方向是运动矢量,所以[1 0]代表东方,[0 -1]代表南方。这使得动作很容易做。
- 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!
它的工作原理!