在Clojure中处理嵌套的map/vector结构的惯用且简洁的方式。

4
我是Clojure的新手,为了学习这门语言,我正在用Clojure重写我的一个旧Groovy脚本。该脚本查询JIRA实例的时间条目,接收JSON结果并根据响应生成报告。
我意识到在S.O上已经有无数关于遍历嵌套结构的问题,但是我没有找到直接的答案,因此我希望得到Clojurist的帮助,找到一种惯用且简洁的方法。核心问题是通用的,与这个特定的代码片段无关。
我希望用Clojure重写以下内容:
// GROOVY CODE
// this part is just here for context
def timeFormat = DateTimeFormat.forPattern('yyyy/MM/dd')
def fromDate   = timeFormat.parseDateTime(opts.f)
def toDate     = timeFormat.parseDateTime(opts.t)

def json       = queryJiraForEntries(opts, http)
def timesheets = [:].withDefault { new TimeSheet() }

// this is what I'm hoping to find a better way for
json.issues.each { issue ->
  issue.changelog.histories.each { history ->
    def date = DateTime.parse(history.created)
    if (date < fromDate || date > toDate) return

    def timeItems = history.items.findAll { it.field == 'timespent' }
    if (!timeItems) return

    def consultant = history.author.displayName
    timeItems.each { item ->
      def from = (item.from ?: 0) as Integer
      def to   = (item.to   ?: 0) as Integer

      timesheets[consultant].entries << new TimeEntry(date: date, issueKey: issue.key, secondsSpent: to - from)
    }
  }
}

(可以在这里找到返回的JSON的示例结构)

请注意,当我们创建结果时间条目时,我们使用最外层的issue.key,中间层的date,以及嵌套结构的最内层的fromto

在Groovy中,each循环中的return仅存在于最内部的each中。我相信其余的代码应该更或多或少是不言自明的。

因此,我试图解决的通用问题是:给定一个深度嵌套的映射和列表结构:

  • 遍历/筛选到结构的特定深度
  • 在该深度级别上执行某些操作,并将结果添加到上下文中
  • 更深入地遍历/筛选结构
  • 在该深度级别上执行某些操作,并将结果添加到上下文中
  • ...
  • 在某个最终级别上,根据上下文中的数据和该级别可用的数据生成结果。

我发现这种带有上下文的遍历和转换数据的模式越来越常见。

我的当前解决方案比groovy的更冗长,对于我这种不擅长阅读clojure代码的人来说,一眼看去要难得多。解析日期等细节并不重要。我正在寻找一个简洁的clojure模式。

编辑1:根据评论请求,这是我的当前代码。我提前道歉,并无耻地归咎于我完全的新手:

;; CLOJURE CODE
(defn valid-time-item? [item]
  (and (= (:field item) "timespent") (:to item) (:from item)))

(defn history->time-items [history]
  (filter valid-time-item? (:items history)))

(defn history-has-time-items? [history]
  (not-empty (history->time-items history)))

(defn history-in-date-range? [opts history]
  (tcore/within? (tcore/interval (:from-date opts) (:to-date opts))
                 (tformat/parse (tformat/formatters :date-time) (:created history))))

(defn valid-history? [opts h]
  (and (history-has-time-items? h) (history-in-date-range? opts h)))

