我正在学习 Clojure,想要一些关于惯用用法的建议。作为小型统计包的一部分,我有一个计算一组数据的众数的函数。(背景:众数是一组数据中最常见的值。有近十几种已发布的算法来计算它。这里使用的来自 Bernard Rosner 的《生物统计学基础》第 6 版。)

(defn tally-map
 " Create a map where the keys are all of the unique elements in the input
   sequence and the values represent the number of times those elements
   occur. Note that the keys may not be formatted as conventional Clojure
   keys, i.e. a colon preceding a symbol."
  [aseq]
  (apply merge-with + (map (fn [x] {x 1}) aseq)))

(defn mode
 " Calculate the mode. Rosner p. 13. The mode is problematic in that it may
   not be unique and may not exist at all for a particular group of data.
   If there is a single unique mode, it is returned. If there are multiple
   modes, they are returned as a list. If there is no mode, that is all
   elements are present in equal frequency, nil is returned."
  [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        k (keys amap)
        f (fn [x] (not (nil? x)))
        modes (filter f (map #(if (= mx (get amap %)) %) k))
        ]
    (cond (= 1 (count modes)) (first modes)
      (every? #(= mx %) (vals amap)) nil
      :else modes)
    )
  )

我有几个问题:

  1. 论点。该函数接受单个序列。像加法函数一样接受可变数量的参数是否更惯用?
  2. 代码气味。看起来“let”比它应该的要复杂一些——这么多的变量赋值。我是否错过了语言或库的任何明显(或不那么明显)的用途,这些用途将使该方法更加简洁?

先谢谢您的帮助。

有帮助吗?

解决方案

这是我的看法:

  1. 有许多核心的clojure函数将序列作为参数,而其他函数则采用多个参数,因此我认为没有真正的惯用法。如果你已经有序列中的数据,我会使用seq作为参数,因为它会为你节省一个应用调用。

  2. 我不会编写一个在某些情况下返回值的函数和在其他情况下返回值的列表,因为调用代码在使用之前总是必须检查返回值。相反,我会将一个模式作为seq返回,只包含一个项目。但您可能有自己的理由,具体取决于调用此函数的代码。

  3. 除此之外,我会像这样重写模式函数:

    (defn mode [aseq]
      (let [amap (tally-map aseq)
            mx (apply max (vals amap))
            modes (map key (filter #(= mx (val %)) amap))
            c (count modes)]
        (cond
          (= c 1) (first modes)
          (= c (count amap)) nil
          :default modes)))
    

    您可以使用标识函数(除非您的数据包含逻辑上错误的值),而不是定义函数。但你甚至不需要那样做。我以不同的方式找到模式,这对我来说更具可读性:地图amap充当一系列地图条目(键值对)。首先,我只过滤那些值为mx的条目。然后我在这些上映射关键功能,给我一系列按键。

    要检查是否有任何模式,我不会再次循环遍历地图。相反,我只是将模式数量与地图条目数量进行比较。如果它们相等,则所有元素都具有相同的频率!

    这是始终返回seq的函数:

    (defn modes [aseq]
      (let [amap (tally-map aseq)
            mx (apply max (vals amap))
            modes (map key (filter #(= mx (val %)) amap))]
        (when (< (count modes) (count amap)) modes)))
    

其他提示

在我看来,将一些函数映射到集合上然后立即将列表缩减为一个项目是使用 reduce 的标志。

(defn tally-map [coll]
  (reduce (fn [h n]
            (assoc h n (inc (h n 0))))
          {} coll))

在这种情况下,我会编写 mode fn,将一个集合作为参数,就像你一样。我可以想到为这样的函数使用多个参数的唯一原因是你计划必须经常输入文字参数。

所以,例如,这是一个交互式的REPL脚本,你经常会按字面意思输入(模式[1 2 1 2 3 3]),那么你应该让函数接受多个参数,以免你输入函数调用中的额外 [] 始终。如果您打算从文件中读取大量数字然后采用这些数字的模式,那么让函数采用一个集合的参数,这样您就可以避免一直使用 apply 。我猜你最常见的用例是后者。我相信 apply 还会增加您在进行带有集合参数的函数调用时避免的开销。

我同意其他人的意见,你应该 mode 返回结果列表,即使只有一个;它会让你的生活更轻松。

可能会重命名 modes

这是一个很好的简洁实现 mode:

(defn mode [data] 
  (first (last (sort-by second (frequencies data)))))

这利用了以下事实:

  • frequencies 函数返回值 -> 频率的映射
  • 您可以将映射视为一系列键值对
  • 如果您按值对该序列进行排序( second 每对中的项目),那么序列中的最后一项将代表模式

编辑

如果您想处理多模式情况,那么您可以插入一个额外的 partition-by 保留所有具有最大频率的值:

(defn modes [data] 
  (->> data
       frequencies 
       (sort-by second)
       (partition-by second)
       last
       (map first)))

对我来说很好看。我会替换

f (fn [x] (not (nil? x)))
mode (filter f (map #(if (= mx (get amap %)) %) k))

mode (remove nil? (map #(if (= mx (get amap %)) %) k))

(我不知道为什么 not-nil?之类的东西不在 clojure.core 中;这是每天都需要的东西。)

  

如果存在单个唯一模式,则返回该模式。如果有多种模式,它们将作为列表返回。如果没有模式,即所有元素以相等的频率存在,则返回nil。“

你可以考虑每次只返回一个seq(一个元素或空格是好的);否则,必须通过调用代码来区分这些情况。通过始终返回seq,您的结果将神奇地用作期望seq的其他函数的参数。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top