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.)
- 数字区切りとしてのアンダーバー
- (ex.)
'1_0_'.hex # => 16
- (ex.)
- 接頭辞
- 16進数でない文字: その文字以降は16進数として認識されない。
- (ex.)
'10g1'.hex # => 16
- (ex.)
'g'.hex # => 0
- (ex.)
'_10'.hex # => 0
- (ex.)
'1__0'.hex # => 1
- (ex.)
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つのsocketにはりついてブロックしてOKな場合
- read/write/accept/connectをつかう
- socketにはりつかなくてよい(時間軸上の点でたびたびトライすればよい)場合
- xxxx_nonblockとretryを組み合わせてつかう
- 複数の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版の場合
- 親クラスはserveというメソッドに応答する。
- その引数は0個である。
- その戻り値は
<<
メソッドに応答する。
- フックメソッド版の場合
- 親クラスはserveというメソッドに応答する。
- その引数は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つある。
PastaLunchSet#serve
を定義する。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つだけで済む。
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
- 子クラスの親クラスに対する依存度を低く保つことで、より低コストでの拡張が可能となる。
- 子クラスを増設/改修した際に、共通処理の呼び出しを実装し損ねるリスクが低い。