株式会社クリアコード > ククログ

ククログ


Effective Ruby

2015年1月に翔泳社から「Effective Ruby」という中級者・上級者向けのRubyの本が出版されました。

Effective Ruby
Peter J. Jones/arton/長尾 高弘
翔泳社
¥ 3,456

内容

とても主張が強い本です。そのため、何でも真に受ける人にはオススメできません。(初級者とか。)他の人の主張を聞いて、自分で咀嚼してよい悪いを判断できるようになってから読むべきです。

「項目5 実行時の警告に注意しよう」で警告を有効にしようと書いているなど、よいことを書いているところはあるのですが、ここでは、著者の主張が気に入らなかったところを紹介します。よいところは自分で読んで確認してください。

「項目2 オブジェクトを扱うときにはnilかもしれないことを忘れないようにしよう」で、「適切なら」と断りをいれていますが、to_sto_iなどでnilを強制的に型変換しようと書いていることが気に入りません。不正なオブジェクトが見つかる時期が遅くなるので気に入りません。なぜか空文字がある、0がある、どうして?という状態になりやすくなります。

「項目3 Rubyの暗号めいたPerl風機能を避けよう」で次のようにifの条件式のところで代入しているのが気に入りません。しかも、一般的であると書いています。代入(=)と比較(==)は紛らわしいので条件式で代入するのはやめて欲しいです。

1
2
3
4
5
6
7
def extract_error (message)
  if m = message.match(/^ERROR:\s+(.+)$/)
    m[1]
  else
    "no error"
  end
end

「項目19 reduceを使ってコレクションを畳み込む方法を身に付けよう」で次のようなコードを書いています。selectよりもreduceの方がよいそうです。理由は効率的だからだそうです。

1
2
3
4
5
6
users.select {|u| u.age >= 21}.map(&:name)

users.reduce([]) do |names, user|
  names << user.name if user.age >= 21
  names
end

たしかに効率的ですが、そうまでしてreduceを使う必要はあるのでしょうか。この例ならeachで十分です。

1
2
3
4
5
names = []
users.each do |user|
  names << user.name if user.age >= 21
end
names

reduceのブロックの中で破壊的な操作をするのが気に入らないのです。次のように使うならいいです。「あるルールに従ってデータ構造内の要素数を縮小していき、最後に残った値を返す」というreduceの発想に沿った使い方だからです。ただ、効率はよくありません。

1
2
3
4
5
6
7
users.reduce([]) do |names, user|
  if user.age >= 21
    names + [user.name]
  else
    names
  end
end

「項目33 エイリアスチェイニングで書き換えたメソッドを呼び出そう」のところは気に入らないというより著者の勘違いですが、演算子でも動きます。send(:"*_without_logging")は動くのです。

あと、些細なことですが、コーディングスタイルは気に入りませんでした。

まとめ

主張の強い中級者・上級者向けのRubyの本、Effective Rubyを紹介しました。他の人の主張の良し悪しを自分で判断できるようになったら読んでみてはいかがでしょうか。ここでは気に入らないところを紹介しましたが、まっとうなことを書いているところもいろいろあります。自分で考えながら読んでみてください。

Effective Rubyを読んだ後はRubyのテスティングフレームワークの歴史(2014年版)を読みたくなることでしょう。

そういえば、最近のRubyの本はGCのことを説明することが当たり前になってきたのでしょうか*1。この本でもGCのことを説明していました。

*1 将来、RubyがJVMのように自分でいろいろチューニングしないといけなくなったらどうしましょう。。。

2015-02-04

mrubyのVMのデバッグ方法

全文検索エンジンのGroongaでmrubyを使っているのですが、たまにCRubyと異なる挙動に遭遇することがあります。このようなときはmrubyに問題があります。特定のメソッドの挙動がおかしいときはmrubyのライブラリーの実装に問題があります。構文の使い方で挙動がおかしいときはmrubyのVMに問題があります。

mrubyの問題に遭遇するときは、なぜかmrubyのVMに問題があるケースばかり*1なのですが、久しぶりにmrubyのVMにある問題を直そうとしたらデバッグの仕方を忘れていました。忘れても後から思い出せるようにメモしておきます。

詳細は後述しますが、基本的な流れは次の通りです。

  1. 問題を再現するスクリプトを作る。
  2. mrubyをデバッグビルドする。
  3. mrbc -vでスクリプトの構文木(?)を確認する。
  4. GDB上でmruby XXX.rbを走らる。
  5. src/vm.cの中で問題のラベルがあるところにブレークポイントを設定する。
  6. 問題があるデータを見つけて直す。

