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

0md85ypi  于 2022-11-04  发布在  Ruby
关注(0)|答案(1)|浏览(161)

在Ruby 2.7和Ruby 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的时候,在他们的脚本运行正常后才注意到他们在空白处有一个随机的%,我把它推得更远一点,看看它能接受多少。

jqjz2hbq

jqjz2hbq1#

语法

百分比字符串型

这是一个接收消息%的 * 百分比字符串型 *。

  • 百分比字符串型 * 的格式为:
  • %字符
    • 开头分隔符 *
  • 字符串内容
    • 结束分隔符 *

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

%

(that是,%空间)
是一个 * 百分比字符串型 *,以空格作为分隔符,没有内容。即,它等效于""

操作员消息发送a % b

a % b

相当于

a.%(b)

即,将消息%发送到表达式a的求值结果,将表达式b的求值结果作为单个自变量传递。
也就是说

%  % b

(大致)相当于

"".%(b)

参数列表

那么,b是什么呢?它是%运算符后面的表达式(不要与 Percent String Literal%符号混淆)。
整个代码(大致)相当于:

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")

AST编号
您可以通过询问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 gem或它的同伴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"))))

语义

到目前为止,我们所讨论的都是 Syntax,即代码的语法结构。但它 * 意味着 * 什么呢?
方法String#%执行 String Formatting a la C的printf family of functions。但是,由于格式字符串(%消息的接收方)是空字符串,因此消息发送的结果也是空字符串,因为没有要格式化的内容。
如果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 * 不是 * 一种纯函数的、懒惰的、非严格的语言。虽然传递给%消息send的参数 * 与消息send的结果无关 *,但它们仍然被求值(因为Ruby是严格和渴望的),而且它们有副作用(因为Ruby不是纯功能性的),也就是说,它们重新赋值变量和改变tbr结果数组的副作用仍然会执行。
如果这段代码是用更像Ruby的风格编写的,变异和副作用更少,而不是使用函数转换,那么用空字符串任意替换结果会立即破坏它。这里没有效果的唯一原因是大量使用了副作用和变异。

相关问题