ひがきの日記

2013-04-13

Ruby初級者向けレッスン 45回 ─例外─

第57回 Ruby/Rails勉強会@関西での初級者向けレッスンの資料を公開します。*1

f:id:mas-higa:20130416224751p:image:w360

エラーメッセージ

require 'open-uri'
open 'http://github.com/rubykansai/workshops/wiki'
.../open-uri.rb:223:in `open_loop': redirection forbidden: http://github.com/rubykansai/workshops/wiki -> https://github.com/rubykansai/workshops/wiki (RuntimeError)
        from .../open-uri.rb:149:in `open_uri'
        from .../open-uri.rb:688:in `open'
        from .../open-uri.rb:34:in `open'
        from ex.rb:2:in `<main>'

f:id:mas-higa:20130416224727p:image:w360

  • 「...(RuntimeError)」までが 1行
  • 後は「from ...」から行末までが 1行
  • 最初の行がエラーの出た場所。
    • 「in `open_loop'」は open_loop メソッドでエラーが出たという意味。
    • 「.../open-uri.rb:223」は open-uri.rb の 223行目でエラーが出たという意味。
      • えっ、でも open_loop なんて見覚えないよ!?
  • 最後の行が呼び出し元。
  • 下から 2行目「in `open'」は <main> から open メソッドが呼ばれたという意味。
  • 結局、英語のメッセージを読む必要がある ><
    • 「redirection forbidden」理由は分からんが http から https へのリダイレクトが禁止されてるらしい。
  • 最初の行の行末「(RuntimeError)」は RuntimeError 例外が発生したという意味。

エラーメッセージと例外

f:id:mas-higa:20130416224703p:image:w360

  • 例外が起きたら、なんらかの対処をしないと、プログラムが異常終了する。

いろいろな例外と、そのエラーメッセージを見て、例外に慣れよう。

例外いろいろ TypeError

"1" + 1

# ~> ex.rb:1:in `+': can't convert Fixnum into String (TypeError)
# ~> 	from ex.rb:1:in `<main>'

f:id:mas-higa:20130416224639p:image:w360

例外いろいろ NoMethodError

1.to_sym

# ~> ex.rb:1:in `<main>': undefined method `to_sym' for 1:Fixnum (NoMethodError)

f:id:mas-higa:20130416224610p:image:w360

例外いろいろ NameError

n.times{puts 'Ruby!'}

# ~> ex.rb:1:in `<main>': undefined local variable or method `n' for main:Object (NameError)

f:id:mas-higa:20130416224539p:image:w360

例外いろいろ NoMethodError (再び)

n = ARGV.first.to_i unless ARGV.empty?
n.times{puts 'Ruby!'}

# ~> ex.rb:2:in `<main>': undefined method `times' for nil:NilClass (NoMethodError)

f:id:mas-higa:20130416224513p:image:w360

  • Rubyには、他の言語のように NullPointerException のようなものはない。

例外いろいろ Errno::ENOENT

open('nothing.txt')

# ~> ex.rb:1:in `initialize': No such file or directory - nothing.txt (Errno::ENOENT)
# ~> 	from ex.rb:1:in `open'
# ~> 	from ex.rb:1:in `<main>'

