Ruby Ruby 里为什么要有 unless?

hegwin · 2018年03月27日 · 最后由 jasl 回复于 2018年03月27日 · 2790 次阅读

本文属于“深夜睡不着,那就开个脑洞吧”系列。

背景

从我开始接触编程开始,编程语言的控制结构if...else...便是所有语言的标配。从我最开始学的 VB,到后来的 C,Java,Lua,JavaScript 皆是如此。而当我开始学 Ruby 时,我发现 Ruby 除了if,竟然还有一个在当时的我看来是代替if notunless,这真是非常神奇了。再到后来接触到 Python 和 Perl,Perl 是第二个我所知的有unless结构的语言。

虽然我一开始也有点惊讶unless的存在,但是写习惯了之后,觉得这货在很多时候的表现也挺好的,在某些时候能让代码的可读性更强,更像自然语言;另外,大家在也总结出了一些 unless 的风格指南:

在单行语句的时候喜爱使用 if/unless 修饰符。另一个好的选择就是使 and/or 来做流程控制。

# bad
if some_condition
  do_something
end

# good
do_something if some_condition

# another good option
some_condition and do_something

以及:

永远不要使用 unless 和 else 组合。将它们改写成肯定条件。

# bad
unless success?
  puts 'failure'
else
  puts 'success'
end

# good
if success?
  puts 'success'
else
  puts 'failure'
end

探寻

每天都能在 Ruby 代码看见unless,本来应该对他习以为常,但我的脑洞就一直很大啊,有时候就会问,为啥要有unless呢?

猜测一:受其他语言启发

根据 wiki 介绍,Ruby 的设计受到了 Perl, Smalltalk, Eiffel, Ada, and Lisp 的影响:

According to the creator, Ruby was influenced by Perl, Smalltalk, Eiffel, Ada, and Lisp.

在这几门语言中,Perl 上面已经提到了,是有unless控制结构的。此外,我最近搜索了一下,发现 Common Lisp 竟然也是有unless表达式的。以下 Common List 代码是从百度贴吧复制过来的,写的一个神经网络,这是摘抄的一部分(贴吧真是代有人才出):

