Clojure:如何确定嵌套列表是否包含非数字项?

Clojure: How to determine if a nested list contains non-numeric items?

我需要编写一个 Clojure 函数,它将未经评估的任意深度嵌套列表作为输入,然后确定列表中的任何项目(不在函数位置)是否为非数字。这是我第一次在 Clojure 中写任何东西,所以我有点困惑。这是我第一次尝试制作该功能:

(defn list-eval
  [x]
  (for [lst x]
    (for [item lst]
      (if(integer? item)
        (println "")
        (println "This list contains a non-numeric value")))))

我尝试使用嵌套 for 循环遍历每个嵌套列表中的每个项目。尝试像这样测试函数:

=> (list-eval (1(2 3("a" 5(3)))))

导致此异常:

ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  listeval.core/eval7976 (form-init4504441070457356195.clj:1)

这里的问题是出在代码上,还是出在我如何调用函数和传递参数上?无论哪种情况,我怎样才能按预期进行这项工作?

发生这种情况是因为 (1 ..) 被视为调用函数,而 1Long,而不是函数。首先,您应该将嵌套列表更改为 '(1(2 3("a" 5(3))))。接下来,您可以递归地将函数更改为 运行:

(defn list-eval
  [x]
  (if (list? x)
    (for [lst x] (list-eval lst))
    (if (integer? x)
      (println "")
      (println "This list contains a non-numeric value"))))
=> (list-eval '(1(2 3("a" 5(3)))))

有一个名为 tree-seq 的很酷的函数可以为您完成遍历结构的所有艰苦工作。使用它然后删除任何集合,删除所有数字,并检查是否还有任何剩余。

(defn any-non-numbers?
  [x]
  (->> x
       (tree-seq coll? #(if (map? %) (vals %) %))
       (remove (some-fn coll? number?))
       not-empty
       boolean))

示例:

user=> (any-non-numbers? 1)
false
user=> (any-non-numbers? [1 2])
false
user=> (any-non-numbers? [1 2 "sd"])
true
user=> (any-non-numbers? [1 2 "sd" {:x 1}])
true
user=> (any-non-numbers? [1 2 {:x 1}])
false
user=> (any-non-numbers? [1 2 {:x 1 :y "hello"}])
true

如果您也想考虑地图键,只需将 (vals %) 更改为 (interleave (keys %) (vals %))

引用

正如其他人提到的,您需要引用一个列表以防止它被评估为 代码。这就是您所看到的异常的原因。

用于嵌套

for 只会下降到您指定的嵌套深度。它不是for循环, 如您所料,但序列理解,如 python 列表理解。

(for [x xs, y ys] y) 将假定 xs 是列表的列表并将其展平。 (for [x xs, y ys, z zs] z) 相同但多了一层嵌套。

要深入到任何深度,您通常会使用递归。 (有很多方法可以迭代地执行此操作,但它们更难让您全神贯注。)

副作用

您正在惰性序列中产生副作用(打印)。这将在 repl 工作, 但如果您不在任何地方使用结果,它就不会 运行 并造成很大的混乱。 这是每个新的 clojurian 都会在某个时候遇到的事情。 (doseq 类似于 for,但有副作用。)

clojure 方法是将处理值的函数与处理值的函数分开 "do stuff",喜欢打印到发射导弹的控制台,并保持 尽可能简单的副作用函数。

综合起来

让我们把问题陈述清楚: 任意嵌套列表?如果有,则向控制台打印一条消息。

在很多情况下,当您在其他语言中使用 for 循环时,reduce 就是您在 clojure 中想要的。

(defn collect-nested-non-numbers
  ;; If called with one argument, call itself with empty accumulator
  ;; and that argument.
  ([form] (collect-nested-non-numbers [] form))
  ([acc x]
   (if (coll? x)
     ;; If x is a collection, use reduce to call itself on every element.
     (reduce collect-nested-non-numbers acc x)

     ;; Put x into the accumulator if it's a non-number
     (if (number? x)
       acc
       (conj acc x)))))

;; A function that ends in a question mark is (by convention) one that
;; returns a boolean.
(defn only-numbers? [form]
  (empty? (collect-nested-non-numbers form)))

;; Our function that does stuff becomes very simple.
;; Which is a good thing, cause it's difficult to test.
(defn warn-on-non-numbers [form]
  (when-not (only-numbers? form)
    (println "This list contains a non-numeric value")))

那行得通。不过,已经存在很多可以帮助您遍历嵌套结构的东西,因此您不需要手动完成。

clojure 附带了 clojure.walk 命名空间。这是为了当你有 一个嵌套的东西,想要改造它的某些部分。有 tree-seq 的解释 在另一个答案中。 Specter 是一个库,它是 一种非常强大的迷你语言,用于表达嵌套结构的转换。

然后是我的实用程序库 comfy,其中包含 clojure.walk 中的函数,因为当你有一个嵌套的东西并想 "reduce" 它成为一个单一的值时。

这样做的好处是您可以使用 reduced,它类似于命令式 break 语句,但用于 reduce。如果它找到一个非数字,则不需要继续遍历整个过程。

(ns foo.core
  (:require
   [madstap.comfy :as comfy]))

(defn only-numbers? [form]
  (comfy/prewalk-reduce
   (fn [ret x]
     (if (or (coll? x) (number? x))
       ret
       (reduced false)))
   true
   form))

也许 "any item in the list (not in function position)" 你是这个意思?

(defn only-numbers-in-arg-position? [form]
  (comfy/prewalk-reduce
   (fn [ret x]
     (if (and (list? x) (not (every? (some-fn number? list?) (rest x))))
       (reduced false)
       ret))
   true
   form))