f:id:mas-higa:20130416224446p:image:w360

  • 存在しないファイルを open しようとした。
  • 「in `initialize'」は File.open メソッドが Fileクラスのインスタンスを生成しようとしている。
  • Errno::Exxx の例外は OS の errno に対応した例外。

例外いろいろ SyntaxError

1 def even?(n)
2   if n % 2 == 0
3     true
4   else
5     false
6 end
7 
8 puts even?(0)
9 puts even?(1)

# ~> -:9: syntax error, unexpected end-of-input, expecting keyword_end

f:id:mas-higa:20130416224423p:image:w360

  • if-else-end の end を忘れている。
  • Ruby のパーサがスクリプトを最後まで読んだが、end が見つからなかった。
  • end を忘れたのは 5行目と 6行目の間だが、例外は最後で出るので注意。

Intelligence と Wisdom

雨が降ってきて…
  • Intelligence
    • 雨だ!
  • Wisdom
    • 傘を差そう
    • 雨宿りしよう

f:id:mas-higa:20130416224357p:image:w360

雨が降ってきたときに、

  • それが雨だと認識できる ─── Intelligence (知識)
  • 濡れないためには、傘を差すか、雨宿りしないと ─── Wisdom (知恵)

例外も同じで、

  • エラーを検知したものが例外を起こす (Intelligence)
  • 対処方法を知っているものが例外を捕捉する (Wisdom)

例えば、アプリケーションがファイルを読もうとして、

  • open できない!
  • read できない!

File ライブラリはエラーを検知できる。(Intelligence)

  • 既定値を使うから、読めなくてもよい。
  • 別のファイルから読みなおしたい。
  • etc...

これはアプリケーションの仕様。(Wisdom)
ライブラリには分かりようがない。

例外を捕捉する (コード例1)

files = %w[file.txt file1.txt file2.txt]

files.each do |fn|
  begin
    open(fn, 'w'){|f| f.puts "Ruby!!"}
    break
  rescue => ex
    $stderr.puts "#{ex} (#{ex.class})"
  end
end

f:id:mas-higa:20130416224328p:image:w360

やりたいこと

  • ファイルに "Ruby!!" と書き込みたい。
  • ファイル名は file.txt
    • もし書けなければ、代わりに file1.txt に書こう。
    • それでも書けなければ、file2.txt に書こう。
    • それでダメなら諦める。

コード解説

  • ファイル名 "file.txt", "file1.txt", "file2.txt" という Array を用意。
  • 各ファイル名に対して処理を繰り返す。─── files.each ...
    • begin - end で例外を捕捉する準備。
    • ファイルに "Ruby!!" を書き込む。─── open( ...
      • 例外が起きると、rescue節に処理が移る。
      • 変数 ex が例外オブジェクトを指す。
      • ここでは、標準エラー出力に例外情報を出力するだけ。
      • 最初の繰り返しに戻る。─── files.each ...
    • 次のファイル名で open & puts に再挑戦。
      • 例外が起きなければ break で繰り返しを脱出する。

やってみる

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
# file.txt はディレクトリ
# file1.txt は書き込み権なし

$ ruby retry.rb
Is a directory - file.txt (Errno::EISDIR)
Permission denied - file1.txt (Errno::EACCES)

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
-rw-r--r--  1 mas  staff   7  4 12 23:39 file2.txt
# file2.txt ができた!

$ cat file2.txt 
Ruby!!

例外を捕捉する

begin
  式1…
[rescue [型1[, 型2]…][=> 変数][then]
  式2…]…
[else
  式3…]
[ensure
  式4…]
end

f:id:mas-higa:20130416224258p:image:w360

  • [ ... ] 内は省略可能。
  • begin - end で例外を捕捉する準備。
  • 式1 が例外を捕捉したい処理 (複数書ける)
  • rescue節で例外を捕捉。(rescue は複数書ける)
    • 「型」に例外クラスを指定すると、特定の例外だけを捕捉できる。(型は複数書ける)
    • 式2 が例外対処のための処理 (複数書ける)
    • 発生した例外に対応する rescue節がなければ、捕捉できない。
  • ensure節は begin - end のブロックを抜ける際の後始末。
    • 例外が起きても、起きなくても実行される。
    • 式4 が後始末の処理 (複数書ける)
  • else節は、上記 式1 で例外が起きなかった際に実行される。
    • rescue節で指定しなかった例外を捕捉する処理ではない。
    • 式3 で起きた例外は、このブロックでは捕捉しない。

上記のコードを省略なしで書くと、3通りの実行パターンがある。

  • 式1→式3→式4 (例外なし)
  • 式1→式2→式4 (例外を捕捉)
  • 式1→式4 (例外が発生するが、捕捉できない)

例外を捕捉する (コード例2)

require './factorial'

def fact(n)
  n.factorial
rescue ArgumentError
  "1以上の整数を指定してください"
rescue NoMethodError
  "整数を指定してください"
end

fact 3      # => 6
fact 4      # => 24
fact 5      # => 120
fact 0      # => "1以上の整数を指定してください"
fact 2.5    # => "整数を指定してください"
fact "2"    # => "整数を指定してください"

f:id:mas-higa:20130416224157p:image:w360

  • メソッド定義の def - end にも rescue, else, ensure が書ける。*4

例外の種類

puts NoMethodError.ancestors
# >> NoMethodError
# >> NameError
# >> StandardError
# >> Exception
# >> Object
# >> Kernel
# >> BasicObject

f:id:mas-higa:20130416224130p:image:w360

rescue修飾子

require './factorial'

3.factorial   rescue 0  # => 6
4.factorial   rescue 0  # => 24
5.factorial   rescue 0  # => 120
0.factorial   rescue 0  # => 0
2.5.factorial rescue 0  # => 0
"2".factorial rescue 0  # => 0

f:id:mas-higa:20130416224057p:image:w360

  • rescue の後置。
    • まず rescue の左辺を評価。
    • 例外が起きると rescue の右辺を評価。
  • 例外が起きたら、結果は 0 でいい。という大雑把なコード。

例外を起こす

raise "simple"

# ~> ex.rb:1:in `<main>': simple (RuntimeError)

