Ruby の落とし穴


Matzにっき(2006-12 -20) 経由で tech addict - ruby gotchas and caveats より。Smalltalk に関連していそうなこと、そうでなくとも新しく知ったところを抜粋して JavaPython の勉強を兼ねてメモ。


暗黙の型強制

1. strings aren't auto converted into numbers or strings
8. 1/2 != 0.5 unless require 'mathn' (this is the default in many languages anyway, sometimes integer math is your friend). With mathn 1/2 becomes a different type (Rational) than 0.5 (Float)

Java
"1" + 1   //=> "11"
1 + "1"   //=> "11"
"a" + 1   //=> "a1"
1 / 2   //=> 0
Python
'1' + 1     #=> TypeError: cannot concatenate 'str' and 'int' objects
'a' + '1'   #=> 'a1'
1 / 2   #=> 0
Ruby
'1' + 1     #=> TypeError: can't convert Fixnum into String
'a' + '1'   #=> 'a1'
1 / 2   #=> 0
require 'mathn'
1 / 2         #=> 1/2
(1/2).class   #=> Rational
VisualWorksSmalltalk
'1' + 1     "=> Error: Message not understood: #+ "
1 + '1'     "=> Error: Message not understood: #sumFromInteger: "
'a' , '1'   "=> 'a1' "
1 / 2         "=> (1/2) "
(1/2) class   "=> Fraction "
SqueakSmalltalk
'1' + 1     "=> 2 "
1 + '1'     "=> 2 "
'1' + '1'   "=> 2 "
'a' + '1'   "=> 1 "
'a' , '1'   "=> 'a1' "
1 / 2         "=> (1/2) "
(1/2) class   "=> Fraction "