問題を再現するスクリプトを作る

すでに問題を見つけているはずなので、それを独立させ、できるだけ小さいコードで再現するようにします。このあいだ遭遇した問題*2は次のスクリプトで再現しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A
  def x
    yield
  ensure
    y
  end

  def y
  end
end

# Work
A.new.x do
end

# Not work
# trace:
#   [2] /tmp/a.rb:5:in A.x
#   [0] /tmp/a.rb:15
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
A.new.x do
  break
end

# trace:
#   [2] /tmp/a.rb:5:in A.call
#   [0] /tmp/a.rb:19
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
lambda do
  A.new.x do
    return
  end
end.call

yieldで呼ばれたブロックの中からbreakまたはreturnすると、ensureの中のselfがおかしくなるという問題です。

特定のメソッドがおかしな結果を返すという類のものではないので、mruby本体の問題である可能性が高いです。

問題を再現するスクリプトはできたので次へ進みます。

mrubyをデバッグビルドする

mruby本体の問題である場合、mrubyをGDB上で実行し、内部状態を確認しながら原因を調べる必要があります。GDBで実行する場合、-g3オプション付きでビルドするとマクロも展開できて便利です。

以前は手動でtasks/toolchains/gcc.rakeを変更して-g3-O0を追加していましたが、面倒になったのでpull requestを送って取り込んでもらいました。今はbuild_config.rbenable_debugと書いておくと勝手に-g3-O0オプションをつけてくれます。

なお、デフォルトのbuild_config.rbにはenable_debugが入っているので特に変更せずにビルドするだけでよいです。

mrbc -vでスクリプトの構文木(?)を確認

GDBでどのあたりを確認すればよいかのアタリをつけるために、mrubyのVMがどのようにスクリプトを実行するかを確認します。

mrbc -v(またはmruby -v)でRubyスクリプトを実行すると構文木(?)*3が出力されます。

例えば、次のRubyスクリプトを入力にするとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A
  def x
    yield
  ensure
    y
  end

  def y
  end
end

A.new.x do
  break
end

これをmrbc -vに渡すと次のような出力になります。

mruby 1.1.0 (2014-11-19) 
00001 NODE_SCOPE:
00001   NODE_BEGIN:
00001     NODE_CLASS:
00010       :A
00010       body:
00002         NODE_BEGIN:
00002           NODE_DEF:
00006             x
00006             NODE_ENSURE:
00006               body:
00003                 NODE_BEGIN:
00003                   NODE_YIELD:
00006               ensure:
00005                 NODE_BEGIN:
00005                   NODE_CALL:
00005                     NODE_SELF
00005                     method='y' (220)
00008           NODE_DEF:
00009             y
00009             NODE_BEGIN:
00012     NODE_CALL:
00012       NODE_CALL:
00012         NODE_CONST A
00012         method='new' (11)
00012       method='x' (219)
00014       args:
00014       block:
00012         NODE_BLOCK:
00014           body:
00013             NODE_BEGIN:
00013               NODE_BREAK:
irep 0xe7b640 nregs=3 nlocals=1 pools=0 syms=3 reps=2
file: /tmp/a.rb
    1 000 OP_LOADNIL	R1		
    1 001 OP_LOADNIL	R2		
    1 002 OP_CLASS	R1	:A
    1 003 OP_EXEC	R1	I(+1)
   12 004 OP_GETCONST	R1	:A
   12 005 OP_SEND	R1	:new	0
   12 006 OP_LAMBDA	R2	I(+2)	2
   12 007 OP_SENDB	R1	:x	0
   12 008 OP_STOP

irep 0xe7bd70 nregs=3 nlocals=1 pools=0 syms=2 reps=2
file: /tmp/a.rb
    2 000 OP_TCLASS	R1		
    2 001 OP_LAMBDA	R2	I(+1)	1
    2 002 OP_METHOD	R1	:x
    8 003 OP_TCLASS	R1		
    8 004 OP_LAMBDA	R2	I(+2)	1
    8 005 OP_METHOD	R1	:y
    8 006 OP_LOADSYM	R1	:y
    8 007 OP_RETURN	R0	return

