Arrayのメソッドいろいろ

挙動がちょっとややこしいものや、「どうなんだっけ?」となりがちな点についてまとめてメモ。

Array#product

Array#product(*array) -> array

レシーバの配列と引数の配列から、それぞれの全要素を掛け合わせた一次元配列を返す。

main = [:meat, :fish]
salad = [:ceasar, :cobb]

main.product(salad)
=> [
  [:meat, :ceasar],
  [:meat, :cobb],
  [:fish, :ceasar],
  [:fish, :cobb]
]

引数に複数の配列を渡すこともできる。

drink = [:tea, :coffee]

main.product(salad, drink)
=> [
  [:meat, :ceasar, :tea],
  [:meat, :ceasar, :coffee],
  [:meat, :cobb, :tea],
  [:meat, :cobb, :coffee],
  [:fish, :ceasar, :tea],
  [:fish, :ceasar, :coffee],
  [:fish, :cobb, :tea],
  [:fish, :cobb, :coffee]
]

Array#transpose

Array#transpose -> array

2次元配列をレシーバとして、行列を逆転させた2次元配列を返す。

kana = [
  ['', '', '', '', ''],
  ['', '', '', '', ''],
  ['', '', '', '', ''],
  ['', '', '', '', ''],
  ['', '', '', '', '']
]

kana.transpose
=> [
  ["", "", "", "", ""],
  ["", "", "", "", ""],
  ["", "", "", "", ""],
  ["", "", "", "", ""],
  ["", "", "", "", ""]
]

Array#zip

Array#zip(*array) -> array

レシーバの配列及び引数の配列をインデックスごとに対応付け、配列を返す。

main = [:meat, :fish]
salad = [:ceasar, :cobb]

main.zip(salad)
=> [
  [:meat, :ceasar],
  [:fish, :cobb]
]

Array#productは全ての組み合わせを生成したけど、Array#zipはインデックスが同一の要素を対応付けるだけ。

こちらも複数の配列を引数にとることができる。

なおインデックスはレシーバを基準として、引数の配列が対応する要素を欠く場合はnilによって補う。

main = [:meat, :fish, :pasta]
salad = [:ceasar, :cobb]
drink = [:tea, :coffee, :juice]

main.zip(salad, drink)
=> [
  [:meat, :ceasar, :tea],
  [:fish, :cobb, :coffee],
  [:pasta, nil, :juice]
]

各種演算子

arr1 = [1,2,3]
arr2 = [2,3,4]

Array#+

シンプルに要素を足し合わせる。

arr1 + arr2  # => [1, 2, 3, 2, 3, 4]

Array#-

差集合

arr1 - arr2  # => [1]

Array#&

積集合

arr1 & arr2  # => [2, 3]

Array#|

和集合

arr1 | arr2  # => [1, 2, 3, 4]

Array#[]

ゲッターもセッターも原則として参照エラーを起こさない。

frameworks = ['Rails', 'Sinatra']
frameworks[4]  # => nil
frameworks[-5]  # => nil
frameworks[1..3]  # => ["Sinatra"]
frameworks[4] = 'Cuba'
frameworks  # => ["Rails", "Sinatra", nil, nil, "Cuba"]

唯一indexをマイナスで指定したセッターはIndexErrorを返す。

frameworks[-10] = 'hanami'   # => IndexError

Array#flatten!

Array#flattenとの違いは以下2点。

  • 破壊的メソッド
  • 平坦化が行われない場合はnilを返す <- caution!
[1,2,3].flatten   # => [1,2,3]
[1,2,3].flatten!  # => nil

配列の生成

  • Kernel.#Array(arg)
    • arg.to_aが呼べればその値を、呼べなければargを唯一の要素とする配列を返す。
  • Array#new
    • Array#new(size = 0, obj = nil)
    • Array#new(size = 0) {|index| ... }

特に注意すべきは以下。

a1 = Array.new(3, 'hoge')
a2 = Array.new(3) {'hoge'}

a1.each {|s| p s.object_id }
=> 70167535380400
   70167535380400
   70167535380400
a2.each {|s| p s.object_id }
=> 70167535364000
   70167535363980
   70167535363960

日時のフォーマット

