函数式的Clojure和命令式的Groovy,哪个更易读?

15

好的,现在不能作弊。

真的,请花一两分钟尝试一下。

"positions"是做什么用的?

编辑:根据cgrand的建议进行了简化。

(defn redux [[current next] flag] [(if flag current next) (inc next)])

(defn positions [coll]
  (map first (reductions redux [1 2] (map = coll (rest coll)))))

现在,这个版本怎么样?

def positions(coll) {
  def (current, next) = [1, 1]
  def previous = coll[0]
  coll.collect {
    current = (it == previous) ? current : next
    next++
    previous = it
    current
  }
}

我正在学习Clojure,并且非常喜欢它,因为我一直喜欢函数式编程。我花费更长的时间来想出Clojure的解决方案,但我享受思考优雅解决方案的过程。Groovy的解决方案还可以,但我已经觉得这种命令式编程方式枯燥乏味和机械化了。在Java 12年后,我感到困境,而使用Clojure进行函数式编程正是我所需要的提升。

好了,言归正传。说实话,我不确定在几个月后回到Clojure代码时是否能够理解它。当然,我可以对代码进行详细注释,但我不需要对我的Java代码进行注释才能理解它。

所以我的问题是:这是逐渐习惯函数式编程模式的问题吗?函数式编程专家们读这段代码时是否会轻松理解?哪个版本你觉得更容易理解?

编辑:此代码的作用是根据玩家的分数计算其位置,同时跟踪并列的玩家。例如:


Pos Points
1. 36
1. 36
1. 36
4. 34
5. 32
5. 32
5. 32
8. 30
7个回答

22
我认为内在的易读性并不存在。只有你习惯和不习惯的。我能够阅读两个版本的代码,尽管我不懂Groovy,但我实际上可以更容易地阅读您的Groovy版本,因为我也花了十年时间看C和Java,只花了一年时间看Clojure。这并没有说明任何语言的易读性,只是说明了我个人的情况。
同样地,我比西班牙语更容易阅读英文,但这也并不能说明这些语言本质上的易读性。(实际上,从简单性和一致性方面来看,西班牙语可能是“更易读”的语言,但我仍然无法阅读它)。我现在正在学习日语,遇到了很大的困难,但母语为日语的人也会对英语感到同样的困难。
如果你大部分时间都在阅读Java,那么像Java的东西肯定比其他东西更容易阅读。除非你花费跟阅读类似于C的语言相同的时间去阅读Lispy语言,否则这种情况可能会持续下去。
要理解一门语言,你需要熟悉以下内容:
- 语法(例如[vector](list)hyphens-in-names) - 词汇表(reductions是什么意思?你可以在哪里查找?) - 计算规则(将函数视为对象是否可行?在大多数语言中这是错误的。) - 习惯用法,如(map first (some set of reductions with extra accumulated values)) 所有这些都需要时间、实践和重复才能学会和内化。但如果你在接下来的6个月里阅读和编写大量的Clojure代码,不仅你将能够理解那段Clojure代码,而且也可能比现在更好地理解它,甚至可以简化它。像这样:
(use 'clojure.contrib.seq-utils)                                        ;;'
(defn positions [coll]
  (mapcat #(repeat (count %) (inc (ffirst %)))
          (partition-by second (indexed coll))))

看着我一年前写的Clojure代码,我感到震惊,因为它非常糟糕,但是我可以读懂它。(不是说你的Clojure代码很糟糕; 我完全没有阅读障碍,而且我也不是大师。)


非常出色的回答。我喜欢你的版本。我承认我花了一些时间才理解它。这真是太聪明了!谢谢。 - foxdonut

8

编辑:可能不再相关。

对我来说,Clojure版本很复杂。它包含更多需要理解的抽象概念。这就是使用高阶函数的代价,你必须知道它们的意义。因此,在孤立的情况下,命令式编程需要更少的知识。但是,抽象的威力在于它们的组合方式。每个命令式循环都必须被读取和理解,而序列抽象允许你去除循环的复杂性并组合强大的操作。

我还会进一步认为Groovy版本至少部分上是函数式的,因为它使用了collect,这实际上是map,一个高阶函数。它也有一些状态。

以下是我如何编写Clojure版本:

(defn positions2 [coll]
  (let [current (atom 1)
        if-same #(if (= %1 %2) @current (reset! current (inc %3)))]
    (map if-same (cons (first coll) coll) coll (range (count coll)))))

这与Groovy版本非常相似,因为它使用可变的“current”,但不同之处在于它没有next/prev变量 - 而是使用不可变序列来代替。正如Brian所说,易读性并非固有的。对于这个特定情况,我更喜欢这个版本,并且似乎处于中间位置。


很酷 :) 我并不是在批评你的代码,我只是回答了关于哪种风格更容易理解的问题。现在目标已经改变了!嘿嘿嘿。 - Timothy Pratley
1
嗯,我并没有把它当作批评,而且,你说得对,第一个版本确实有些复杂。 :-) - foxdonut