irep 0xe962d0 nregs=3 nlocals=2 pools=0 syms=1 reps=1
file: /tmp/a.rb
    2 000 OP_ENTER	0:0:0:0:0:0:0
    6 001 OP_EPUSH	:I(+1)
    3 002 OP_BLKPUSH	R2	0:0:0:0
    3 003 OP_SEND	R2	:call	0
    3 004 OP_EPOP	1
    3 005 OP_RETURN	R2	return

irep 0xe9c0f0 nregs=3 nlocals=1 pools=0 syms=1 reps=0
file: /tmp/a.rb
    5 000 OP_LOADSELF	R1		
    5 001 OP_SEND	R1	:y	0
    5 002 OP_RETURN	R0	return

irep 0xe9d4d0 nregs=3 nlocals=2 pools=0 syms=0 reps=0
file: /tmp/a.rb
    8 000 OP_ENTER	0:0:0:0:0:0:0
    9 001 OP_LOADNIL	R2		
    9 002 OP_RETURN	R2	return

irep 0xe9d600 nregs=2 nlocals=1 pools=0 syms=0 reps=0
file: /tmp/a.rb
   13 000 OP_RETURN	R1	break

まずは以下のツリー状の方を確認します。

00001 NODE_SCOPE:
...

問題が発生するときに実行するコードは以下でした。

1
2
3
A.new.x do
  break
end

これに対応するサブツリーを見つけます。breakなどコードの中にあるキーワードに注目して見つけます。見つけやすいようにコードの中にリテラルを入れてもいいでしょう。

今回の場合は以下のサブツリーが対応します。

00012     NODE_CALL:
00012       NODE_CALL:
00012         NODE_CONST A
00012         method='new' (11)
00012       method='x' (219)
00014       args:
00014       block:
00012         NODE_BLOCK:
00014           body:
00013             NODE_BEGIN:
00013               NODE_BREAK:

今回はbreakが怪しいので次の部分に注目します。

00013             NODE_BEGIN:
00013               NODE_BREAK:

特に左側の13の部分に注目します。これに対応する数値を、ツリーの後に出力されている次の部分の中から見つけます。

irep 0xe7b640 nregs=3 nlocals=1 pools=0 syms=3 reps=2
...

今回の場合は次の箇所です。

irep 0xe9d600 nregs=2 nlocals=1 pools=0 syms=0 reps=0
file: /tmp/a.rb
   13 000 OP_RETURN	R1	break

ここにあるOP_XXXに注目します。今回の場合はOP_RETURNです。これがわかったら次に進みます。

GDB上でmruby XXX.rbを走らる

GDB上で問題のあるスクリプトを実行します。

% gdb --args bin/mruby XXX.rb
...
(gdb)

プログラムを実行する前にブレークポイントを設定します。

src/vm.cの中で問題のラベルがあるところにブレークポイントを設定する

どこにブレークポイントを設定すればよいかというと、src/vm.cの中のOP_XXXがある場所です。

今回だと次の箇所です。

