9

我最近开始学习 Clojure。通常它看起来很有趣,但我不能习惯一些语法上的不便(与以前的 Ruby/C# 经验相比)。

嵌套表达式的前缀表示法。在 Ruby 中,我习惯于使用从左到右的链接/管道来编写复杂的表达式:some_object.map { some_expression }.select { another_expression }。当您逐步从输入值移动到结果时,这真的很方便,您可以专注于单个转换,而无需在键入时移动光标。相反,当我在 Clojure 中编写嵌套表达式时,我将代码从内部表达式编写到外部,并且必须不断移动光标。它减慢并分散注意力。我知道->and->>宏,但我注意到它不是惯用的。当您开始使用 Clojure/Haskell 等进行编码时,您是否遇到过同样的问题?你是怎么解决的?

4

4 回答 4

10

最初我对 Lisps 也有同样的感觉,所以我能感受到你的痛苦 :-)

然而,好消息是您会发现,只要花一点时间并经常使用,您可能会开始喜欢前缀表示法。事实上,除了数学表达式,我现在更喜欢它而不是中缀样式。

喜欢前缀符号的原因:

  • 与函数的一致性——大多数语言混合使用中缀(数学运算符)和前缀(函数调用)表示法。在 Lisps 中,如果您将数学运算符视为函数,那么这一切都是一致的,具有一定的优雅性
  • ——如果函数调用总是在第一个位置,就会变得更加理智。
  • Varargs - 能够为几乎所有操作员提供可变数量的参数真是太好了。(+ 1 2 3 4 5)恕我直言,比1 + 2 + 3 + 4 + 5

然后一个技巧是在以这种方式构建代码具有逻辑意义时自由地使用->和自由。->>这在处理对象或集合的后续操作时通常很有用,例如

(->>
  "Hello World" 
  distinct 
  sort 
  (take 3))

==> (\space \H \W)

我发现在使用前缀样式时非常有用的最后一个技巧是在构建更复杂的表达式时充分利用缩进。如果你缩进得当,那么你会发现前缀符号实际上很容易阅读:

(defn add-foobars [x y]
  (+
    (bar x y)
    (foo y)
    (foo x)))
于 2012-04-24T01:42:00.493 回答
4

据我所知->,并且->>在 Clojure 中是惯用的。我一直在使用它们,在我看来,它们通常会导致代码可读性更高。

以下是这些宏在 Clojure“生态系统”周围的流行项目中使用的一些示例:

举例证明:)

于 2012-04-24T01:21:54.590 回答
3

当我第一次开始使用 lisp 时,我确实看到了同样的障碍,这真的很烦人,直到我看到它使代码更简单、更清晰的方式,一旦我了解了它的好处,烦恼就消失了

initial + scale + offset

变成了

(+ initial scale offset)

然后尝试(+)前缀表示法允许函数指定自己的标识值

user> (*)
1
user> (+)
0

还有很多例子,我的意思不是为前缀表示法辩护。我只是希望传达的是,随着积极的一面变得明显,学习曲线(在情感上)变得平坦。

当然,当您开始编写宏时,前缀表示法就变成了必须具备的,而不是方便。


为了解决您问题的第二部分,线程优先和线程最后的宏在使代码更清晰的任何时候都是惯用的:)它们比纯算术更常用于函数调用,尽管没有人会因为在他们制作时使用它们而责怪你方程更可口。


ps:(.. object object2 object3)-> object().object2().object3()

(doto my-object
   (setX 4)
   (sety 5)`
于 2012-04-24T00:12:21.287 回答
3

如果您的表达式链很长,请使用let. 长的失控表达式或深度嵌套的表达式在任何语言中都不是特别可读的。这是不好的:

(do-something (map :id (filter #(> (:age %) 19) (fetch-data :people))))

这稍微好一点:

(do-something (map :id
                   (filter #(> (:age %) 19)
                           (fetch-data :people))))

但这也很糟糕:

fetch_data(:people).select{|x| x.age > 19}.map{|x| x.id}.do_something

如果我们正在阅读本文,我们需要知道什么?我们正在do_something调用people. 这段代码很难阅读,因为第一个和最后一个之间的距离太远,以至于我们在它们之间旅行时忘记了我们在看什么。

在 Ruby 的情况下,do_something(或任何产生我们最终结果的东西)在行尾迷失了方向,所以很难说出我们在对我们的people. 在 Clojure 的例子中,我们正在做的事情是显而易见的do-something,但是如果不通读整个事情的内部,就很难知道我们在做什么。

任何比这个简单示例更复杂的代码都会变得非常痛苦。如果你所有的代码都像这样,你的脖子会累得在所有这些意大利面条上来回扫描。

我更喜欢这样的东西:

(let [people (fetch-data :people)
      adults (filter #(> (:age %) 19) people)
      ids    (map :id adults)]
  (do-something ids))

现在很明显:我从 开始people,我到处乱跑,然后我do-something对他们。

你可能会逃脱惩罚:

fetch_data(:people).select{|x|
  x.age > 19
}.map{|x|
  x.id
}.do_something

但至少我可能更愿意这样做:

adults = fetch_data(:people).select{|x| x.age > 19}
do_something( adults.map{|x| x.id} )

let即使您的中间表达没有好名字,使用它也并非闻所未闻。(这种风格偶尔会用在 Clojure 自己的源代码中,例如 的源代码defmacro

(let [x (complex-expr-1 x)
      x (complex-expr-2 x)
      x (complex-expr-3 x)
      ...
      x (complex-expr-n x)]
  (do-something x))

这对调试有很大帮助,因为您可以通过以下方式随时检查事物:

(let [x (complex-expr-1 x)
      x (complex-expr-2 x)
      _ (prn x)
      x (complex-expr-3 x)
      ...
      x (complex-expr-n x)]
  (do-something x))
于 2012-04-24T20:12:23.827 回答