Clojure实现Conway生命游戏添加Glider到网格报错问询
Clojure实现康威生命游戏滑翔机添加问题
已实现的基础函数
使用Clojure实现Conway's Game of Life过程中,已完成可正常运行的核心基础函数如下:
(ns glider.core (:gen-class)) (defn new-grid "Creates a 2d grid with rows*cols." [rows cols] (partition cols (take (* rows cols) (repeat false)))) (defn get-coordinates "Returns the row/col coordinates of each 2d grid field." [grid] (let [rows (count grid) cols (count (get grid 0))] (partition cols (for [r (range rows) c (range cols)] [r c])))) (defn set-at "Sets the field at row/col to state." [grid row col state] (assoc grid row (assoc (get grid row) col state)))
三个基础函数的功能:
new-grid:创建指定行列数的二维网格,所有单元格初始状态为false(约定true为细胞存活,false为细胞死亡)get-coordinates:返回二维网格所有单元格对应的行列坐标,结果按行分组set-at:接收网格、行号、列号、目标状态,返回修改指定位置状态后的新网格
问题代码与报错
编写add-glider函数,尝试将滑翔机图案添加到网格的右下象限:
(defn add-glider "Adds a glider pattern to the lower-right quadrant of the grid." [grid] (let [rows (count grid) cols (count (get grid 0)) row-offset (/ rows 2) col-offset (/ cols 2) glider [[false true false] [false false true] [true true true]] glider-coords (get-coordinates glider) grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)] (apply (fn [[r c]] (set-at grid r c true)) grid-coords)))
原实现思路:
- 计算网格行列数,取网格中点作为行列偏移量
- 定义3*3的滑翔机布尔状态矩阵
- 调用
get-coordinates生成滑翔机自身坐标,通过偏移计算得到滑翔机在目标网格上的对应坐标 - 将这些坐标位置的单元格状态设置为
true
配套测试代码:
(defn -main "Creates a grid and adds a glider to it." [& args] (let [grid (new-grid 8 8) grid-with-glider (add-glider grid)] (println grid) (println grid-with-glider)))
通过lein运行时抛出错误:
$ lein repl glider.core=> (-main) Execution error (ClassCastException) at glider.core/add-glider$fn (core.clj:32). class clojure.lang.PersistentVector cannot be cast to class java.lang.Number (clojure.lang.PersistentVector is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
报错指向第32行代码:
grid-coords (map (fn [[r c]] [(+ r row-offset) (+ c col-offset)]) glider-coords)
需要解答两个问题:
- 上述错误信息的具体含义是什么?
- 当前使用
apply的实现思路是否合理,这类批量设置网格单元格的需求应如何正确实现?
问题解答
1. 错误信息含义
这个类型转换错误的核心是传入+运算符的参数类型不符合要求:+仅接受数字类型参数,但实际传入了Clojure的向量(PersistentVector)类型,无法转换为数字。
导致问题的根源是对get-coordinates返回值的结构理解错误:get-coordinates会用partition按行对坐标分组,3*3的滑翔机调用该函数返回的是嵌套结构(((0,0) (0,1) (0,2)) ((1,0) (1,1) (1,2)) ((2,0) (2,1) (2,2))),而非平铺的单个坐标对序列。后续map处理时,每次取到的元素是一整行的3个坐标组成的向量,不是单个[r c]坐标对,解构时r会拿到行内第一个坐标(本身是向量类型),传入+做运算时就触发了类型错误。
代码还隐藏了另一个问题:Clojure中/默认返回分数类型,当网格行列数为奇数时,偏移量会是分数,无法作为序列索引使用。
2. 实现思路问题与正确实现方案
原代码中apply的用法完全不合理,存在两个核心缺陷:
apply会把后续集合的所有元素展开作为参数传入目标函数,但原代码写的匿名函数仅接受1个坐标参数,即使坐标结构正确,也只会接收到第一个坐标,剩余参数会被直接忽略set-at是纯函数,每次调用仅返回修改了单个位置的新网格,原代码每次调用set-at都基于原始未修改的grid操作,不会累积修改结果,就算参数正确最终也只会保留最后一次修改的效果
这类批量更新不可变结构的需求,标准实现方式是使用reduce做累积迭代,修正后的完整add-glider实现如下:
(defn add-glider "Adds a glider pattern to the lower-right quadrant of the grid." [grid] (let [rows (count grid) cols (count (first grid)) ;; 用quot做整数除法,保证偏移量是合法的整数索引 row-offset (quot rows 2) col-offset (quot cols 2) glider [[false true false] [false false true] [true true true]] ;; 遍历滑翔机矩阵,取出所有需要设为存活状态的目标坐标 glider-cells (for [[r row] (map-indexed vector glider) [c state] (map-indexed vector row) :when state] [(+ r row-offset) (+ c col-offset)])] ;; 用reduce累积修改:初始值是原始网格,每次迭代更新一个坐标的状态 (reduce (fn [g [r c]] (set-at g r c true)) grid glider-cells)))
修正点说明:
- 用
quot做整数除法计算偏移量,保证索引为合法整数 - 不再调用
get-coordinates获取嵌套的分组坐标,直接通过map-indexed遍历滑翔机矩阵,拿到每个单元格的行号、列号、存活状态,仅过滤出需要设为true的存活单元格坐标 - 用
reduce替代错误的apply实现,迭代过程中始终基于上一次修改生成的新网格做更新,保证所有修改都能累积生效
内容的提问来源于stack exchange,提问作者Patrick Bucher
相关产品推荐
相关产品推荐