1440      CASE(OP_RETURN) {
1441        /* A B     return R(A) (B=normal,in-block return/break) */

GDB上でブレークポイントを設定します。

(gdb) b vm.c:1440

ブレークポイントを設定したら実行します。

(gdb) r

ブレークポイントで止まるので後は問題があるデータを見つけて、正しいデータになるようにします。

なお、GDB上でC-x C-aを実行するとソースコード表示あり・なしを切り替えられるので必要に応じて使いわけます。出力したい内容が多い場合はソースコード表示をなしにして表示領域を広くとり、ステップ実行をする場合はソースコードを表示しながら流れを追いやすくします。

問題があるデータを見つけて直す

問題箇所の付近をステップ実行できるようになったので、mrb_p()でデータを確認しながら問題を見つけます。

(gdb) call mrb_p(mrb, v)
Array

問題を見つけるやり方の1つは、正常なケースのスクリプトも用意して動作を比較する方法です。期待する結果がわかるので問題のあるデータを見つけやすくなります。

別のやり方は、再現スクリプトからわかった問題の値を探す方法です。今回の場合は次のようなエラーメッセージがでているため、selfがおかしくなっていることがわかります。

1
2
3
4
5
6
7
# trace:
#   [2] /tmp/a.rb:5:in A.x
#   [0] /tmp/a.rb:15
# /tmp/a.rb:5: undefined method 'y' for main (NoMethodError)
A.new.x do
  break
end

問題があるデータを見つけたら後は直すだけです。

まとめ

久しぶりにmrubyを直そうとするとやり方を忘れていたので後で思い出せるようにやり方をまとめました。

必要ならmruby本体の問題も修正できるので、mrubyをアプリケーションに組み込みたいので技術支援して欲しい、という場合はご相談ください。

*1 普通にRubyで書いているつもりなのですが、特殊な書き方をしているのかもしれません。

*2 pull requestはマージ済みなのでmasterでは直っています。

*3 なんと呼ぶのがよいのでしょうか。

タグ: Ruby
2015-02-17

クリアなコードの作り方: 専用の機能を使う

「やりたい処理もできる」機能ではなく「やりたい処理用」の機能を使うことで、書いた人の意図が伝わるコードになるという話です。「動く」コードは書けるけど「意図が伝わる」コードはまだ書けない、という初級者向けの話です。

「動く」だけだとどんな機能を使って実装しても同じですが、「意図が伝わる」という観点では使う機能によって違いがあります。これは読む人が期待することとコードが意図することが異なるからです。

繰り返し機能が必要なケースを考えます。ここでは次のように1から5まで出力したいとします。

% ruby output-numbers.rb
1
2
3
4
5

Rubyには繰り返し機能を提供するeachというメソッド*1があります。これを使って実装すると「繰り返して処理をしたい」という「意図」が伝わるコードになります。

1
2
3
4
numbers = [1, 2, 3, 4, 5]
numbers.each do |number|
  puts(number)
end

Rubyには「繰り返し」機能と「それぞれの繰り返しの結果を集める」機能を提供するmapというメソッド*2もあります。mapにも「繰り返し」機能があるので、mapを使っても「動く」コードを書けます。

1
2
3
4
numbers = [1, 2, 3, 4, 5]
numbers.map do |number|
  puts(number)
end

これら2つのコードは同じ「動き」になりますが、「意図が伝わる」という点では違います。

mapには「それぞれの繰り返しの結果を集める」機能があるので、プログラムを読む人は「mapで集めた結果をどう使うのだろう」と考えながら読みます。しかし、このプログラムではmapの「それぞれの繰り返しの結果を集める」機能を使っていないので、次のコードのようにmapの結果を代入していません。

1
2
3
converted_numbers = numbers.map do |number|
  puts(number)
end

今回のコードは単に結果を捨てています。

mapのことを知っている注意深く読む人*3ならここで次のように考えます。

「それぞれの繰り返しの結果を集める」機能を提供するmapの結果を使っていないということは…これは代入忘れのバグじゃないか?

しかし、コード全体を読んでみるとmapの「それぞれの繰り返しの結果を集める」機能は使っていなくて、単に「繰り返し」機能だけを使っていることがわかります。そしてこう思います。

なんだ、「それぞれの繰り返しの結果を集める」機能を使っていないのか。じゃあ、バグじゃないか。まぎらわしいな。。。

mapではなく「繰り返し」機能だけを提供するeachを使っていれば、読む人はまぎらわしく思わずに書いた人が何をしたかったかを理解できます。このようなコードが「意図が伝わる」コードです。

「意図が伝わる」コードは読む人が理解する時間が短くなりますし、間違って理解されにくくなります。これはコードの修正や改良に役に立ちます。多くのコードは一度書いたら終わりではなく、動くようになったあとにメンテナンスされます。そのため、修正や改良に役立つことは重要です。

「動く」コードを書けるようになったら「意図が伝わる」コードを目指してください。

まとめ

eachmapを例にして「動く」だけのコードと「意図が伝わる」コードの違いを説明しました。ちなみに、eachでよいところにmapを使っているコードはわりとよく見るコードです。いつもの癖でmapを使ったり、最初は「それぞれの繰り返しの結果を集める」機能が必要だったけど途中で必要なくなったのにmapを使い続けてしまっている、ということなのかもしれません。

「動く」コードを書けるようになったら、「動く」だけではなく「意図が伝わる」コードを目指してください。「意図が伝わる」コードは改良や修正などメンテナンスがしやすいからです。「とりあえず動くもの」の次を目指すときに参考にしてください。

あわせて読みたい:

お知らせ:

*1 JavaScriptならArray.prototype.forEach

*2 JavaScriptならArray.prototype.map

*3 まわりにいる「意図が伝わる」コードを書く人を思い浮かべてください。

2015-02-23

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|