从中缀表达式转换为前缀表达式的过程

9

最近我开始学习Clojure。一般来说,它看起来很有趣,但是与以前的Ruby/C#经验相比,我无法适应一些语法上的不便。

嵌套表达式的前缀符号表示法。在Ruby中,我习惯用从左到右的方式链接/管道化复杂的表达式:some_object.map { some_expression }.select { another_expression }。这真的很方便,因为您可以逐步从输入值转移到结果,您可以专注于单个转换,并且在打字时不需要移动光标。相反,在编写Clojure中的嵌套表达式时,我从内部表达式向外部编写代码,必须不断移动光标。它会减慢速度并分散注意力。我知道->->>宏,但我注意到这不是惯用法。当您开始在Clojure / Haskell等中编码时,是否遇到了同样的问题?您是如何解决的?


我觉得这只是惯用语,因为这是你习惯的。前缀表示法比中缀表示法更一致,因此一旦你习惯了它,阅读起来可能会更容易。 - Mike Axiak
Mike,假设你需要编写表达式(filter #(expr1) (map #(expr2) expr3))。你会从内部的expr3开始输入还是从filter开始? - Alexey
3
我通常从外部开始,朝内逐步处理。所以我会想想我想要什么,然后再从那个目标往回走。 - Mike Axiak
1
这可能就是关键:在函数式语言中,你必须朝着你需要的方向思考,然后再从那里推导出你所拥有的(与面向对象编程相反)。 - Alexey
如果expr1expr2是命名函数,那么(filter #(expr1) (map #(expr2) expr3))将被转换为(filter expr1 (map expr2 expr3))。Clojure旨在消除一些老式Lisp中典型的多余括号。 - Brian Carper
显示剩余2条评论
4个回答

10

我一开始对Lisp的看法也是和你一样,所以我能理解你的痛苦 :-)

不过好消息是,随着时间的推移和经常使用,你会发现你可能开始喜欢前缀符号表示法。实际上,除了数学表达式外,我现在更喜欢它比中缀样式。

喜欢前缀符号表示法的原因:

  • 与函数的一致性 - 大多数语言使用中缀(数学运算符)和前缀(函数调用)符号的混合。在Lisps中它是完全一致的,如果你认为数学运算符就是函数,这种一致性具有某种优雅之处
  • - 如果函数调用总是位于第一个位置,则宏会变得更加明智。
  • 可变参数 - 对于几乎所有运算符,拥有可变数量的参数非常方便。 (+ 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)))

->->>的不便之处在于你有两个。一些函数(consmapapply)将复合对象作为最后一个参数,而其他函数(conjupdate-in)将其作为第一个参数,你不能用单个->->>将它们链接在一起。附言:在纯面向对象编程语言如Ruby和Smalltalk中,中缀表示法是完全一致的:1.+(2), [1].zip([2]),并且复合对象始终是点号前的参数。 - Alexey
3
@Alexey: ->->> 的不同参数顺序有其原因,与序列抽象和集合之间的区别有关。这可能会让人感到困惑,因为集合经常被视为序列。我经常参考 Rich Hickey 的解释 - http://groups.google.com/group/clojure/msg/a8866d34b601ff43 - Alex Stoddard
2
如果你真的需要的话,swiss-arrows库提供了钻石魔杖宏:https://dev59.com/_2kw5IYBdhLWcg3wMHqm - Alex Stoddard
我认为真正的原因是:他需要像conj这样的函数具有可变数量的参数,因此他无法将主要参数放在最后。虽然如果需要,它会像-<>一样表现得很好。 - Alexey

4

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

如果我们正在阅读这个,我们需要知道什么?我们正在对“people”的一些属性的某个子集调用do_something。由于第一个和最后一个之间有太多的距离,所以这段代码很难阅读,到达最后时我们会忘记我们在看什么。
在Ruby中,do_something(或者产生我们最终结果的任何内容)在行末丢失了,因此很难确定我们对people做了什么。在Clojure的情况下,很明显我们正在执行do-something,但是很难确定我们正在执行什么操作,除非我们读完整个代码块。
任何比这个简单示例更复杂的代码都将变得相当痛苦。如果您所有的代码都像这样,那么扫描所有这些代码行会让您的脖子累得发疼。
我更喜欢这样的东西:
(let [people (fetch-data :people)
      adults (filter #(> (:age %) 19) people)
      ids    (map :id adults)]
  (do-something ids))

现在很明显:我先从“人”开始,四处闲逛,然后对他们进行一些“做某事”的操作。
你也许可以逃脱这个问题:
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))

3

我刚开始使用Lisp时,确实遇到了同样的障碍,这让我很烦恼,直到我明白了它使代码更简单、更清晰的方式,一旦我理解了它的好处,烦恼就消失了

initial + scale + offset

成为

(+ initial scale offset)

然后尝试使用(+)前缀符号表示法,允许函数指定其自身的标识值。

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

这里还有更多的例子,我的观点并不是为了捍卫前缀表示法。我只是希望传达当积极面显现时,学习曲线会变得平缓(情感上)。

当然,当你开始编写宏时,前缀表示法成为必备而不仅仅是方便。


对于你问题的第二部分,如果"首个线程"和"最后一个线程"宏能使代码更清晰,它们就是惯用语 :) 它们更常用于函数调用而不是纯数学运算,但是如果它们使等式更易懂,没有人会责怪你使用它们。


附加说明:(.. object object2 object3) -> object().object2().object3();

(doto my-object
   (setX 4)
   (sety 5)`

Arthur,你认为前缀表示法编写代码与使用点号进行中缀调用的.methods一样方便,还是说它确实不太方便但有其他优势? - Alexey
1
一旦我发现了 .. 宏,我就觉得 Java 中的中缀符号表示法非常笨拙,阅读和输入都很困难。(这只是我的个人看法) - Arthur Ulfeldt
1
@ArthurUlfeldt,您能否详细说明“当您开始编写宏时,中缀符号成为必备而不是方便”?或者您是想说前缀吗? - ivant

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