(defun weights-change-network-module()
  (unless (= (network-weight-change-rate-fn) 0)
    (Batch-adding-neurons *new-neurons-number* *new-nodex-number* *new-nodey-number* )
    (repeat-matching-nodes ));节点重新分配
  (dolist (obj *neurons-list* )
  (unless (= (weights-comprehensive-regulation obj ) 0)
    (modify-weights-module (neurons-name obj)(weights-comprehensive-regulation obj ))))

把这段代码翻译成 Ruby 风格,大概就是这样:

def weights_change_network_module()
  unless network_weight_change_rate_fn == 0
    Batch_adding_neurons *new_neurons_number* *new_nodex_number* *new_nodey_number*
    repeat_matching_nodes
  end #节点重新分配
  dolist (obj *neurons_list*)
  unless weights_comprehensive_regulation(obj) == 0
    modify_weights_module (neurons_name obj)(weights_comprehensive_regulation obj )
  end
end

可把自己牛逼坏了,居然看得懂 Lisp,我得叉会腰 好吧,其实并没有,我完全不懂那些 *new_neurons_number*是什么东西

所以,我第一个感觉应该是 Matz 应该是受到 Lisp 或者 Perl 的启发,在设计 Ruby 时,加入了unless

猜测二:if 比较反人类

我一直觉得 if condition; do something; else; do something else; end是一个非常符合人类思维的表达,直到 17 年 8 月,一个同学给我安利了一个游戏叫 HRM,界面如下:

这其实是一个编程小游戏:右边是程序代码,在这一关能使用的命令非常少,就只有“输入”,“输出”,“加法”, “减法”和三个 jump,左边的方块 7, 2, -3, -8...是代表给程序输入,中间地上的格子,0,1,2 是三个存储器,相当于变量。这道题的目标是每次取两个输入值,然后输出二者中比较大的那个,以此循环。

这时候,我才忽然意识到,我们所写的:

if a - b > 0
  p a
else
  p b
end

在电脑看来,应该是这副模样:

00 if a -b > 0
01 goto 04
02 print b
03 goto 05
04 print a
05 end

去掉 goto 语句,就会有一种这样的感觉,和我们直接感觉有些颠倒:

if a - b > 0
print b
print a

那么,Ruby 真的时这个游戏里的这样去实现的吗?我也不知道,所以测试了下:

#!/usr/bin/env ruby
code = <<CODE
if a - b > 0
  p a
else
  p b
end
CODE

puts RubyVM::InstructionSequence.compile(code).disasm

输出的 YARV 指令如下:

== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace            1                                               (   1)
0002 putself          
0003 opt_send_without_block <callinfo!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0006 putself          
0007 opt_send_without_block <callinfo!mid:b, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0010 opt_minus        <callinfo!mid:-, argc:1, ARGS_SIMPLE>, <callcache>
0013 putobject_OP_INT2FIX_O_0_C_ 
0014 opt_gt           <callinfo!mid:>, argc:1, ARGS_SIMPLE>, <callcache>
0017 branchunless     30
0019 trace            1                                               (   2)
0021 putself          
0022 putself          
0023 opt_send_without_block <callinfo!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0026 opt_send_without_block <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0029 leave                                                            (   1)
0030 trace            1                                               (   4)
0032 putself          
0033 putself          
0034 opt_send_without_block <callinfo!mid:b, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0037 opt_send_without_block <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0040 leave           

Hmmm,虽然有点乱,但是连蒙带猜应该能明白是怎么回事。

0002 putself - 把当前的 self 作为方法的接受者;

0003 opt_send_without_block <callinfo!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache> ,发出名为a的消息,0 个参数,不带块;

0010 opt_minus <callinfo!mid:-, argc:1, ARGS_SIMPLE>, <callcache> 发送名为 - 的消息,1 个参数,即做减法,opt_plus 或者 opt_minus 是优化后的专有 YARV 指令;

0014 opt_gt <callinfo!mid:>, argc:1, ARGS_SIMPLE>, <callcache> 比较大小;

然后重点来了!

0017 branchunless 30,否则跳到第 30 行,这里有个 unless!

紧跟在这下面的指令是打印 a 的值:

0023 opt_send_without_block <callinfo!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0026 opt_send_without_block <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>, <callcache>

虽然有个unless在这里横着,可是“条件”和“结果”的顺序居然因为负负得正,又和我们人类写代码的顺序一样了,真是不可思议;不过 Ruby 是在 1.9 版本才有了 YARV...

嘛,没有结论,只是我该关上脑洞去睡觉了。

Ruby 为啥要有 unless 呢?其实同样可以严肃的问为啥英语里有 unless 呢?

但是关于如何使用 unless 是个很好的话题,这里有讨论: https://github.com/bbatsov/ruby-style-guide/issues/329。该讨论同时延伸了 not!,以及 &&/||and/or 等的用法区别。

比较认同这个说法

if unless looks weird, try to use a simple if
don't mix and/not/or with &&/!/||
only use and/not/or for logical comparisons

这里用了个 weird,所以除了团队有代码风格约束外,更多的和你的舒适感有关。更具体的示例可以看这里

另外,个人认为在不给具体代码示例的时候如下的说法可能对新人会产生误解的,因为不写成 modifier 的形式不一定就不好,有时恰恰相反:

# bad
if some_condition
  do_something
end

# good
do_something if some_condition

最后,Perl 和 Ruby 里都还有个 until 的关键词,楼主可以继续开一下脑洞 : )

ruby 就是写法太多,想法自然多

这段翻译错了:

  dolist (obj *neurons_list*)
  unless weights_comprehensive_regulation(obj) == 0
    modify_weights_module (neurons_name obj)(weights_comprehensive_regulation obj )
  end

# =>
neurons_list.each do |obj|
  unless obj.weights_comprehensive_regulation == 0
    modify_weights(obj.neurons_name, obj.weights_comprehensive_regulation)
  end
end

无聊一下:)

Lisp 下还有 when 不需要 else 的 if

jasl 回复

我一直觉得 gaurd 语句是这样的,但是没有 unless 用 if not 也没问题啊… 😂

def can_work?
  return false if age < 18 # 这是个guard语句
  # 其他判断
  # ...
end
gingerhot 回复

于是…我去查了下“英语里为什么会有 unless”,以下来自 Century Dictionary…

mid-15c., earlier onlesse, from (not) on lesse (than) "(not) on a less compelling condition (than);". The first syllable originally on, but the negative connotation and the lack of stress changed it to un-. "Except could once be used as a synonym for unless, but the words have now drawn entirely apart"

看起来是从 on lesse 发展过来的

Ruby 的 until,这个我昨天做了个题,我还用震到 until 了,感觉一个好处是,until 的表达式里的标量可以不用提前初始化,可以在循环体里才初始化

coderek 回复

哈哈哈 是的

gihnius 回复

原来如此!这两个星号是这样的意思,了解了~~

gihnius 回复

我忽然觉得 Lisp 挺神的,想去了解一下

hegwin 回复

你理解成语法糖也没问题,这种风格的另一种名字叫 guard clause

需要 登录 后方可回复, 如果你还没有账号请 注册新账号