(defn issue->histories-with-key [issue]
  (map #(assoc % :issue-key (:key issue))(get-in issue [:changelog :histories])))

(defn json->histories [opts json]
  (filter #(valid-history? opts %) (flatten (map issue->histories-with-key (:issues json)))))

(defn time-item->time-entry [item date issue-key]
  (let [get-int (fn [k] (Integer/parseInt (get item k 0)))]
    {:date          (tformat/unparse date-formatter date)
     :issue-key     issue-key
     :seconds-spent (- (get-int :to) (get-int :from)) }))

(defn history->time-entries [opts history]
  (let [date       (tformat/parse (tformat/formatters :date-time) (:created history))
        key        (:issue-key history)]
    (map #(time-item->time-entry % date key) (history->time-items history))))

(defn json->time-entries [opts json]
  (flatten (map #(history->time-entries opts %) (json->histories opts json))))

(defn generate-time-report [opts]
  (json->time-entries opts (query-jira->json opts)))

为了简洁起见,上述代码省略了某些脚手架等内容。上述中的入口点是generate-time-report,它返回一组映射。

issue->histories-with-key中,我通过将问题键插入到每个历史记录映射中来保留issue.key上下文。除了代码的一般结构外,这是我发现丑陋和不可扩展的地方之一。此外,我还没有将consultant维度添加到clojure解决方案中。

编辑2:经过一些调整和评论以及下面答案的输入后,进行了第二次尝试。这个更短,使用了更接近原始代码的结构,并包含原始代码中的consultant部分。

;; CLOJURE CODE - ATTEMPT 2
(defn create-time-entry [item date consultant issue-key]
  (let [get-int #(Integer/parseInt (or (% item) "0"))]
    {:date          (f/unparse date-formatter date)
     :issue-key     issue-key
     :consultant    consultant
     :seconds-spent (- (get-int :to) (get-int :from)) }))

(defn history->time-entries [history issue-key from-date to-date]
  (let [date       (f/parse (f/formatters :date-time) (:created history))
        items      (filter #(= (:field %) "timespent") (:items history))
        consultant (get-in history [:author :displayName])]
    (when (and (t/within? (t/interval from-date to-date) date) (not-empty items))
      (map #(create-time-entry % date consultant issue-key) items))))

(defn issue->time-entries [issue from-date to-date]
  (mapcat #(history->time-entries % (:key issue) from-date to-date)
          (get-in issue [:changelog :histories])))

(defn json->time-entries [json from-date to-date]
  (mapcat #(issue->time-entries % from-date to-date) (:issues json)))

(defn generate-time-report [opts]
  (let [{:keys [from-date to-date]} opts]
    (filter not-empty
            (json->time-entries (query-jira->json opts) from-date to-date))))

1
请在Clojure代码中发布您尝试过的内容。 - Frank C.
Clojure尝试已添加到问题中。 - Matias Bjarland
3
我没有特别偏好,但至少根据 一个Clojure风格指南 ,在转换方法名称中使用 -> 看起来是一种推荐的做法。你有什么看法? - Matias Bjarland
1
你的代码可能更适合使用声明性数据描述和验证,例如Plumatic/Schema提供的功能。我不确定新的Clojure spec是否适用。 - Thumbnail
我仍会尝试使用模式和自定义转换来实现这个。 - Matias Bjarland
显示剩余5条评论
2个回答

3
两年之后,我想提出一个对自己问题的建议性解决方案。在Clojure中,解决“向下深入到某个深度,捕获一些上下文,继续向下深入,再次捕获一些上下文”的问题,似乎可以使用for comprehensions。
使用for comprehensions可以像以下示例一样操作:
(defn clojure-rewrite [opts http]
  (let [from-date (local-date-time (:f opts) 0)
        to-date   (local-date-time (:t opts) 23 59)
        json      (query-jira opts http)]
    (for [issue   (-> json :issues)
          history (-> issue :changelog :histories)
          :let    [date (local-date-time df (:created history))]
          :when   (before? from-date date to-date)
          item    (:items history)
          :when   (= (:field item) "timespent")
          :let    [secs #(Integer/parseInt (or (% item) "0"))]]
      {:consultant    (-> history :author :displayName)
       :date          date
       :issue-key     (:key issue)
       :seconds-spent (- (secs :to) (secs :from))})))

为了基本实现原始的Groovy代码所做的事情,该方法返回一个在Clojure中习惯用法的映射集合。

该代码使用Java 8和出色的clojure.java-time库(由于篇幅限制,require未包含在代码中),这使得日期解析有所不同,但我认为该模式是相当普适的。

我认为这说明可以在Clojure中解决原始(而相当通用)的问题,并保持简洁。实际上,甚至比我的初始目标和希望的Groovy代码更加简洁。


2

我认为你的Clojure代码一点都不差。以下是我会做的改进。只需要做出几个小改动。

(defn valid-time-item? [item]
  (and (= (:field item) "timespent") (:to item) (:from item)))

(defn history->time-items [history]
  (filter valid-time-item? (:items history)))

(defn history-has-time-items? [history]
  (not-empty (history->time-items history)))

(defn history-in-date-range? [history from-date to-date]
  (tcore/within? (tcore/interval from-date to-date)
                 (tformat/parse (tformat/formatters :date-time) (:created history))))

(defn valid-history? [h from-date to-date]
  (and (history-has-time-items? h) (history-in-date-range? h from-date to-date)))

(defn issue->histories-with-key [issue]
  (map #(assoc % :issue-key (:key issue)) (get-in issue [:changelog :histories])))

(defn time-item->time-entry [item date issue-key]
  (let [get-int (fn [k] (Integer/parseInt (get item k 0)))]
    {:date date
     :issue-key issue-key
     :seconds-spent (- (get-int :to) (get-int :from))}))

(defn history->time-entries [history]
  (map #(time-item->time-entry % (:created history) (:issue-key history)) (history->time-items history)))

(defn json->time-entries [json opts]
  (let [{:keys [from-date to-date]} opts]
    (->> json
         :issues
         (mapcat issue->histories-with-key)
         (filter #(valid-history? % from-date to-date))
         (mapcat #(history->time-entries %)))))

(defn generate-time-report [opts]
  (json->time-entries opts (query-jira->json opts)))

主要变化

  • 取消了嵌套的json->time-entries实现。现在清楚了json如何变成time-entries。例如:json -> issues -> history -> time-entry。
  • 使用mapcat代替(flatten (map ...
  • 提前解构:from-date:to-date。我认为将from-dateto-date发送到函数中使函数签名更易读,而不是使用opts
  • 交换subjectjson)和opts的位置。将最重要的参数放在第一位,除非它是接受lambda的集合函数,如mapfilter等。

非常感谢您的答案和指引,非常感激。我忘记了 :keys 解构和线程最后确实使代码更易于理解。我已经在问题中进行了更改,事情仍然按预期工作,所以再次感谢您。我仍然想知道有经验的 Clojure 用户是否会这样解决问题。或者他们可能会使用完全不同的方法吗?你会吗?出于好奇,我会让这个问题保持开放状态一段时间,看看是否会出现其他有趣的观点。否则,我认为这是一个被接受的答案。 - Matias Bjarland
在我看来,这段代码有点太多的辅助函数了。我刚刚注意到你上传了更多的JSON输入文件内容。我想重新从头开始写一遍,看看我会如何不同地处理它。我们将看看结果如何。 - tap
将此标记为已接受的答案。我会认为我的代码整体模式或多或少符合惯用的Clojure语言。感谢您提供的指导。 - Matias Bjarland
一年过去了,我仍然对这个模式感到不满意。仅仅比较 Groovy 和 Clojure 的可读性就让我有些不满意。别误会,我喜欢 Clojure,但必须有更简洁的方法。也许我会尝试使用 Specter 来编写它。 - Matias Bjarland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接