まず、Smalltak では文字列(や配列などの順序付きコレクション)どうしの結合には "+" ではなく ","(カンマ)を使うので、「'文字列' + 数値」という式に対して期待することについては、他の言語とは少々事情が異なります。VisualWorks などの“正統派”の Smalltalk 処理系では '1' + 1 は(単純に文字列がメソッド #+ を起動できない旨の)エラーになりますが、Squeak では 1 + 1 を期待しているものとして 2 を返してきます。


Integer >> #/ が分数を返すことについては、1970 年代初頭、Smalltalk の演算において型強制が導入された経緯に触れた squeak-dev への投稿が歴史や経緯が好きな自分には興味深かったです。

また、同じく分数にからめては、ダン・インガルスの こんな逸話も(ただし内容は本件とは関係ない、動的なシステムの落とし穴…といった話ですが)。かいつまむと、まだ Fraction クラス(Ruby でいう Rational クラス)が Smalltalk になかった 1970 年代、ある学生がこれを不用意に導入したところ Alto(この文脈では暫定ダイナブック)の GUI がうまく動かなくなり、原因を調べたら本来整数でなければならない BitBlt の低レベルの原始メソッドのパラメータに分数が紛れ込み、コールに失敗していた…というような話です。


条件分岐などにおける真偽の判定と論理演算の優先度

2. false and nil are the only valid false values, anything else is true
3. && and || are the high priority AND/OR constructs, and and or are low priority versions

Java
if (null) {} else { System.out.println(false); }   // error
if (0) {} else { System.out.println( false ); }    // error
Boolean x; x = true & false; x;    //=> false
Boolean x; x = true && false; x;   //=> false
Python
True if None else False   #=> False
True if 0 else False      #=> False
True if "" else False     #=> False
True if [] else False     #=> False
x = True and False; x   #=> False
Ruby
nil ? true : false   #=> false
0 ? true : false     #=> true
"" ? true : false    #=> true
[] ? true : false    #=> true
x = true and false; x   #=> true
x = true && false; x    #=> false
Smalltalk
nil ifTrue: [true] ifFalse: [false]   "=> NonBooleanReceiver: proceed for truth."
0 ifTrue: [true] ifFalse: [false]     "=> NonBooleanReceiver: proceed for truth."
|x| x := true & false. x        "=> false "
|x| x := true and: [false]. x   "=> false "


条件分岐などにおける真偽の判定について Smalltalk では、原則として true と false しか許しません。ただ、他のオブジェクトの場合でも、単にエラーにするのではなく、いったん仮想マシンがレシーバに「mustBeBoolean」を送信するので、これにより起動されるメソッド #mustBeBoolean をオーバーライドしておけば、任意のオブジェクトについて真としての振る舞いをさせられます。

優先度については、そもそも Smalltalk では変数への代入(:= または ←)の優先度が低いので、#& でも #and: でも結果は変わりません。落とし穴があるとすれば、#and:、#or: のパラメータは真偽値ではなく、真偽値を返すブロックである必要があるということでしょうか。


クラス変数の隠蔽とクラス変数を含むクラスメソッド定義時の注意

9. class variables and their inheritance semantics
10. singleton class modification for the class objects isn't good or really allowed
13. You might think that class_eval would provide access to the class's context and variables, apparently not so much.

Java
class B { public static String Var; }
class D extends B { public static String Var; }
B.Var = "B.Var";
System.out.println( B.Var );   //=> B.Var
System.out.println( D.Var );   //=> null
D.Var = "D.Var";
System.out.println( B.Var );   //=> B.Var
System.out.println( D.Var );   //=> D.Var
Python
class B: pass
class D(B): pass
B.Var = "B.Var"
print B.Var       #=> "B.Var"
print D.Var       #=> "B.Var"
D.Var = "D.Var"
print D.Var       #=> "D.Var"
print B.Var       #=> "B.Var"
Ruby
class B1
  def self.Var_B; @@Var end
  def self.Var_B=(x); @@Var=x end
end
class D1 < B1
  def self.Var_D; @@Var end
  def self.Var_D=(x); @@Var=x end
end
B1.Var_B="via Var_B"
p B1.Var_B             #=> "via Var_B"
p D1.Var_B             #=> "via Var_B"
D1.Var_D="via Var_D"
p D1.Var_B             #=> "via Var_D"-1.8
                       #=> "via Var_B"-1.9
p D1.Var_D             #=> "via Var_D"
class B2
  def self.Var_B; @@Var end
  def self.Var_B=(x); @@Var=x end
end
class D2 < B2
  def self.Var_D; @@Var end
  def self.Var_D=(x); @@Var=x end
end
D2.Var_D="via Var_D"
B2.Var_B="via Var_B"
p B2.Var_B             #=> "via Var_B"
p D2.Var_B             #=> "via Var_B"
p D2.Var_D             #=> "via Var_D"
D2.Var_D="via Var_D"
p D2.Var_B             #=> "via Var_B"-1.8/1.9
p D2.Var_D             #=> "via Var_D"
class << B2; def Var1; @@Var end end
p B2.Var1                   #=> uninitialized class variable @@Var in Object (NameError)
def B2.Var2; @@Var end
p B2.Var2                   #=> uninitialized class variable @@Var in Object (NameError)
B2.class_eval { p @@Var }   #=> uninitialized class variable @@Var in Object (NameError)


Smalltalk では、原則としてサブクラスでのクラス変数の再定義を許していないので Ruby が(1.8 で)抱えているような問題とは、通常の使用においては無縁のようです。

Object subclass: #B
   instanceVariableNames: ''
   classVariableNames: 'Var'
   poolDictionaries: ''
   category: 'Category-Name'
B subclass: #D
   instanceVariableNames: ''
   classVariableNames: 'Var'
   poolDictionaries: ''
   category: 'Category-Name'
=> Error: Var is already defined in B


ただ、これをエラーにするのは、あくまでコンパイラの判断であって、Smalltalk の言語仕様上、クラス変数の再定義が不可能…というわけではないようです。実際、下に示すように、とても簡単に試すことができました。比較的複雑な仕組みを持つインスタンス変数の再定義の場合と違って、きっちり隠蔽もされます。

Object subclass: #B
   instanceVariableNames: ''
   classVariableNames: 'Var'

B >> VarB
   ^ Var

B >> VarB: x
   Var := x
B subclass: #D
   instanceVariableNames: ''
   classVariableNames: 'VarD'

D classPool removeKey: #VarD; at: #Var put: nil

D >> VarD
   ^ Var

D >> VarD: x
   Var := x
B VarB: 'via VarB'
B VarB                   "=> 'via VarB' "
D VarB                   "=> 'via VarB' "
D VarD                   "=> nil "
D VarB: 'via VarB, too'
B VarB                   "=> 'via VarB, too' "
D VarB                   "=> 'via VarB, too' "
D VarD                   "=> nil "
D VarD: 'via VerD'
B VarB                   "=> 'via VarB, too' "
D VarB                   "=> 'via VarB, too' "
D VarD                   "=> 'via VerD' "

"=" で終わる名前を持つメソッドは引数で与えた値しか返せない

11. assignment methods always return the assigned value, not the return value of the assignment function

Ruby
class C
  def foo(x); return "foo()" end
  def foo=(x); return "foo=()" end
end
C.new.foo(:something)    #=> "foo()"
C.new.foo = :something   #=> :something

はじめて知りましたが、これはひどい仕様ですね。w せめてコンパイル時に「return を書いても無駄、無駄、無駄、無駄ッ…!!」程度の警告は出すべきでしょう。


ローカル変数がメソッド名などを隠蔽する

12. local variable scoping is tricky and can hide other variables and methods (this is slated for a change in ruby 2.0)

Ruby
class C
   def foo; "C#foo" end
   def bar1; foo = "local var foo in bar1"; foo end
   def bar2; foo = "local var foo in bar2"; foo() end
   def bar3; foo = "local var foo in bar3"; self.foo end
end
C.new.bar1   #=> "local var foo in bar1"
C.new.bar2   #=> "C#foo"
C.new.bar3   #=> "C#foo"


Ruby でメソッドのコールに際して括弧やレシーバの self を省略できる仕様の弊害。省略をしなければ問題はないもよう。