f:id:mas-higa:20130416224018p:image:w360

  • 例外を起こすには Kernel#raise メソッドを使う。
  • RuntimeError が発生する。

例外を起こす (クラス指定)

raise ArgumentError, "bad argument"

# ~> ex.rb:1:in `<main>': bad argument (ArgumentError)

f:id:mas-higa:20130416223946p:image:w360

  • 例外クラスを指定して raise できる。

例外を起こす (オブジェクト)

raise TypeError.new("can't convert...")

# ~> ex.rb:1:in `<main>': can't convert... (TypeError)

f:id:mas-higa:20130416223843p:image:w360

例外を起こす (独自の例外クラス)

class MyError < StandardError; end

raise MyError, 'original'

# ~> ex.rb:3:in `<main>': original (MyError)

f:id:mas-higa:20130416223758p:image:w360

  • 独自の例外クラスを作れる。*6

サンプルコード

スライド、コードに間違いがあれば、ご指摘ください。

*1:スライドだけ欲しい人は直接どうぞ http://higaki-it.jp/ruby/57/print.pdf

*2スタックトレースとかバックトレースとか言う。

*3:あれ? 今実行したらメッセージ違う!?

*4:else節の評価結果はメソッドの評価結果となるが、ensure節の評価結果はメソッドの評価結果とはならない。

*5:例外でないオブジェクトを指定すると TypeError が発生する。えっ?

*6エンタープライズだと独自の例外じゃないと起こせないことあるよね。

*7:StandardError なら、クラス指定なしの rescue でも捕捉できるから。

2012-07-21

第55回 Ruby/Rails 勉強会@関西で初級者向けレッスンやってきた

初級者向けレッスンを担当したので、以下スライドを解説。*1 *2

クラスとは

f:id:mas-higa:20120722174048p:image

クラスを作ってみよう

class Person; end

obj = Person.new
  # => #<Person:0x10102718>

obj.class           # => Person
Person.superclass   # => Object

f:id:mas-higa:20120722174047p:image

  • Person という名前のクラスを作る。
  • class から end までがクラス。
  • クラス名は大文字で始める (キャメルケース)

属性を持たせてみよう

class Person
  def initialize name
    @name = name
  end
end

matz = Person.new('matz')
  # => #<Person:0x10138598 @name="matz">

f:id:mas-higa:20120722174045p:image

属性にアクセスしてみよう

class Person
  attr_reader :name
end

matz.name       # => "matz"

f:id:mas-higa:20120722174044p:image

attr_writer
セッター
attr_reader
ゲッター
attr_accessor
セッター&ゲッター

変数・定数のおさらい

ローカル変数
person
インスタンス変数
@person
クラス変数
@@person
グローバル変数
$person
定数
Person

f:id:mas-higa:20120722174042p:image

  • 最初の数文字を見れば区別できる
  • クラス名は定数

属性を増やしてみよう

class Person
  def initialize name, born = nil
    @name, @born = name, born
  end
  attr_accessor :born
end

matz.methods.map(&:to_s).grep(/born/)
  # => ["born", "born="]

f:id:mas-higa:20120722173746p:image

属性を増やしてみよう (2)

  • アクセスしてみる
matz.born = Time.local(1965, 4, 14)
dhh = Person.new('dhh',
        Time.local(1979, 10, 15))

matz.born
  # => 1965-04-14 00:00:00 +0900
