Ruby 言語仕様ガイド

他言語の経験者が 2〜3 時間でざっと読み通せるリファレンスです。ブロック・イテレータ・メタプログラミングなど Ruby 固有の概念を重点的に解説します。

Ruby 3.x経験者向けコード例付き
01

変数・定数・データ型

基礎
変数の種類基礎

Ruby の変数は先頭文字でスコープが決まる。ローカル変数は小文字/アンダースコア始まり、インスタンス変数は @、クラス変数は @@、グローバル変数は $ が先頭に付く。

Ruby
local_var = "ローカル"      # ローカル変数

class Counter
  @@count = 0               # クラス変数(全インスタンス共有)

  def initialize(name)
    @name = name            # インスタンス変数
    @@count += 1
  end

  def self.count = @@count  # クラスメソッド(Ruby 3.0+ の短縮形)
end

$debug = true               # グローバル変数(使用は最小限に)

Counter.new("a")
Counter.new("b")
p Counter.count             # => 2
2
クラス変数 @@ は継承ツリー全体で共有されるため、サブクラスで意図せず書き換わることがある。インスタンス変数 @class << self 内のインスタンス変数で代替するのが一般的。
定数(大文字始まり)基礎

大文字始まりの識別子は定数。再代入すると警告が出るが実行は止まらない。完全に凍結するには freeze を呼ぶ。

Ruby
MAX_RETRIES = 3
PI = 3.14159

# MAX_RETRIES = 5  # warning: already initialized constant MAX_RETRIES

FROZEN_LIST = [1, 2, 3].freeze
# FROZEN_LIST << 4  # FrozenError: can't modify frozen Array
freeze はオブジェクト自体を凍結するが、配列・ハッシュの**要素**は凍結しない(浅い凍結)。完全な不変性が必要なら要素ごとに freeze を呼ぶか deep_freeze 相当のロジックが必要。
基本型基礎

Ruby の主要な組み込み型。true/false は TrueClass/FalseClass の唯一のインスタンス。nil は NilClass のインスタンスで「値なし」を表す。

Ruby
42          # Integer(任意精度)
3.14        # Float
"hello"     # String
:name       # Symbol(不変・内部的にキャッシュ)
true        # TrueClass
false       # FalseClass
nil         # NilClass
[1, 2, 3]   # Array
{ a: 1 }    # Hash
型チェック(is_a? / kind_of? / class)基礎

is_a? / kind_of?(同義語)は継承・モジュール込みで判定する。instance_of? は厳密な型一致のみ。class は実際のクラスを返す。

Ruby
n = 42
p n.class          # => Integer
p n.is_a?(Integer) # => true
p n.is_a?(Numeric) # => true  ← 親クラスも true
p n.instance_of?(Numeric) # => false  ← 厳密一致のみ

p "hi".is_a?(Comparable) # => true  ← モジュールも true
Integer true true false true
02

制御フロー

基礎
if / elsif / else / unless基礎

if は末尾に end が必要。unless は「〜でなければ」を意味する否定形 if。どちらも式であり値を返す。

Ruby
score = 75

result = if score >= 90
  "S"
elsif score >= 70
  "A"
else
  "B"
end
p result   # => "A"

# unless(否定形)
puts "ゲスト" unless score >= 90
"A" ゲスト
case / when(パターンマッチ含む)基礎

case/when=== で比較する。Ruby 3.0 以降は case/in でパターンマッチが使える。

Ruby
# 値による分岐(=== を使用)
val = "hello"
case val
when String  then puts "文字列"
when Integer then puts "整数"
else              puts "その他"
end

# Ruby 3.0+ パターンマッチ
data = { name: "Alice", age: 30 }
case data
in { name: String => name, age: (18..) }
  puts "成人: #{name}"
end
文字列 成人: Alice
while / until / loop基礎

while は条件が真の間繰り返し、until は偽の間繰り返す。loop は無限ループで break で脱出する。

Ruby
i = 0
while i < 3
  print "#{i} "
  i += 1
end
# => 0 1 2

j = 3
until j == 0
  print "#{j} "
  j -= 1
end
# => 3 2 1

loop do
  puts "one shot"
  break
end
0 1 2 3 2 1 one shot
修飾子形式(後置 if / unless)基礎

式 if 条件 / 式 unless 条件 の形式で 1 行に書ける。ガード節としてメソッド冒頭の早期 return に多用する。

Ruby
puts "正" if 1 > 0
puts "偽ではない" unless false

def greet(name)
  return "名前が空です" if name.nil? || name.empty?
  "Hello, #{name}!"
end

p greet("")      # => "名前が空です"
p greet("Ruby")  # => "Hello, Ruby!"
正 偽ではない "名前が空です" "Hello, Ruby!"
03

メソッド

基礎
def と暗黙の return基礎

def でメソッドを定義する。Ruby では最後に評価した式の値が自動的に返値になる(暗黙の return)。明示的な return も使える。

Ruby
def add(a, b)
  a + b          # 最後の式が戻り値
end