8

我同意Timothy的观点:你引入了过多的抽象概念。我重新编写了你的代码,最终得到了以下结果:

(defn positions [coll]
  (reductions (fn [[_ prev-score :as prev] [_ score :as curr]] 
                (if (= prev-score score) prev curr))
    (map vector (iterate inc 1) coll)))

关于你的代码,

(defn use-prev [[a b]] (= a b))
(defn pairs [coll] (partition 2 1 coll))
(map use-prev (pairs coll))

可以简单地重构为:

(map = coll (rest coll))

谢谢,我根据你的重构建议已经编辑了上面的内容。 - foxdonut

4
Clojure看起来更加复杂,但可能更加优雅。 面向对象编程是为了让语言在更高层次上更易于理解。 函数式语言似乎更具有“算法”(基本/初级)的感觉。 这只是我目前的感受。当我使用Clojure的经验更多时,也许会改变。
我担心我们正在陷入哪种语言可以最简洁地解决问题或用最少的代码行解决问题的游戏中。
对我来说,问题有两个方面:
1.一开始很容易理解代码在做什么? 这对于代码维护者非常重要。
2.猜测代码背后的逻辑有多容易? 过于冗长?太简洁?
“尽可能简单,但不要更简单。” 阿尔伯特·爱因斯坦

我认为问题不在于代码行数最少。至少这不是我的意图。问题更多的是你优雅地列举出来的那些。例如,在此脚本中,我更喜欢我的代码版本,而不是'gnud'的更短但可读性较差(在扩展注释中)的版本:https://dev59.com/dHVC5IYBdhLWcg3wxEN1#245724 - foxdonut

3

我知道这不是对问题的回答,但如果有测试,我将能够更好地“理解”代码,例如:

assert positions([1]) == [1]
assert positions([2, 1]) == [1, 2]
assert positions([2, 2, 1]) == [1, 1, 3]
assert positions([3, 2, 1]) == [1, 2, 3]
assert positions([2, 2, 2, 1]) == [1, 1, 1, 4]

这将告诉我,从现在起一年内代码预计要做什么。比我在这里看到的任何优秀版本的代码都要好得多。

我真的离题了吗?

另一件事是,我认为“可读性”取决于上下文。它取决于谁来维护代码。例如,为了维护Groovy代码的“功能”版本(无论多么出色),不仅需要Groovy程序员,还需要能够编写功能Groovy代码的程序员......另一个更相关的例子是:如果几行代码使“初学者”Clojure程序员更容易理解,那么整个代码将更具可读性,因为它将被更大的社区理解:不需要学习三年Clojure才能掌握代码并进行编辑。


3

Groovy支持多种解决这个问题的样式:

coll.groupBy{it}.inject([]){ c, n -> c + [c.size() + 1] * n.value.size() }

这段代码明显没有进行过重构,但是并不难理解。


3
我也正在学习Clojure并且非常喜欢。但在我目前的学习阶段,Groovy版本更易理解。不过让我喜欢Clojure的是阅读代码时会有“啊哈!”的体验,当你最终“领悟”到代码的含义时。而我真正享受的是几分钟后的相似体验,当你意识到代码可以应用于其他类型的数据,并且无需对代码进行任何更改时。我已经数不清在Clojure中通过一些数字代码工作后,后来想到同样的代码如何用于字符串、符号、小部件等的次数了。

我使用的比喻是关于学习颜色。还记得当您第一次认识红色时吗?您很快就明白了 - 世界上有这么多红色的东西。然后您听到品红色的术语,并迷失了一段时间。但是再次接触后,您就理解了这个概念,并且有了一种更具体的描述某个颜色的方式。您需要内化这个概念,在头脑中保留更多信息,但最终会得到更强大和简洁的东西。


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