dhh.born
  # => 1979-10-15 00:00:00 +0900

f:id:mas-higa:20120722173744p:image

  • 代入のような表記で born= メソッドが呼ばれる

メソッドを作ってみよう

class Person
  def age
    (Time.now.strftime('%Y%m%d').to_i -
     @born.strftime('%Y%m%d').to_i) /
      10000
  end
end
matz.age    # => 47
dhh.age     # => 32

f:id:mas-higa:20120722173743p:image

メソッドを上書きしてみよう

matz.to_s  # => "#<Person:0x10138598>"

class Person
  def to_s
    "#{@name}(#{age})"
  end
end
matz.to_s  # => "matz(47)"
dhh.to_s   # => "dhh(32)"

f:id:mas-higa:20120722173741p:image

インスタンスを比較してみると……

person = Marshal.load(Marshal.dump matz)

person == dhh   # => false
person == matz  # => false # おかしい

f:id:mas-higa:20120722173739p:image

  • 深いコピー
    • Marshal.dump して Marshal.load する
  • == はメソッド
    • Person#== を定義しても良いが……
    • ==, !=, >, <, >=, <= 別々に定義するの面倒

順序を決めよう

class Person
  include Comparable
  def <=> o
    @name <=> o.name
  end
end
person == matz  # => true
person == dhh   # => false
matz > dhh      # => true

f:id:mas-higa:20120722173622p:image

  • <=> 演算子で順序を決定
  • Comparable で ==, != , >, <, >=, <= を生成

Array#sort してみよう

  • 順序が決まれば sort できる
people = [matz, dhh]

people.sort  # => [dhh(32), matz(47)]

f:id:mas-higa:20120722173621p:image

Array ときたら、次は ……

Hash のキーにしてみると……

値に入れてもおもしろくないので

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

key = Marshal.load(Marshal.dump matz)

key == matz # => true
h[key]      # => nil # おかしい

f:id:mas-higa:20120722173619p:image

  • 深いコピーをしたオブジェクトをキーにすると
    • 値が取り出せない!

hash 値を計算しよう

class Person
  def hash
    code = 17
    code = 37 * code + @name.hash
    code = 37 * code + @born.hash
  end
end
matz.hash   # => -22068619118
dhh.hash    # => 14923733106

f:id:mas-higa:20120722173617p:image

  • 計算方法はハチドリ本に書いてあった*4
    • 出所は Effective Java らしい

eql? を上書きしよう

  • hash 値がぶつかっていないか調べる必要がある
  • Hash クラスは eql? でキーが正しいか判断する
class Person
  def eql? o
    return false unless @name.eql? o.name
    return false unless @born.eql? o.born
    true
  end
end
key.eql? matz   # => true
key.eql? dhh    # => false

f:id:mas-higa:20120722173616p:image

  • a.eql? b が真の場合 a.hash == b.hash であること
    • そのように hash, eql? を定義する

Hash にアクセスしてみよう

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

h[key]      # => "Ruby"

f:id:mas-higa:20120722173429p:image

  • 最初に作った h (Hash) は Person#hash を定義する前に作成したもの
  • 格納位置がおかしいので作り直す*5

等値性のおさらい

==
内容が等しいか?
===
case 式で使用
eql?
Hash クラスが使用
equal?
同一オブジェクトか?

f:id:mas-higa:20120722173428p:image

アクセス制御してみよう

オマケ

  • public
  • protected
  • private
class Person
  protected :born
end
matz.born
# ~> protected method `born' called for matz(47):Person (NoMethodError)

f:id:mas-higa:20120722173426p:image

  • protected なメソッドを呼ぶと例外が発生する

今日 話さなかったこと

  • Range の始点と終点

f:id:mas-higa:20120722173425p:image

上記を調べて初級者を抜け出そう。

まとめ

f:id:mas-higa:20120722173423p:image

以上、クラスを作る際の決まり事を紹介した。

*1:スライドだけ欲しい人は直接どうぞ。http://higaki-it.jp/ruby/55/slide.pdf

*2:コードが必要な人は gist からどうぞ。

*3Kernel モジュールで定義されている

*4:会場の @nayutaya さんによると [@name, @born].hash とするのが一般的らしい

*5:会場の @no6v さんによると h.rehash するといいらしい