都度調べればいいけど、最低限「日付は小文字、時間は大文字」ってイメージをもっとくとよさそう。DateとTimeでフォーマットは基本的に共通。

日付

d = Date.new(2020,12,31)  # => #<Date: 2020-12-31 ((2459215j,0s,0n),+0s,2299161j)>

d.strftime('%y')  # => "20"
d.strftime('%Y')  # => "2020"
d.strftime('%m')  # => "12"
d.strftime('%d')  # => "31"
d.strftime('%D')  # => "12/31/20"
d.strftime('%x')  # => "12/31/20"

d.to_s            # => "2020-12-31"
d.strftime("%F")  # => "2020-12-31"

時間

t = Time.new(2020,12,31,23,59,59)  # => 2020-12-31 23:59:59 +0900

t.strftime("%H")  # => "23"
t.strftime("%M")  # => "59"
t.strftime("%S")  # => "59"
t.strftime("%X")  # => "23:59:59"

t.to_s            # => "2020-12-31 23:59:59 +0900"

X進数の数値リテラルほか

リファレンスのなぞり書き。

X進数の数値リテラル

接頭辞をつけることで10進数以外の表記でリテラルを記述できる。 接頭辞と整合的でない入力が続いた場合はエラーを返す。

16進数 => 接頭辞0x

0x10  # => 16
0x1g  # => SyntaxError

2進数 => 接頭辞0b

0b10  # => 2
0b12  # => SyntaxError

8進数 => 接頭辞0o or 0

0o10  # => 8
0o19  # => SyntaxError
010  # => 8
019  # => SyntaxError

String#hex

レシーバを16進数表記とみなして、相当する値をintegerで返す。

'10'.hex  # => 16

16進数として認識できる文字がない場合は0を返す。

''.hex  # => 0

レシーバに含まれる文字は以下のいずれかとして解釈される。

  • 16進数の数値部分: [0-9a-fA-F]
  • 16進数の数値以外の部分: 戻り値に影響しない。
    • 接頭辞0x
      • (ex.)'0x10'.hex # => 16
    • 数字区切りとしてのアンダーバー
      • (ex.)'1_0_'.hex # => 16
  • 16進数でない文字: その文字以降は16進数として認識されない。
    • (ex.)'10g1'.hex # => 16
    • (ex.)'g'.hex # => 0
    • (ex.)'_10'.hex # => 0
    • (ex.)'1__0'.hex # => 1

String#oct

String#hexの8進数版。

String#to_i

String#to_i(base = 10) -> Integer

  • 引数baseを基数とする数値表現としてレシーバを解釈して、数値を返す。
  • String#hexはString#to_i(8)と同じ。
  • baseを0と指定するとprefixから基数を判断する。prefixがない場合は10進数として解釈する。

ヒアドキュメント

リファレンスのなぞり書き。

基本文法

