令人惊讶的有效 Ruby 语法:% 无处不在



在 Ruby 2.7 和 3.1 中,无论 % 符号是否存在,此脚本都会执行相同的操作:

def count(str)
state = :start
tbr = []
str.each_char do
%  %case state
when :start
tbr << 0
%  %state = :symbol
%  when :symbol
tbr << 1
%  % state = :start
%  end
end
tbr
end
p count("Foobar")

如何解析?您可以添加更多%或删除一些%,它仍然有效,但不能进行任何组合。我通过反复试验找到了这个例子。

我正在教某人 Ruby,只有在他们的脚本工作后才注意到他们的边距中有一个随机的百分比。我把它推得更远一点,看看它会接受多少。

语法

百分比字符串文本

这是接收消息%的百分比字符串文本

百分比字符串文本的格式为:

  • %角色
  • 打开分隔符
  • 字符串内容
  • 闭合分隔符

如果开分隔符<[({之一,则结束分隔符必须是相应的>])}。否则,开始分隔符可以是任意字符,并且结束分隔符必须是相同的字符。

所以

%  

(即%空间空间)

是一个百分比字符串文本,以空格作为分隔符,没有内容。 即它相当于"".

操作员消息发送a % b

a % b

相当于

a.%(b)

%将消息发送到表达式a的计算结果,将表达式b的计算结果作为单个参数传递。

这意味着

%  % b

(大致)相当于

"".%(b)

参数列表

那么,b呢?嗯,它是%运算符后面的表达式(不要与百分比字符串文字%符号混淆)。

整个代码(大致)等同于此:

def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
"".%(state = :symbol)
""when :symbol
tbr << 1
"".%(state = :start)
""end)
end
tbr
end
p count("Foobar")

阿斯特

你可以通过问Ruby来自己弄清楚这一点:

# ruby --dump=parsetree_with_comment test.rb
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################
# @ NODE_SCOPE (id: 62, line: 1, location: (1,0)-(17,17))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): (empty)
# +- nd_args (arguments):
# |   (null node)
[…]
#     |           |       +- nd_body (body):
#     |           |           @ NODE_OPCALL (id: 48, line: 5, location: (5,0)-(12,7))*
#     |           |           | # method invocation
#     |           |           | # format: [nd_recv] [nd_mid] [nd_args]
#     |           |           | # example: foo + bar
#     |           |           +- nd_mid (method id): :%
#     |           |           +- nd_recv (receiver):
#     |           |           |   @ NODE_STR (id: 12, line: 5, location: (5,0)-(5,3))
#     |           |           |   | # string literal
#     |           |           |   | # format: [nd_lit]
#     |           |           |   | # example: 'foo'
#     |           |           |   +- nd_lit (literal): ""
#     |           |           +- nd_args (arguments):
#     |           |               @ NODE_LIST (id: 47, line: 5, location: (5,4)-(12,7))
#     |           |               | # list constructor
#     |           |               | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
#     |           |               | # example: [1, 2, 3]
#     |           |               +- nd_alen (length): 1
#     |           |               +- nd_head (element):
#     |           |               |   @ NODE_CASE (id: 46, line: 5, location: (5,4)-(12,7))
#     |           |               |   | # case statement
#     |           |               |   | # format: case [nd_head]; [nd_body]; end
#     |           |               |   | # example: case x; when 1; foo; when 2; bar; else baz; end
#     |           |               |   +- nd_head (case expr):
#     |           |               |   |   @ NODE_DVAR (id: 13, line: 5, location: (5,9)-(5,14))
#     |           |               |   |   | # dynamic variable reference
#     |           |               |   |   | # format: [nd_vid](dvar)
#     |           |               |   |   | # example: 1.times { x = 1; x }
#     |           |               |   |   +- nd_vid (local variable): :state
[…]

这里有一些有趣的地方是(id: 12, line: 5, location: (5,0)-(5,3))处的节点,它是第一个字符串文字,(id: 48, line: 5, location: (5,0)-(12,7))是发送的第一个%消息:

#     |           |       +- nd_body (body):
#     |           |           @ NODE_OPCALL (id: 48, line: 5, location: (5,0)-(12,7))*
#     |           |           | # method invocation
#     |           |           | # format: [nd_recv] [nd_mid] [nd_args]
#     |           |           | # example: foo + bar
#     |           |           +- nd_mid (method id): :%
#     |           |           +- nd_recv (receiver):
#     |           |           |   @ NODE_STR (id: 12, line: 5, location: (5,0)-(5,3))
#     |           |           |   | # string literal
#     |           |           |   | # format: [nd_lit]
#     |           |           |   | # example: 'foo'
#     |           |           |   +- nd_lit (literal): ""

注意:这只是获取解析树的最简单方法,不幸的是,它包含许多内部细节,这些细节与弄清楚正在发生的事情并不真正相关。还有其他方法,例如parser宝石或其配套ast,可以产生更具可读性的结果:

# ruby-parse count.rb
(begin
(def :count
(args
(arg :str))
(begin
(lvasgn :state
(sym :start))
(lvasgn :tbr
(array))
(block
(send
(lvar :str) :each_char)
(args)
(send
(dstr) :%
(case
(lvar :state)
(when
(sym :start)
(begin
(send
(lvar :tbr) :<<
(int 0))
(send
(dstr) :%
(lvasgn :state
(sym :symbol)))
(dstr)))
(when
(sym :symbol)
(begin
(send
(lvar :tbr) :<<
(int 1))
(send
(dstr) :%
(lvasgn :state
(sym :start)))
(dstr))) nil)))
(lvar :tbr)))
(send nil :p
(send nil :count
(str "Foobar"))))

语义学

到目前为止,我们谈论的只是语法,即代码的语法结构。但这意味着什么?

该方法String#%执行 C 的printf系列函数的字符串格式化。但是,由于格式字符串(%消息的接收方)是空字符串,因此消息发送的结果也是空字符串,因为没有要格式化的内容。

如果 Ruby 是一种纯粹的函数式、懒惰、非严格的语言,那么结果将等价于这样:

def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
""
""when :symbol
tbr << 1
""
""end)
end
tbr
end
p count("Foobar")

这反过来又相当于这个

def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
tbr << 0
""
when :symbol
tbr << 1
""
end)
end
tbr
end
p count("Foobar")

相当于这个

def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start
""
when :symbol
""
end)
end
tbr
end
p count("Foobar")

相当于这个

def count(str)
state = :start
tbr = []
str.each_char do
"".%(case state
when :start, :symbol
""
end)
end
tbr
end
p count("Foobar")

相当于这个

def count(str)
state = :start
tbr = []
str.each_char do
""
end
tbr
end
p count("Foobar")

相当于这个

def count(str)
state = :start
tbr = []
tbr
end
p count("Foobar")

相当于这个

def count(str)
[]
end
p count("Foobar")

显然,这不是正在发生的事情,原因是Ruby是一种纯粹的功能,懒惰,非严格的语言。虽然传递给消息发送%的参数与消息发送的结果无关,但它们仍然被评估(因为 Ruby 是严格和急切的),并且它们有副作用(因为 Ruby 不是纯粹的功能),即它们重新分配变量和改变tbr结果数组的副作用仍然被执行。

如果这段代码是用更像 Ruby 的风格编写的,突变更少,副作用更少,而是使用函数转换,那么任意用空字符串替换结果会立即破坏它。这里没有效果的唯一原因是因为大量使用副作用和突变。

最新更新