2012-05-26

第54回 Ruby/Rails 勉強会@関西で初級者向けレッスンやってきた

初級者向けレッスンを担当したので、以下スライドを解説。*1

スライドだけ欲しい人は直接どうぞ。

f:id:mas-higa:20120531220309p:image

Array とは

[1, 1, 2, 3]

[1, "two", [3, "3"], 4.0, :five]
  • まずは Array のリテラルを紹介。
  • 最初のは、Fixnum を 4つ持つ Array.
  • 次のは、Fixnum, String, Array, Float, Symbol の 5つの要素を持つ Array.

f:id:mas-higa:20120531220524p:image

Array とは (2)

a = [1, "two", [3, "3"], 4.0, :five]

a[0]      # => 1
a[-1]     # => :five
a[1] = "2nd"
a[3, 2]   # => [4.0, :five]
a[1..-2]  # => ["2nd", [3, "3"], 4.0]
a[5]      # => nil
  • 添字は 0 オリジン
  • 添字が負数なら後ろから数える
  • 代入できる
  • サイズを指定して取り出せる
  • 範囲を指定して取り出せる
  • 値がなければ nil

f:id:mas-higa:20120531220522p:image

Array オブジェクトの作り方

["a", "b", "c"] # => ["a", "b", "c"]
("a".."c").to_a # => ["a", "b", "c"]
[*"a".."c"]     # => ["a", "b", "c"]
%w[a b c]       # => ["a", "b", "c"]

f:id:mas-higa:20120531220520p:image

Array オブジェクトの作り方 (2)

"No Ruby, No Life.".scan(/\w+/)
  # => ["No", "Ruby", "No", "Life"]

"1,1,2,3,5,8".split(/,/)
  # => ["1", "1", "2", "3", "5", "8"]
  • String から Array を作ることがある。
  • 最初の例は、String#scan で単語を抽出。*4
  • 次の例は、String#split で、なんちゃって CSV

f:id:mas-higa:20120531220518p:image

Hash とは

{:AAPL=>566.71, :GOOG=>605.23}

{AAPL: 566.71, GOOG: 605.23}
    # => {:AAPL=>566.71, :GOOG=>605.23}
  • つづいて Hash のリテラルを紹介。
  • キーと値は => で区切る。
    • 1.9 からは下の表記を使える。(よりデータの羅列に見える)

f:id:mas-higa:20120531220510p:image

Hash とは (2)

h = {AAPL: 566.71, GOOG: 605.23}

h[:AAPL]      # => 566.71
h[:MSFT] = 31.16
h[:FB]        # => nil
  • Hash もアクセスには [ 角かっこ ] を使う
  • 代入できる (値の存在しないキーに対しても可)
  • 値がなければ nil

f:id:mas-higa:20120531221117p:image

Hash オブジェクトの作り方

a = [:AAPL, 566.71, :GOOG, 605.23]

Hash[*a]
    # => {:AAPL=>566.71, :GOOG=>605.23}
  • Array から Hash を作ることが (たまに) ある。
  • キーと値を羅列した Array を展開して Hash.[] で Hash オブジェクトを生成。
  • ファイルから読み込んだデータを String#split して Hash を生成するような用途。

順序がおかしくても気にしない

Hash[:AAPL, 566.71, :GOOG, 605.23]
  # => {:AAPL=>566.71, :GOOG=>605.23}
Hash[:AAPL, 566.71, 605.23, :GOOG]
  # => {:AAPL=>566.71, 605.23=>:GOOG}

要素数が奇数なら、

Hash[:AAPL, 566.71, :GOOG, 605.23, :FB]
# ~> odd number of arguments for Hash (ArgumentError)

あまり使わないけど、使うとハマる Array の初期化

f:id:mas-higa:20120531221115p:image

Array の初期化

Array.new(4, 0)     # => [0, 0, 0, 0]

a = Array.new(3, "ruby")
    # => ["ruby", "ruby", "ruby"]

a[0].upcase!        # => "RUBY"

a   # => ["RUBY", "RUBY", "RUBY"]
  • 値がないときも、nil 以外を返して欲しい
    • nil だと演算できない
a = []

a[0] += 1
# ~> undefined method `+' for nil:NilClass (NoMethodError)