def sign(n)
  return "正" if n > 0
  return "負" if n < 0
  "ゼロ"         # ← これが暗黙の return
end

p add(3, 4)   # => 7
p sign(-5)    # => "負"
7 "負"
デフォルト・キーワード・可変長引数基礎

Ruby はデフォルト引数・キーワード引数(name:)・スプラット演算子(*args/**kwargs)を組み合わせられる。

Ruby
def greet(name, greeting: "Hello")
  "#{greeting}, #{name}!"
end

def sum(*nums)
  nums.sum
end

def show(**opts)
  opts.each { |k, v| puts "#{k}: #{v}" }
end

p greet("Alice")                  # => "Hello, Alice!"
p greet("Bob", greeting: "Hi")    # => "Hi, Bob!"
p sum(1, 2, 3, 4)                 # => 10
show(lang: "Ruby", ver: "3.3")
"Hello, Alice!" "Hi, Bob!" 10 lang: Ruby ver: 3.3
メソッドの可視性(public / private / protected)基礎

private メソッドはクラス外から呼び出せない。protected は同クラスおよびサブクラスのインスタンスから呼べる(比較メソッドなどに使用)。

Ruby
class BankAccount
  def initialize(balance)
    @balance = balance
  end

  def >(other)
    balance > other.balance   # protected メソッドを呼べる
  end

  protected

  def balance = @balance

  private

  def secret_pin = "1234"
end

a = BankAccount.new(100)
b = BankAccount.new(200)
p a > b             # => false
# a.balance         # NoMethodError
# a.secret_pin      # NoMethodError
false
! メソッドと ? メソッドの慣習基礎

! で終わるメソッドは破壊的(レシーバ自体を変更)または例外を発生させる危険な版。? で終わるメソッドは真偽値を返す述語メソッド。

Ruby
s = "hello"
p s.upcase   # => "HELLO"(元は変わらない)
p s          # => "hello"
s.upcase!    # 破壊的
p s          # => "HELLO"

p [1, nil, 2, nil].compact          # => [1, 2](コピー)
p "".empty?   # => true
p nil.nil?    # => true
p 5.between?(1, 10)  # => true
"HELLO" "hello" "HELLO" [1, 2] true true true
04

文字列

基礎
文字列リテラル(ダブルクォートとシングルクォート)基礎

"..." はエスケープシーケンスと文字列補間が有効。'...' はほぼそのまま(\\'\\\\ のみ解釈)。

Ruby
name = "Ruby"
p "Hello, #{name}!"   # => "Hello, Ruby!"  ← 補間あり
p 'Hello, #{name}!'   # => "Hello, #{name}!"  ← 補間なし

p "改行:
次の行"
p '改行:
次の行'       # 
 はそのまま文字列
"Hello, Ruby!" "Hello, #{name}!" "改行: 次の行" "改行:\n次の行"
文字列補間(#{})基礎

#{...} の中には任意の Ruby 式を書ける。式の評価結果が to_s で文字列化されて埋め込まれる。

Ruby
x = 7
p "7 の 2 乗は #{x ** 2}"    # => "7 の 2 乗は 49"
p "現在時刻: #{Time.now}"
p "配列: #{[1,2,3].join(", ")}"
ヒアドキュメント(<<~HEREDOC)基礎

<<~HEREDOC はインデントを自動的に除去する(Ruby 2.3+)。SQL・HTML などの複数行テキストに便利。

Ruby
sql = <<~SQL
  SELECT *
  FROM users
  WHERE age > #{18}
SQL
puts sql
SELECT * FROM users WHERE age > 18
主な文字列メソッド基礎

String クラスには豊富なメソッドが揃っている。gsub は正規表現対応の全置換、% は printf 風フォーマット演算子。

Ruby
s = "  Hello, Ruby!  "
p s.strip          # => "Hello, Ruby!"
p s.upcase         # => "  HELLO, RUBY!  "
p s.gsub("Ruby", "World")  # => "  Hello, World!  "
p s.split(", ")    # => ["  Hello", "Ruby!  "]
p "%.2f" % 3.14159 # => "3.14"
p "Name: %s, Age: %d" % ["Alice", 30]
"Hello, Ruby!" " HELLO, RUBY! " " Hello, World! " [" Hello", "Ruby! "] "3.14" "Name: Alice, Age: 30"
05

配列とハッシュ

基礎
配列リテラルと %w[] / %i[]基礎

通常の配列リテラルのほか、%w[] で文字列配列、%i[] でシンボル配列を簡潔に作れる。

Ruby
nums   = [1, 2, 3]
words  = %w[apple banana cherry]   # => ["apple", "banana", "cherry"]
syms   = %i[red green blue]        # => [:red, :green, :blue]

p words[0]     # => "apple"
p nums[-1]     # => 3(後ろから)
p nums[1, 2]   # => [2, 3](start, length)
p nums[0..1]   # => [1, 2](範囲)
"apple" 3 [2, 3] [1, 2]
主な配列メソッド基礎

map は変換、select は絞り込み、reject は除外、reduce は畳み込み。これらは Enumerable モジュールが提供する。

Ruby
nums = [1, 2, 3, 4, 5]

p nums.map    { |n| n * 2 }        # => [2, 4, 6, 8, 10]
p nums.select { |n| n.odd? }       # => [1, 3, 5]
p nums.reject { |n| n.odd? }       # => [2, 4]
p nums.reduce(:+)                  # => 15
p [[1, [2]], [3]].flatten          # => [1, 2, 3]
p [1, 2].zip([3, 4])               # => [[1, 3], [2, 4]]
[2, 4, 6, 8, 10] [1, 3, 5] [2, 4] 15 [1, 2, 3] [[1, 3], [2, 4]]
ハッシュリテラルとシンボルキー基礎

シンボルキーには key: value の省略記法が使える(Ruby 1.9+)。fetch はキーが存在しない場合に例外またはデフォルト値を返す。

Ruby
user = { name: "Alice", age: 30, role: :admin }

p user[:name]              # => "Alice"
p user.fetch(:age)         # => 30
p user.fetch(:missing, 0)  # => 0(デフォルト値)

# ネストしたハッシュには dig
config = { db: { host: "localhost", port: 5432 } }
p config.dig(:db, :host)   # => "localhost"
p config.dig(:db, :pass)   # => nil(KeyErrorではない)
"Alice" 30 0 "localhost" nil
ハッシュ操作(merge / select / transform_values)基礎

merge は 2 つのハッシュを統合(後者が優先)。transform_values / transform_keys は値・キーを一括変換する。

Ruby
a = { x: 1, y: 2 }
b = { y: 99, z: 3 }

p a.merge(b)                           # => {x:1, y:99, z:3}
p a.select { |k, v| v > 1 }           # => {y:2}
p a.transform_values { |v| v * 10 }   # => {x:10, y:20}
p a.transform_keys { |k| k.to_s }     # => {"x"=>1, "y"=>2}
{:x=>1, :y=>99, :z=>3} {:y=>2} {:x=>10, :y=>20} {"x"=>1, "y"=>2}
06

例外処理

基礎
begin / rescue / ensure / else基礎

rescue で例外をキャッチ。ensure は例外の有無に関わらず必ず実行(クリーンアップ用)。else は例外が発生しなかった場合に実行。

Ruby
begin
  result = 10 / Integer(gets.chomp)
rescue ZeroDivisionError => e
  puts "ゼロ除算: #{e.message}"
rescue ArgumentError => e
  puts "不正な入力: #{e.message}"
else
  puts "結果: #{result}"
ensure
  puts "処理終了(必ず実行)"
end
raise / retry基礎

raise で例外を発生させる。rescue ブロック内で retry を呼ぶと begin から再試行できる。無限ループに注意してカウンタで制限する。

Ruby
attempts = 0
begin
  attempts += 1
  raise "一時的なエラー" if attempts < 3
  puts "成功(#{attempts} 回目)"
rescue RuntimeError => e
  puts "リトライ #{attempts}: #{e.message}"
  retry if attempts < 3
end
リトライ 1: 一時的なエラー リトライ 2: 一時的なエラー 成功(3 回目)
独自例外クラス基礎

StandardError を継承して独自例外を定義する。RuntimeError の継承でも良いが、ライブラリでは StandardError を推奨。

Ruby
class InsufficientFundsError < StandardError
  def initialize(amount, balance)
    super("残高不足: 引き出し額 #{amount}、残高 #{balance}")
    @amount  = amount
    @balance = balance
  end

  attr_reader :amount, :balance
end

begin
  raise InsufficientFundsError.new(500, 200)
rescue InsufficientFundsError => e
  puts e.message
  puts "不足額: #{e.amount - e.balance}"
end
残高不足: 引き出し額 500、残高 200 不足額: 300
メソッドに直接 rescue を書く基礎

メソッド全体を例外処理の対象にする場合は begin/end を省略して def 直後に rescue を書ける。

Ruby
def parse_int(str)
  Integer(str)
rescue ArgumentError
  nil
end

p parse_int("42")    # => 42
p parse_int("abc")   # => nil
42 nil
07

クラスとモジュール

基礎
class / initialize / attr_accessor基礎

initialize はコンストラクタ。attr_accessor は getter/setter を自動生成する。読み取り専用は attr_reader、書き込み専用は attr_writer

Ruby
class Person
  attr_accessor :name, :age
  attr_reader   :id

  @@next_id = 1

  def initialize(name, age)
    @name = name
    @age  = age
    @id   = @@next_id
    @@next_id += 1
  end

  def to_s = "#{@name}(#{@age})"
end

alice = Person.new("Alice", 30)
alice.age = 31
p alice.to_s   # => "Alice(31)"
p alice.id     # => 1
"Alice(31)" 1
継承(<)と super基礎

class Sub < Parent で継承。super は親クラスの同名メソッドを呼び出す。引数なしの super は全引数をそのまま転送する。

Ruby
class Animal
  def initialize(name)
    @name = name
  end

  def speak = "#{@name} が鳴く"
end

class Dog < Animal
  def speak = "#{super}: ワン!"  # super で親を呼ぶ
end

d = Dog.new("ポチ")
p d.speak          # => "ポチ が鳴く: ワン!"
p d.is_a?(Animal)  # => true
"ポチ が鳴く: ワン!" true
Module と include(Mixin)基礎

Ruby はクラスの多重継承を持たないが、moduleinclude による Mixin で複数の機能を合成できる。モジュールはインスタンス化できない。

Ruby
module Greetable
  def greet = "Hello, I'm #{name}"
end

module Farewell
  def bye = "Goodbye from #{name}"
end

class User
  include Greetable
  include Farewell
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

u = User.new("Bob")
p u.greet   # => "Hello, I'm Bob"
p u.bye     # => "Goodbye from Bob"
"Hello, I'm Bob" "Goodbye from Bob"
extend(クラスメソッドとして取り込み)基礎

include がインスタンスメソッドを追加するのに対し、extend はクラス(またはオブジェクト)のシングルトンクラスにメソッドを追加する。

Ruby
module ClassLogger
  def log(msg) = puts("[LOG] #{self}: #{msg}")
end

class Service
  extend ClassLogger
end

Service.log("起動")   # クラスメソッドとして使える

# 個別オブジェクトへの extend
obj = Object.new
obj.extend(ClassLogger)
obj.log("hello")
[LOG] Service: 起動 [LOG] #<Object:...>: hello
08

イテレータとブロック

基礎
each / map / select / times基礎

Ruby では for ループよりメソッドにブロックを渡す書き方が慣用的。数値には timesuptostep などが使える。

Ruby
[10, 20, 30].each   { |n| print "#{n} " }   # => 10 20 30
[10, 20, 30].map    { |n| n / 10 }          # => [1, 2, 3]
[1..5].first(5).select { |n| n.even? }      # => [2, 4]

3.times { |i| print "#{i} " }    # => 0 1 2
1.upto(5) { |i| print "#{i} " }  # => 1 2 3 4 5
(1..10).step(3) { |i| print "#{i} " }  # => 1 4 7 10
ブロック(do...end と {...})基礎

do...end{...} は同義だが優先順位が異なる。{}do...end より強く束縛される。複数行なら do...end、1行なら {} が慣習。

Ruby
# 1 行ブロックは {} が慣習
[1, 2, 3].map { |n| n ** 2 }

# 複数行は do...end が慣習
[1, 2, 3].each do |n|
  squared = n ** 2
  puts "#{n}^2 = #{squared}"
end

# 優先順位の違い(注意)
p [1,2].map { |n| n * 2 }  # p([1,2].map{...}) と解釈
# p [1,2].map do |n| n * 2 end  # (p [1,2]).map do...end と解釈されることがある
1^2 = 1 2^2 = 4 3^2 = 9
yield とブロックの渡し方基礎

yield で呼び出し側のブロックに制御を渡す。block_given? でブロックの有無を確認できる。&block 引数でブロックを Proc として受け取れる。

Ruby
def repeat(n)
  n.times { yield }
end

repeat(3) { print "Ruby! " }
# => Ruby! Ruby! Ruby!

def measure(&block)
  start = Time.now
  block.call
  puts "経過: #{Time.now - start}s"
end

measure { sleep(0.01) }
Proc.new / lambda / -> の違い基礎

Proc.newlambda/-> は引数チェックと return の挙動が異なる。lambda は引数が合わないと ArgumentErrorreturn はラムダ内のみで完結する。

Ruby
square   = ->(n) { n ** 2 }         # lambda(短縮形)
cube     = lambda { |n| n ** 3 }    # lambda(明示形)
greeting = Proc.new { |n| "Hi #{n}" }

p square.call(4)   # => 16
p cube.(3)         # => 27  (.() は .call() の糖衣)
p greeting.("Bob") # => "Hi Bob"

p square.lambda?   # => true
p greeting.lambda? # => false
16 27 "Hi Bob" true false
09

比較・等値・宇宙船演算子

基礎
== / equal? / eql?基礎

== は値の等値(オーバーライド可能)。equal? はオブジェクト同一性(object_id の比較)。eql? はハッシュキー比較に使われる(型も含めた厳密な等値)。

Ruby
a = "hello"
b = "hello"
c = a

p a == b       # => true   値が等しい
p a.equal?(b)  # => false  別オブジェクト
p a.equal?(c)  # => true   同じオブジェクト

p 1 == 1.0     # => true   数値として等しい
p 1.eql?(1.0)  # => false  型が違う
true false true true false
<=> と sort基礎

<=> は -1/0/1 を返す汎用比較演算子。sort はこれを使って並べ替える。sort_by は任意のキーでソートする際に便利。

Ruby
p 1 <=> 2    # => -1
p 2 <=> 2    # => 0
p 3 <=> 2    # => 1

words = %w[banana apple cherry]
p words.sort                          # => ["apple", "banana", "cherry"]
p words.sort_by { |w| w.length }      # => ["apple", "banana", "cherry"]

people = [{ name: "Bob", age: 30 }, { name: "Alice", age: 25 }]
p people.sort_by { |p| p[:age] }.map { |p| p[:name] }
-1 0 1 ["apple", "banana", "cherry"] ["apple", "banana", "cherry"] ["Alice", "Bob"]
Comparable モジュール基礎

Comparableinclude して <=> を実装すれば、<><=>=between?clamp が自動で使えるようになる。

Ruby
class Temperature
  include Comparable
  attr_reader :degrees

  def initialize(d) = (@degrees = d)
  def <=>(other) = degrees <=> other.degrees
end

temps = [30, 20, 25].map { |d| Temperature.new(d) }
sorted = temps.sort.map(&:degrees)
p sorted                             # => [20, 25, 30]

t = Temperature.new(22)
p t.between?(Temperature.new(20), Temperature.new(25))  # => true
[20, 25, 30] true
=== と case/when の関係基礎

case/when の内部では === が使われる。Range・Regexp・Proc などはそれぞれ意味のある === を実装している。

Ruby
# Range#=== は include? と同義
p (1..10) === 5       # => true

# Regexp#=== は =~ と同義
p /\d+/ === "abc123"  # => true

# Proc#=== は call と同義
even = ->(n) { n.even? }
p even === 4           # => true

# case/when で活用
def classify(x)
  case x
  when String   then "文字列"
  when (1..9)   then "1〜9"
  when Integer  then "その他整数"
  end
end

p classify("hi")  # => "文字列"
p classify(5)     # => "1〜9"
true true true "文字列" "1〜9"
10

Enumerable

基礎
Enumerable モジュールの概要基礎

Enumerableeach を実装したクラスに include するだけで 50 以上のコレクション操作メソッドを提供する。Array・Hash・Range などに組み込み済み。

Ruby
class NumberSet
  include Enumerable

  def initialize(*nums) = (@data = nums)

  def each(&block) = @data.each(&block)
end

ns = NumberSet.new(3, 1, 4, 1, 5, 9)
p ns.sort          # => [1, 1, 3, 4, 5, 9]
p ns.min           # => 1
p ns.max           # => 9
p ns.select(&:odd?) # => [3, 1, 1, 5, 9]
[1, 1, 3, 4, 5, 9] 1 9 [3, 1, 1, 5, 9]
each_with_object / each_with_index / each_slice / each_cons基礎

each_with_object は累積オブジェクトを持ち回る、each_with_index はインデックス付き反復、each_slice / each_cons はウィンドウ処理に使う。

Ruby
# each_with_object: 累積オブジェクト
result = [1, 2, 3].each_with_object({}) do |n, h|
  h[n] = n ** 2
end
p result    # => {1=>1, 2=>4, 3=>9}

# each_with_index: インデックス付き
%w[a b c].each_with_index { |v, i| puts "#{i}: #{v}" }

# each_slice: n 個ずつ
(1..6).each_slice(2) { |s| p s }    # [1,2] [3,4] [5,6]

# each_cons: スライドウィンドウ
(1..5).each_cons(3) { |c| p c }     # [1,2,3] [2,3,4] [3,4,5]
{1=>1, 2=>4, 3=>9} 0: a 1: b 2: c [1, 2] [3, 4] [5, 6] [1, 2, 3] [2, 3, 4] [3, 4, 5]
group_by / chunk / tally基礎

group_by はキーでグルーピング、chunk は連続する同値要素をまとめる、tally は Ruby 2.7+ で各要素の出現回数を数える。

Ruby
words = %w[one two three four five six]
p words.group_by { |w| w.length }
# => {3=>["one","two","six"], 4=>["four","five"], 5=>["three"]}

p [1, 1, 2, 2, 1, 3].chunk { |n| n }.map { |k, v| [k, v.size] }
# => [[1,2],[2,2],[1,1],[3,1]]

p %w[a b a a c b].tally
# => {"a"=>3, "b"=>2, "c"=>1}
lazy とチェーン基礎

lazy は遅延評価の Enumerator を返す。無限シーケンスに対して map/select を適用し、必要な分だけ firsttake で取り出せる。

Ruby
# 無限の自然数から偶数の先頭 5 つ
result = (1..Float::INFINITY)
           .lazy
           .select { |n| n.even? }
           .map    { |n| n ** 2 }
           .first(5)

p result   # => [4, 16, 36, 64, 100]
[4, 16, 36, 64, 100]
11

Proc・lambda・クロージャ

基礎
Proc と lambda の違い基礎

主な違いは 2 点。(1) 引数の数: lambda は厳密チェック、Proc は余剰・不足を許容。(2) return の挙動: lambda はラムダ内のみ、Proc は呼び出し元メソッドごとリターン。

Ruby
lam = lambda { |x, y| x + y }
prc = Proc.new { |x, y| [x, y].inspect }

p lam.call(1, 2)     # => 3
# lam.call(1)        # ArgumentError

p prc.call(1, 2)     # => "[1, 2]"
p prc.call(1)        # => "[1, nil]"  ← 不足は nil 扱い
p prc.call(1, 2, 3)  # => "[1, 2]"   ← 余剰は無視
3 "[1, 2]" "[1, nil]" "[1, 2]"
& 演算子(ブロック ⇔ Proc / メソッド参照)基礎

& でブロックを Proc に変換、または Proc/Method をブロックとして渡せる。method(:name) でメソッドを Method オブジェクトとして取り出せる。

Ruby
# Proc をブロックとして渡す
double = ->(n) { n * 2 }
p [1, 2, 3].map(&double)        # => [2, 4, 6]

# Symbol#to_proc を利用(&:メソッド名)
p ["hello", "world"].map(&:upcase)  # => ["HELLO", "WORLD"]
p [1, 2, 3].select(&:odd?)          # => [1, 3]

# method() でメソッド参照
p [1, -2, 3].map(&method(:puts))
[2, 4, 6] ["HELLO", "WORLD"] [1, 3]
カリー化(curry)基礎

curry で Proc/lambda をカリー化する。引数を 1 つずつ渡して部分適用でき、全引数が揃った時点で評価される。

Ruby
add = ->(a, b) { a + b }
add5 = add.curry.(5)   # 第1引数に 5 を固定

p add5.(3)    # => 8
p add5.(10)   # => 15

multiply = ->(a, b, c) { a * b * c }.curry
double_then = multiply.(2).(3)   # a=2, b=3 で固定
p double_then.(4)   # => 24
8 15 24
クロージャと変数キャプチャ基礎

ブロック・Proc・lambda はすべてクロージャ。定義された時点の変数スコープを「キャプチャ」し、後から参照・変更できる。

Ruby
def make_counter(start = 0)
  count = start
  inc   = -> { count += 1; count }
  dec   = -> { count -= 1; count }
  get   = -> { count }
  [inc, dec, get]
end

inc, dec, get = make_counter(10)
inc.()
inc.()
dec.()
p get.()   # => 11  (外の count を共有している)
11
12

ファイル・IO

基礎
File.open / read / write基礎

File.open にブロックを渡すとブロック終了時に自動でクローズする(ensure 不要)。File.read / File.write は一括読み書きの短縮形。

Ruby
# 書き込み
File.write("hello.txt", "Hello, Ruby!\n")

# 読み取り(全体)
content = File.read("hello.txt")
puts content   # => Hello, Ruby!

# 行ごとに読み取り(自動クローズ)
File.open("hello.txt") do |f|
  f.each_line { |line| puts line.chomp }
end

# 追記
File.open("hello.txt", "a") { |f| f.puts "2行目" }
IO クラスと $stdin / $stdout基礎

$stdin$stdout$stderr はグローバルな IO オブジェクト。テストやリダイレクトで差し替えられる。STDIN は定数でリダイレクト不可。

Ruby
# 標準出力への書き込み
$stdout.puts "標準出力"
$stderr.puts "エラー出力"

# StringIO でキャプチャ(テスト等)
require 'stringio'
buf = StringIO.new
$stdout = buf
puts "captured"
$stdout = STDOUT
p buf.string   # => "captured\n"
CSV・JSON ライブラリ基礎

csvjson は標準ライブラリ(gem 不要)。CSV.foreach は大きなファイルを行ごとにストリーム処理できる。

Ruby
require 'csv'
require 'json'

# JSON
data = { name: "Alice", scores: [90, 85] }
json_str = JSON.generate(data)
p JSON.parse(json_str, symbolize_names: true)
# => {name:"Alice", scores:[90,85]}

# CSV(文字列から)
csv_str = "name,age\nAlice,30\nBob,25"
CSV.parse(csv_str, headers: true) do |row|
  puts "#{row['name']}: #{row['age']}"
end
{:name=>"Alice", :scores=>[90, 85]} Alice: 30 Bob: 25
Dir と Pathname基礎

Dir.glob でファイルパターン検索、Pathname はパスを OOP 的に操作できるラッパー。FileUtils と組み合わせてディレクトリ操作に使う。

Ruby
require 'pathname'

# Dir.glob でファイル列挙
Dir.glob("**/*.rb").first(3).each { |f| puts f }

# Pathname で OOP 的なパス操作
base = Pathname.new("/tmp/myapp")
config = base / "config" / "app.yml"
p config.to_s          # => "/tmp/myapp/config/app.yml"
p config.dirname       # => #<Pathname:/tmp/myapp/config>
p config.extname       # => ".yml"
p config.exist?        # => false(ファイルがなければ)
13

正規表現

基礎
リテラル(/pattern/i)と Regexp.new基礎

正規表現は /pattern/ リテラルで書くのが基本。i で大小文字無視、m.が改行にもマッチx で空白・コメントを無視できる。

Ruby
p /hello/i === "Hello World"   # => true

# Regexp.new(動的パターン)
word = "ruby"
re = Regexp.new(word, Regexp::IGNORECASE)
p re === "I love Ruby"   # => true

# x オプションで可読性向上
tel = /
  \A        # 先頭
  (\d{2,4}) # 市外局番
  -
  (\d{2,4}) # 市内局番
  -
  (\d{4})   # 番号
  \z        # 末尾
/x
p tel.match?("03-1234-5678")   # => true
true true true
=~ / match / scan / gsub基礎

=~ はマッチした位置(整数)または nil を返す。match は MatchData、scan は全マッチを配列で返す。gsub はブロックで動的置換が可能。

Ruby
str = "Ruby 3.3 released in 2024"

p str =~ /\d+/          # => 5(最初のマッチ位置)
p str.match(/\d+/)[0]   # => "3"

p str.scan(/\d+/)        # => ["3", "3", "2024"]

# gsub でブロック置換
p str.gsub(/\d+/) { |m| "(#{m})" }
# => "Ruby (3).(3) released in (2024)"
5 "3" ["3", "3", "2024"] "Ruby (3).(3) released in (2024)"
キャプチャグループと $1, $2基礎

() でグループを作りキャプチャする。=~ でマッチ後は $1$2 などのグローバル変数でアクセスできる。match の戻り値からも取得可能。

Ruby
if "2024-05-13" =~ /(\d{4})-(\d{2})-(\d{2})/
  puts "年: #{$1}, 月: #{$2}, 日: #{$3}"
end

m = "John Doe".match(/(\w+)\s(\w+)/)
p m[0]   # => "John Doe"
p m[1]   # => "John"
p m[2]   # => "Doe"
年: 2024, 月: 05, 日: 13 "John Doe" "John" "Doe"
named captures((?<name>...))基礎

(?...) で名前付きキャプチャを定義できる。インデックスより意図が明確になる。match の MatchData から m[:name] でアクセス。

Ruby
pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
m = "2024-05-13".match(pattern)

p m[:year]   # => "2024"
p m[:month]  # => "05"
p m[:day]    # => "13"
p m.named_captures
# => {"year"=>"2024", "month"=>"05", "day"=>"13"}
"2024" "05" "13" {"year"=>"2024", "month"=>"05", "day"=>"13"}
14

標準ライブラリ・Gem

基礎
Bundler と Gemfile基礎

Bundler は gem の依存管理ツール。Gemfile に依存を記述し bundle installGemfile.lock を生成する。bundle exec でロックされたバージョンを使用。

Ruby
# Gemfile
source "https://rubygems.org"

ruby "3.3.0"

gem "rails",   "~> 7.1"
gem "pg",      "~> 1.5"
gem "puma",    "~> 6.0"

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
end
Rake(タスクランナー)基礎

Ruby の標準的なタスクランナー。Rakefiletask を定義し rake タスク名 で実行する。namespace でタスクをグループ化できる。

Ruby
# Rakefile
require 'rake'

desc "データベースをリセットする"
task :db_reset do
  puts "DB をリセット中..."
end

namespace :assets do
  desc "アセットをビルドする"
  task :build do
    puts "アセットをビルド中..."
  end
end

# $ rake db_reset
# $ rake assets:build
Net::HTTP と URI基礎

標準ライブラリの Net::HTTP で HTTP リクエストを送れる。単純な GET には Net::HTTP.get が便利。より複雑な用途には FaradayHTTParty gem が一般的。

Ruby
require 'net/http'
require 'uri'
require 'json'

uri = URI("https://api.github.com/users/ruby")
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  req = Net::HTTP::Get.new(uri)
  req["Accept"] = "application/json"
  http.request(req)
end

p response.code   # => "200"
user = JSON.parse(response.body)
p user["public_repos"]
pp / benchmark / optparse基礎

pp は構造をわかりやすく出力するデバッグ用メソッド(print pretty)。Benchmark は実行時間計測、OptionParser はコマンドライン引数解析に使う。

Ruby
require 'benchmark'
require 'optparse'

# Benchmark
Benchmark.bm(10) do |x|
  x.report("Array#sum:") { (1..100_000).to_a.sum }
  x.report("inject:   ") { (1..100_000).inject(:+) }
end

# OptionParser
options = {}
OptionParser.new do |opts|
  opts.on("-n NAME", "名前") { |v| options[:name] = v }
  opts.on("-v", "--verbose")  { options[:verbose] = true }
end.parse!(ARGV)
pp options
15

メタプログラミング

応用
method_missing と respond_to_missing?応用

method_missing は存在しないメソッドが呼ばれた際に呼び出される。必ず respond_to_missing? もオーバーライドして respond_to? の整合性を保つ。

Ruby
class DynamicProxy
  def initialize(target) = (@target = target)

  def method_missing(name, *args, &block)
    if @target.respond_to?(name)
      puts "[呼び出し] #{name}(#{args.join(', ')})"
      @target.send(name, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @target.respond_to?(name) || super
  end
end

proxy = DynamicProxy.new([1, 2, 3])
p proxy.length    # [呼び出し] length => 3
p proxy.respond_to?(:length)   # => true
[呼び出し] length() 3 true
method_missing は NoMethodError をキャッチするため、タイポによるエラーが隠れやすい。可能なら define_method で明示的に定義する方が安全。
define_method / class_eval / instance_eval応用

define_method は実行時にメソッドを動的に定義する。class_eval はクラスのコンテキストでコードを評価、instance_eval はオブジェクトのコンテキストで評価する。

Ruby
class Report
  %w[pdf csv html].each do |fmt|
    define_method("export_#{fmt}") do
      "Exporting as #{fmt.upcase}"
    end
  end
end

r = Report.new
p r.export_pdf   # => "Exporting as PDF"
p r.export_csv   # => "Exporting as CSV"

# class_eval で既存クラスに後付け
String.class_eval do
  def palindrome? = self == self.reverse
end

p "racecar".palindrome?   # => true
"Exporting as PDF" "Exporting as CSV" true
send / public_send / tap / then応用

send は private メソッドも呼べる動的ディスパッチ。public_send は public のみ。tap はデバッグや中間処理に、thenyield_self)はパイプライン的な変換に使う。

Ruby
"hello".send(:upcase)          # => "HELLO"
"hello".public_send(:upcase)   # => "HELLO"

# tap: 値を変えずに副作用(デバッグ等)
result = [1, 2, 3]
  .tap { |a| p "before: #{a}" }
  .map { |n| n * 2 }
  .tap { |a| p "after: #{a}" }

# then(yield_self): パイプライン変換
"  42  "
  .then { |s| s.strip }
  .then { |s| Integer(s) }
  .then { |n| n * 2 }
  .tap  { |n| p n }   # => 84
"before: [1, 2, 3]" "after: [2, 4, 6]" 84
Object#freeze / dup / clone応用

freeze でオブジェクトを凍結(変更不可)。dup は freeze を解除したコピーを作る(シャローコピー)。clone は freeze 状態を保持したコピーを作る。

Ruby
str = "hello".freeze
p str.frozen?       # => true

# str << " world"   # FrozenError

copy_dup   = str.dup
copy_clone = str.clone

p copy_dup.frozen?    # => false(freeze 解除)
p copy_clone.frozen?  # => true(freeze 保持)

copy_dup << " world"
p copy_dup   # => "hello world"
true false true "hello world"
16

Ruby の型とパフォーマンス

応用
Duck typing の哲学応用

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」。型ではなくメソッドの存在でオブジェクトを扱う。respond_to? で能力の確認が可能。

Ruby
def print_length(obj)
  if obj.respond_to?(:length)
    puts "長さ: #{obj.length}"
  else
    puts "length をサポートしていません"
  end
end

print_length("hello")    # => 長さ: 5
print_length([1, 2, 3])  # => 長さ: 3
print_length({ a: 1 })   # => 長さ: 1
print_length(42)          # => length をサポートしていません
長さ: 5 長さ: 3 長さ: 1 length をサポートしていません
RBS(型定義ファイル)と steep応用

Ruby 3.0 以降は RBS(Ruby Signature)で型定義を別ファイルに記述できる。steep ツールで静的型チェックを実施する。

Ruby
# person.rbs(型定義ファイル)
# class Person
#   attr_reader name: String
#   attr_reader age: Integer
#   def initialize: (name: String, age: Integer) -> void
#   def greet: () -> String
# end

# person.rb(実装)
class Person
  attr_reader :name, :age

  def initialize(name:, age:)
    @name = name
    @age  = age
  end

  def greet = "Hello, I'm #{name}!"
end

# $ steep check  で型エラーを検出
Frozen string literal応用

ファイル先頭に # frozen_string_literal: true を付けると全文字列リテラルが自動 freeze される。文字列オブジェクトの生成が減りメモリ効率が向上する。

Ruby
# frozen_string_literal: true

s = "hello"
p s.frozen?   # => true

# s << " world"  # FrozenError

# ミュータブルな文字列が必要なら String.new か +""
mutable = +"hello"
mutable << " world"
p mutable   # => "hello world"
true "hello world"
既存コードベースに後から付けると文字列への代入箇所が FrozenError になる可能性がある。新規ファイルに付けるのが安全。Rails 7 以降はデフォルトで有効。
ガベージコレクション(Incremental GC / Major・Minor GC)応用

Ruby の GC は世代別 GC(Minor: 若い世代、Major: 全世代)と Incremental GC(Ruby 2.2+)を採用。長いポーズを分割して STW(Stop-The-World)を最小化する。

Ruby
# GC の統計情報
p GC.stat.slice(:count, :major_gc_count, :minor_gc_count)

# 手動 GC(通常は不要)
GC.start

# ObjectSpace で生きているオブジェクト数を確認
require 'objspace'
p ObjectSpace.count_objects.slice(:T_STRING, :T_ARRAY, :T_HASH)

# GC.compact(Ruby 3.0+)でヒープをコンパクション
GC.compact
GC.compact はオブジェクトのアドレスが変わるため、C 拡張が Raw ポインタを保持していると問題が起きる場合がある。本番投入前に十分なテストを行うこと。
🏠