<<[(-|~)]["'`]識別子["'`]
   ...
識別子

<<[(-|~)]

<<

  • ヒアドキュメント内の空白文字は全て文字列に含まれる。
  • 終端の識別子は行頭にある必要がある。=インデントされていると識別子とみなされない。
string = 
  <<EOS
    line1
      line2
EOS
=> "    line1\n      line2\n"
string = 
  <<EOS
    line1
      line2
  EOS
EOS
=> "    line1\n      line2\n  EOS\n"

行頭にない(=インデントされた)識別子は単なる文字列と見なされる。

<<-

  • ヒアドキュメント内の空白文字は全て文字列に含まれる。
  • 終端の識別子は行頭になくてよい。=インデントされていても識別子とみなされる。
string = 
  <<-EOS
    line1
      line2
  EOS
=> "    line1\n      line2\n"

<<~

  • ヒアドキュメント内の空白文字は、最もインデントが浅い行を基準として取り除かれる。
  • 終端の識別子は行頭になくてよい。=インデントされていても識別子とみなされる。
string = 
  <<~EOS
    line1
      line2
  EOS
=> "line1\n  line2\n"

最もインデントが浅いline1を基準として、そこからの相対的なインデントのみが空白文字となっている。

["'`]識別子["'`]

それぞれの囲み文字で囲まれた文字列として評価される。

'

n = 10
<<'EOS'
  #{n}
EOS
=> "  \#{n}\n"

"

n = 10
<<"EOS"
  #{n}
EOS
=> "  10\n"

なし

"と同等。

n = 10
<<EOS
  #{n}
EOS
=> "  10\n"

`

<<`EOS`
  echo hoge
EOS
=> "hoge\n"

Nonblocking TCP Server

Socket(TCP)通信をかじったのでメモ。

first implementation

サーバ。

require 'socket'
server = TCPServer.new(4481)
loop do
  connection = server.accept
  request = connection.read
  connection.write("request served: #{request}")
  connection.close
end

クライアント。

require 'socket'
client = TCPSocket.new('localhost', 4481)
client.write('hoge')
client.close_write
client.read  # => "request served: hoge"
client.eof?  # => true

multiple read/write

1つのTCPコネクションで複数回read/writeできるようにする。

サーバ。

require 'socket'
server = TCPServer.new(4481)
loop do
  connection = server.accept
  loop do
    request = connection.gets.chomp
    break if request == 'exit'
    connection.puts("request served: #{request}")
  end
  connection.close
end

クライアント。

require 'socket'
client = TCPSocket.new('localhost', 4481)
client.puts('hoge')
client.gets  # => "request served: hoge\n"
client.puts('fuga')
client.gets  # => "request served: fuga\n"
client.puts('exit')
client.eof?  # => true

nonblock accept

上記のサーバでは、クライアントからの接続要求を待っている間ブロックされてしまう。(accept)

仮にサーバが1秒に1ずつ数を数え上げる処理も行いたいとする。

クライアントからの接続要求を待ちつつも数え上げも行えるようにしたサーバが以下。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      request = connection.gets.chomp
      break if request == 'exit'
      connection.puts("request served: #{request}")
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

サーバを立ち上げた瞬間から数え上げ始める。

クライアントからの接続があるとリクエスト処理に専念する。

クライアントが接続を切ると数え上げを再開する。

nonblock read

上記のサーバでは、クライアントからのデータ送信を待っている間ブロックされてしまう。(read)

クライアントからのデータ送信を待ちつつも数え上げも行えるようにしたサーバが以下。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      begin
        request = connection.read_nonblock(1024).chomp
        break if request == 'exit'
        connection.puts("request served: #{request}")
      rescue Errno::EAGAIN
        p counter += 1
        sleep(1)
        retry    
      end
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

クライアントからの接続を待っている間にも数え上げを続けることができた。

nonblock write

いま、サーバから返すデータが大量になったとする。

before: "request served: #{request}\n"

after: "request served: #{request}" * 1_000_000 + "\n"

この場合、クライアントがメッセージを送ると、レスポンス送信においてクライアント側のバッファがいっぱいになり、writeがブロックされる。

このとき数え上げは中断されるが、クライアントがreadするとクライアント側のバッファに空きができるのでwriteを再開し、writeを終えると数え上げが再開される。

これを、クライアントがreadしなくてもサーバで数え上げを続けるようにしてみる。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      begin
        request = connection.read_nonblock(1024).chomp
        break if request == 'exit'
        payload = "request served: #{request}" * 1_000_000 + "\n"
        loop do
          begin
            sent = connection.write_nonblock(payload)
            break if sent >= payload.size
            payload.slice!(0, sent)                
          rescue Errno::EAGAIN
            p counter += 1
            sleep(1)
            retry
          end
        end
      rescue Errno::EAGAIN
        p counter += 1
        sleep(1)
        retry    
      end
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

レスポンス時にクライアントがreadせずwriteがブロックされた状態でも、数え上げを続けることができた。

nonblock connect

まとめようと思ったが、connectがブロックされる状態を再現できなかったので省略する。

IO.selectとの関係

各手法の特徴
  • read/write/accept/connectは、1つのsocketにはりついてブロックする。

  • xxxx_nonblockは、1つのsocketについて全くはりつかずに、試みたらただちにreturnする。

  • IO.selectは複数のsocketを見渡しながらブロックする。

つかいわけ
  1. 1つのsocketにはりついてブロックしてOKな場合
    • read/write/accept/connectをつかう
  2. socketにはりつかなくてよい(時間軸上の点でたびたびトライすればよい)場合
    • xxxx_nonblockとretryを組み合わせてつかう
  3. 複数のsocketを見渡して待ち構えたい場合
    • xxxx_nonblockとIO.selectを組み合わせてつかう
    • さらにtimeoutとretryをつかうことで、時々待ち構える状態から抜けることができる

上記の例は全て2.の場合の実装とした。

superをフックメソッドで代替すると良い、という話

下記書籍を読んでて学びがあったので、自分なりに整理してメモ。

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

要約

継承において、あるメソッドを呼んだら各サブクラスに共通の処理をしつつ、各サブクラス独自の処理もしたいとき、以下の方法がある。

  • superをつかう
    • Parent#methodを定義し、共通処理を記述する
    • Child#methodを定義し、superで共通処理(Parent#method)を呼んだうえで、独自の処理を記述する
  • フックメソッドをつかう
    • Child#methodは定義しない
    • Child#featureに各サブクラス独自の処理を定義する
    • Parent#methodを定義し、共通処理を記述したうえで、独自処理(Child#feature)を呼び出す

両者を比較すると、superをつかうよりも、フックメソッドをつかう方が良いことが多い*1

サンプル

まず、superを使う例。

class LunchSet
  def serve
    ['salad', 'coffee']
  end
end

class MeatLunchSet < LunchSet
  def serve
    super << 'meat'
  end
end

class FishLunchSet < LunchSet
  def serve
    super << 'fish'
  end
end

MeatLunchSet.new.serve  # => ["salad", "coffee", "meat"]

続いて、フックメソッドをつかう例。

class LunchSet
  def serve
    ['salad', 'coffee', main_dish]
  end

  private
  def main_dish
    raise NotImplementedError
  end
end

class MeatLunchSet < LunchSet
  private
  def main_dish
    'meat'
  end
end

class FishLunchSet < LunchSet
  private
  def main_dish
    'fish'
  end
end

MeatLunchSet.new.serve  # => ["salad", "coffee", "meat"]

serveメソッドには親クラスのLunchSetが応答し、その中でフックメソッドのmain_dishを呼び出すようにしている。

上記をそれぞれ「super版」「フックメソッド版」とし、以下2つの観点から違いを述べる。

観点1: 子クラスの親クラスに対する依存度

子クラスは親クラスについて、以下のことを知っている。

  • super版の場合
    1. 親クラスはserveというメソッドに応答する。
    2. その引数は0個である。
    3. その戻り値は<<メソッドに応答する。
  • フックメソッド版の場合
    1. 親クラスはserveというメソッドに応答する。
    2. その引数は0個である。

フックメソッド版の方が親クラスについて知っていることが少ない。つまり、親クラスに対する依存度が低い。

オブジェクト間の依存度はなるべく低く保っておいた方が、変更が波及しないので拡張する際のコストが小さい。

よってフックメソッド版の方が望ましいコードと言える。

では、実際に変更が生じた場合の具体例を以下でみてみよう。

LunchSet#serveの戻り値が配列から文字列に変更された状況を想定する。

super版の場合
class LunchSet
  def serve
    ['salad', 'coffee'].join(', ')  # changed
  end
end

class MeatLunchSet < LunchSet
  def serve
    super + ', meat'  # changed
  end
end

class FishLunchSet < LunchSet
  def serve
    super + ', fish'  # changed
  end
end

MeatLunchSet.new.serve  # => "salad, coffee, meat"

親クラスだけではなく、子クラスでも変更が生じている。

これは、MeatLunchSet#serve内の処理が「LunchSet#serveの戻り値<<メソッドに応答する」という事実に依存していたためである。

フックメソッド版の場合
class LunchSet
  def serve
    ['salad', 'coffee', main_dish].join(', ')  # changed
  end

  private
  def main_dish
    raise NotImplementedError
  end
end

class MeatLunchSet < LunchSet
  private
  def main_dish
    'meat'
  end
end

class FishLunchSet < LunchSet
  private
  def main_dish
    'fish'
  end
end

MeatLunchSet.new.serve  # => "salad, coffee, meat"

変更されたのは親クラスのみであり、子クラスには変更が生じていない。

よって、複数のクラスに波及することなく、低コストで変更を実現できるという点で、フックメソッド版の方が望ましい設計と言える。

観点2: 共通処理の呼び出しを忘れるリスク

super版では、1つのメソッドを呼び出す継承階層の旅の中で、親クラスが共通処理を行い、子クラスが独自処理を行う。複数のクラスがこっそりと連携しているため、そこでバトンが取り落とされても、気づかれない場合がある。

フックメソッド版では、最初のメソッドによって親クラスの共通処理が呼ばれ、親クラスは改めてselfに対して独自処理を呼び出し、子クラスがこれを引き受ける。この連携の過程は2回のメソッド呼び出しから構成され明示的であるため、バトンの受け渡しは衆目に晒されており、いつの間にかひっそりと過誤が生じるリスクは小さい。

以下で、サブクラスとしてPastaLunchSetを新たに作成することを想定する。

super版の場合

適切にPastaLunchSetクラスを実装するために必要なステップは2つある。

  1. PastaLunchSet#serveを定義する。
  2. PastaLunchSet#serveの中で共通処理を行うためにsuperを呼び出す。

1.については、既存の親クラスにも子クラスにもserveメソッドがあることから、その必要性は明らかである。

しかし1. に比べると、2.の必要性はそれほど明白ではない。既存の子クラスのserveメソッドの中身を慎重に観察して、嗅ぎださなければならない。

以下に2.が漏れてしまったケースを示す。

class LunchSet
  def serve
    ['salad', 'coffee']
  end
end

class MeatLunchSet < LunchSet
  def serve
    super << 'meat'
  end
end

class FishLunchSet < LunchSet
  def serve
    super << 'fish'
  end
end

class PastaLunchSet < LunchSet
  def serve
    'pasta'
  end
end

PastaLunchSet.new.serve  # => "pasta"

このパスタランチでは残念ながら食後のコーヒーを楽しむことはできない。

もちろん、これほど簡単な例ではsuperの呼び出しを忘れることは現実的ではない。しかしより複雑にアプリケーションにおいては、十分に有り得ることだろう。

しかもこの例では、メソッドを呼び出した時点ではエラーを生じずに、おそらくは実際にサラダやコーヒーに手を付けようとした時点で、つまり真に問題がある箇所とは別の箇所と形態においてエラーを誘発する。 原因を特定しにくいという点で、たちの悪い不具合だと言える。

フックメソッド版の場合

こちらの場合、適切にPastaLunchSetクラスを実装するために必要なステップは1つだけで済む。

  1. PastaLunchSet#main_dishを定義する。

既存の子クラスにmain_dishメソッドがあるため、この必要性は明白である。

class LunchSet
  def serve
    ['salad', 'coffee', main_dish]
  end

  private
  def main_dish
    raise NotImplementedError
  end
end

class MeatLunchSet < LunchSet
  private
  def main_dish
    'meat'
  end
end

class FishLunchSet < LunchSet
  private
  def main_dish
    'fish'
  end
end

class PastaLunchSet < LunchSet
  private
  def main_dish
    'pasta'
  end
end

PastaLunchSet.new.serve  # => ["salad", "coffee", "pasta"]

これならば、必要な記述が漏れてしまうリスクは、super版の場合よりも格段に低いだろう。

要約(再掲+α)

継承において、あるメソッドを呼んだら各サブクラスに共通の処理をしつつ、各サブクラス独自の処理もしたいとき、以下の方法がある。

  • superをつかう
    • Parent#methodを定義し、共通処理を記述する
    • Child#methodを定義し、superで共通処理(Parent#method)を呼んだうえで、独自の処理を記述する
  • フックメソッドをつかう
    • Child#methodは定義しない
    • Child#featureに各サブクラス独自の処理を定義する
    • Parent#methodを定義し、共通処理を記述したうえで、独自処理(Child#feature)を呼び出す

両者を比較した場合、フックメソッドをつかう方が以下の点で望ましい。*2

  • 子クラスの親クラスに対する依存度を低く保つことで、より低コストでの拡張が可能となる。
  • 子クラスを増設/改修した際に、共通処理の呼び出しを実装し損ねるリスクが低い。

*1:もちろん場合によるとは思う

*2:もちろん、場合による。