f:id:mas-higa:20120528214743g:image

全ての要素が同一のオブジェクトを指している。


では、どうするか?

そこでブロックですよ!

f:id:mas-higa:20120531221113p:image

Array の初期化 (2)

a = Array.new(3){"ruby"}
    # => ["ruby", "ruby", "ruby"]

a[0].upcase! # => "RUBY"

a   # => ["RUBY", "ruby", "ruby"]

ブロックで初期値を指定すると、別のオブジェクトになる。

f:id:mas-higa:20120528215806g:image

レッスンではサッと流したけど以下が重要。

  • ブロックが要素数だけ繰り返される
  • ブロック内でオブジェクトを生成している

次の例だと、せっかくブロック使っても無意味。

s = "ruby"
a = Array.new(3){s}
    # => ["ruby", "ruby", "ruby"]

a[0].upcase!  # => "RUBY"

a   # => ["RUBY", "RUBY", "RUBY"]

また、ブロックでは値が受け取れる。

a = Array.new(3){|i| i.to_s}
    # => ["0", "1", "2"]

Hash についても同じことがいえる。

f:id:mas-higa:20120531221112p:image

Hash のデフォルト値

hash = Hash.new(0.0)  # => {}
hash[:AAPL]           # => 0.0

hash = Hash.new{|h, k| h[k] = ""}
                      # => {}
hash[:GOOG]           # => ""
hash[:IBM]            # => ""
※ キーの破壊
  • Hash はサイズを指定しない
    • サイズだけ指定されても、どのキーなんだよってことになる
  • ブロックには Hash と キーが渡される
    • そこでデフォルト値を生成する

キーの破壊

  • Hash はキーの hash メソッドで値の格納位置を決める (たぶん)
  • 格納位置の値が確かにそのキーの値であるか eql? メソッドで確認する (たぶん)

なので、キーを破壊すると、破壊前の格納位置に到達できなくなる (たぶん)

ただし、String をキーにしても、Hash のキーを破壊することはできない。

Ruby は String を特別に扱っている

  1. String オブジェクトをコピーして
  2. freeze する
key = "ruby"    # [あとでこわす]

h = {}
h[key] = "関西"
h   # => {"ruby"=>"関西"}

h.first                   # => ["ruby", "関西"]
h.first.first             # => "ruby"
h.first.first.eql? key    # => true   # 文字列は同じ
h.first.first.equal? key  # => false  # 別のオブジェクト
h.first.first.frozen?     # => true   # freeze されている

key.upcase!               # => "RUBY"
h   # => {"ruby"=>"関西"} # 破壊できない

f:id:mas-higa:20120531221110p:image

繰り返し each

[0, 1, 2].each{|i| puts i}

[0, 1, 2].each do |i|
  puts i
end

# >> 0
# >> 1
# >> 2
  • 繰り返しには each メソッドを使う
  • ブロックは { 波かっこ } または do end で囲む
    • ブロック内を繰り返す
    • 繰り返しのたびに i が順番に要素を指す

f:id:mas-higa:20120531221641p:image

繰り返し Enumerable

Array.ancestors
  # => [Array, Enumerable, Object, Kernel, BasicObject]
Hash.ancestors
  # => [Hash, Enumerable, Object, Kernel, BasicObject]
  • Enumerable
    • 繰り返しを行なうクラスのための Mix-in
    • クラスには each メソッドが必要

Enumerable という便利なモジュールがありまして、

f:id:mas-higa:20120531221638p:image

繰り返し Enumerable (2)

a = [2, 3, 5, 7]        # => [2, 3, 5, 7]

a.map{|i| i * i}        # => [4, 9, 25, 49]
a.select{|i| i.odd?}    # => [3, 5, 7]

a.inject{|s, i| s += i} # => 17

a.all?{|n| n.prime?}    # => true

それぞれのメソッドは「るりま」を見てね。きりがないし。*5

でも、この後、しょーもない紙芝居やるよりも inject の動きとか見た方が良かったんじゃないか、とスライド作りながら思ってた。*6

inject を教科書通りに呼ぶと、こんな感じ。

a = [2, 3, 5, 7]  # => [2, 3, 5, 7]

a.inject(0){|s, i| s += i}  # => 17

a.inject(0) do |s, i|
  puts "s = #{s} + #{i}"
  s += i
end

# >> s = 0 + 2
# >> s = 2 + 3
# >> s = 5 + 5
# >> s = 10 + 7

各ループで s と i は上記の値を指している。

つづいてスライドの例

a.inject{|s, i| s += i}     # => 17

a.inject do |s, i|
  puts "s = #{s} + #{i}"
  s += i
end

# >> s = 2 + 3
# >> s = 5 + 5
# >> s = 10 + 7

s の初期値が省略されると、s = a[0]; i = a[1] から繰り返しが始まる。

しかし、上級者になると、こんな書き方をする。

a.inject(&:+)               # => 17

f:id:mas-higa:20120531221635p:image

繰り返しと多重代入

a = [[:matz, 47], [:dhh, 32]]
a.size      # => 2

a.each{|i| puts "#{i[0]}(#{i[1]})"}

a.each{|name, age|puts "#{name}(#{age})"}

# >> matz(47)
# >> dhh(32)

ブロックの引数は、代入だと思ってみる。

i = [:matz, 47]
i       # => [:matz, 47]

name, age = [:matz, 47]
name    # => :matz
age     # => 47

さらに無理のある例、

f:id:mas-higa:20120531221632p:image

繰り返しと多重代入 (2)

a = [[1, [:matz, 47]], [2, [:dhh, 32]]]
a.size      # => 2

a.each do |id, (name, age)|
  puts "#{id}: #{name}(#{age})"
end

# >> 1: matz(47)
# >> 2: dhh(32)

これも代入だと

i = [1, [:matz, 47]]
i       # => [1, [:matz, 47]]

id, (name, age) = [1, [:matz, 47]]
id      # => 1
name    # => :matz
age     # => 47

かっこがないと、

id, name, age = [1, [:matz, 47]]
id      # => 1
name    # => [:matz, 47]
age     # => nil

f:id:mas-higa:20120531221629p:image

繰り返しと多重代入 (3)

h = {matz: 47, dhh: 32}

h.each{|i| puts "#{i[0]}(#{i[1]})"}

h.each{|name, age|puts "#{name}(#{age})"}

# >> matz(47)
# >> dhh(32)

Hash では、みんな自然と多重代入してた

h = {matz: 47, dhh: 32}

h.each do |i|
  puts i.class
  p i
end

# >> Array
# >> [:matz, 47]
# >> Array
# >> [:dhh, 32]

Hash#each すると、キーと値のペアが Array で渡される。


要素の数と引数の数が合わないと、どうなるのか?*7

# 引数が多い場合
i, j, k = [1, 2]
i   # => 1
j   # => 2
k   # => nil

# 引数が足りない場合
i, j = [1, 2, 3]
i   # => 1
j   # => 2

# 引数が足りなくても……
i, *j = [1, 2, 3]
i   # => 1
j   # => [2, 3]

ちょっと蘊蓄

f:id:mas-higa:20120531222112p:image

Array のコピー

a = [1, 2, 3]

b = a

a[0] = 0

a   # => [0, 2, 3]
b   # => [0, 2, 3]

f:id:mas-higa:20120528220146g:image

  • 代入はコピーじゃない
  • a が指す Array オブジェクトを b も指すようになった

f:id:mas-higa:20120531222111p:image

Array のコピー (2)

a = ["a", "b", "c"]

b = a.dup

a[0] = "A"

a   # => ["A", "b", "c"]
b   # => ["a", "b", "c"]
  • Object#dup*8 (または Object#clone) でコピー
  • 浅いコピーなので注意!

f:id:mas-higa:20120531222109p:image

Array のコピー (3)

a = ["a", "b", "c"]

b = a.dup

a[1].upcase!

a   # => ["a", "B", "c"]
b   # => ["a", "B", "c"]

f:id:mas-higa:20120528220708g:image

今回、話さなかったこと

[].respond_to? :each   # => true
  • ブロックは Proc
block = Proc.new{|i| i * 2}
[*0..4].map &block   # => [0, 2, 4, 6, 8]

block[5]             # => 10
  • 外部イテレータ
a = [2, 3, 5, 7]
i = a.each
i.next  # => 2
i.next  # => 3
i.next  # => 5
i.next  # => 7
i.next  # ~> `next': iteration reached an end (StopIteration)

まとめ

  • Array と Hash の作り方・使い方
    • 初期化・デフォルト値はブロックで
  • 繰り返しはブロックで
  • 浅いコピー・破壊に注意

演習問題は [あとでかく]

*1:はてなダイアリーを使うと用語の説明が省略できるかな、と思って。

*2オブジェクトには型がある。

*3:離散範囲なら可能

*4:1.9 でマルチバイト文字列から単語を抽出するには /\p{Word}+/ を使う

*5:Integer#prime? は require 'prime' しないと使えない

*6:紙芝居を作ってるうちに楽しくなってきて、こんなスライドに……

*7:質問されたけど、答えを用意してなかった

*8:duplicate の略 (だよね?) なので <でゅぷ> って読んでたけど <だっぷ> と読む説がある

2009-07-18

るりまを使おう

RubyKaigi2009 で「るりまを gem でインストールできたら便利じゃね?」という意見が出ていたけど、るりまのインストールってそんなに面倒か?

スナップショットのダウンロード

これを適当なディレクトリに展開する。

ruby-refm-1.9.1-dynamic-日付 というディレクトリができるので、ruby-refm-1.9.1-dynamic という名前 (日付部分を取り除く) でシンボリックリンクしておくと便利。

refe コマンドの作成。

例えばこんな感じ。

#! /bin/bash

cmd="ruby -Eeuc-jp -C 展開したディレクトリ/ruby-refm-1.9.1-dynamic -I bitclust/lib bitclust/bin/refe.rb -d db-1_9_1"

if [ -t 1 ]; then
  $cmd "$@" | ${PAGER:-more}
else
  $cmd "$@"
fi

あとは使うだけ

$ refe File
class File < IO

ファイルアクセスのためのクラスです。

通常 [[m:Kernel.#open]] または [[m:File.open]] を使って生成します。
[[c:IO]] クラスがインクルードしている [[c:File::Constants]] は File クラスに関係する定数を
格納したモジュールです。
また [[c:File::Stat]] は stat 構造体( [[man:stat(2)]] 参照)を表すクラスです。

とってもカンタンだ。

emacs からも使える。

2009-04-21

ReFe で at_exit を引くとエラー

ReFe とは、Ruby の日本語リファレンスマニュアルを引くためのツール。

昔は refe というコマンドがあったけど、現在はるりまの bitclust に取って代られた。

詳しくは るりま Wiki を参照。

ところで refe コマンドは、どうなってるのが普通なんだろうか?
コマンド自体はるりまには付属してなさそう …… と思ったら、るりま Wiki に書いてあった。

ちなみに今使ってる refe はこんな感じ。

#! /usr/local/bin/bash

cmd="ruby19 -Eeuc-jp -C /usr/local/share/ruby-refm-1.9.1-dynamic -I bitclust/lib bitclust/bin/refe.rb -d db-1_9_1"

if [ -t 1 ]; then
  $cmd "$@" | ${PAGER:-more}
else
  $cmd "$@"
fi

なぜか refe で at_exit を引くとエラーになってしまうんだけど、るりまの中の人に連絡した方がいいんだろうか?*1

$ refe at_exit
/usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/nameutils.rb:190:in `typechar2mark': must not happen: "" (RuntimeError)
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/nameutils.rb:99:in `methodid2typemark'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/methodentry.rb:66:in `typemark'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/methodid.rb:109:in `match?'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/classentry.rb:300:in `block in get_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/classentry.rb:300:in `each'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/classentry.rb:300:in `detect'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/classentry.rb:300:in `get_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/methoddatabase.rb:365:in `get_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/completion.rb:460:in `entry'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:394:in `describe_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:329:in `show_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:232:in `find_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:260:in `find_class_or_method'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:209:in `search_pattern'
        from /usr/local/share/ruby-refm-1.9.1-dynamic-20090228/bitclust/lib/bitclust/searcher.rb:93:in `exec'
        from bitclust/bin/refe.rb:26:in `_main'
        from bitclust/bin/refe.rb:18:in `main'
        from bitclust/bin/refe.rb:36:in `<main>'

Kernel.at_exit ならエラーにならない。

週末に中の人と会うから聞いてみよう。

*1:るりまは 20090329 のスナップショット。