💎

【Ruby 34日目】基本文法 - splat演算子

に公開

はじめに

Rubyのsplat演算子(***)について、Ruby 3.4の仕様に基づいて詳しく解説します。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

splat演算子は、配列やハッシュを展開・結合するための演算子です:

  • *(splat演算子) - 配列の展開や可変長引数の処理
  • **(double splat演算子) - ハッシュの展開やキーワード引数の処理
  • 引数での使用 - 可変長引数を受け取る
  • 呼び出しでの使用 - 配列やハッシュを展開して渡す

これらを使うことで、柔軟な引数処理やデータ構造の操作が可能になります。

基本的な使い方

配列の展開(*演算子)

# 配列を個別の引数として展開
def sum(a, b, c)
  a + b + c
end

numbers = [1, 2, 3]
puts sum(*numbers)  #=> 6

配列の結合

# 複数の配列を結合
arr1 = [1, 2, 3]
arr2 = [4, 5, 6]
arr3 = [7, 8, 9]

combined = [*arr1, *arr2, *arr3]
puts combined.inspect  #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 配列の途中に要素を挿入
result = [0, *arr1, 10]
puts result.inspect  #=> [0, 1, 2, 3, 10]

可変長引数の受け取り

def greet(greeting, *names)
  names.each do |name|
    puts "#{greeting}, #{name}!"
  end
end

greet("Hello", "Alice", "Bob", "Charlie")
#=> Hello, Alice!
#   Hello, Bob!
#   Hello, Charlie!

配列の分割代入

# 配列の最初と残りを分ける
first, *rest = [1, 2, 3, 4, 5]
puts first        #=> 1
puts rest.inspect #=> [2, 3, 4, 5]

# 配列の最後と残りを分ける
*beginning, last = [1, 2, 3, 4, 5]
puts beginning.inspect #=> [1, 2, 3, 4]
puts last              #=> 5

# 配列の中間を取り出す
first, *middle, last = [1, 2, 3, 4, 5]
puts first          #=> 1
puts middle.inspect #=> [2, 3, 4]
puts last           #=> 5

ハッシュの展開(**演算子)

# ハッシュをキーワード引数として展開
def create_user(name:, age:, country:)
  "#{name} (#{age}) from #{country}"
end

user_data = { name: "Alice", age: 25, country: "Japan" }
puts create_user(**user_data)  #=> Alice (25) from Japan

ハッシュのマージ

# 複数のハッシュをマージ
defaults = { host: "localhost", port: 3000, ssl: true }
custom = { port: 8080, timeout: 30 }

config = { **defaults, **custom }
puts config.inspect
#=> {:host=>"localhost", :port=>8080, :ssl=>true, :timeout=>30}

# ハッシュリテラル内で展開
user = { name: "Alice", age: 25 }
profile = { **user, country: "Japan", verified: true }
puts profile.inspect
#=> {:name=>"Alice", :age=>25, :country=>"Japan", :verified=>true}

可変長キーワード引数

def build_query(table:, **conditions)
  query = "SELECT * FROM #{table}"
  unless conditions.empty?
    where = conditions.map { |k, v| "#{k} = '#{v}'" }.join(" AND ")
    query += " WHERE #{where}"
  end
  query
end

puts build_query(table: "users", age: 25, country: "Japan")
#=> SELECT * FROM users WHERE age = '25' AND country = 'Japan'

よくあるユースケース

ケース1: メソッド引数の転送

引数をそのまま別のメソッドに転送します。

class Logger
  def log(level, *args, **kwargs)
    timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    message = format_message(*args)
    metadata = format_metadata(**kwargs)

    puts "[#{timestamp}] [#{level.upcase}] #{message} #{metadata}"
  end

  private

  def format_message(*parts)
    parts.join(" ")
  end

  def format_metadata(**data)
    return "" if data.empty?
    "| " + data.map { |k, v| "#{k}=#{v}" }.join(" ")
  end
end

logger = Logger.new
logger.log(:info, "User", "logged", "in", user_id: 123, ip: "192.168.1.1")
#=> [2025-11-10 10:00:00] [INFO] User logged in | user_id=123 ip=192.168.1.1

ケース2: 設定のマージ

デフォルト設定とカスタム設定をマージします。

class Application
  DEFAULT_CONFIG = {
    host: "localhost",
    port: 3000,
    ssl: false,
    timeout: 30,
    retry_count: 3
  }

  def initialize(**custom_config)
    @config = { **DEFAULT_CONFIG, **custom_config }
  end

  def start
    puts "Starting application with config:"
    @config.each { |key, value| puts "  #{key}: #{value}" }
  end
end

app = Application.new(port: 8080, ssl: true, debug: true)
app.start
#=> Starting application with config:
#     host: localhost
#     port: 8080
#     ssl: true
#     timeout: 30
#     retry_count: 3
#     debug: true

ケース3: 配列の動的な結合

複数のソースから配列を構築します。

class ReportBuilder
  def build_report(title, *sections)
    header = ["=" * 50, title, "=" * 50]
    footer = ["=" * 50, "End of Report", "=" * 50]

    report = [*header, "", *sections.flatten, "", *footer]
    report.join("\n")
  end
end

builder = ReportBuilder.new

section1 = ["Section 1:", "- Item A", "- Item B"]
section2 = ["Section 2:", "- Item C", "- Item D"]

puts builder.build_report("Monthly Report", section1, section2)
#=> ==================================================
#   Monthly Report
#   ==================================================
#
#   Section 1:
#   - Item A
#   - Item B
#   Section 2:
#   - Item C
#   - Item D
#
#   ==================================================
#   End of Report
#   ==================================================

ケース4: 関数の部分適用

引数の一部を固定した新しい関数を作成します。

class PartialFunction
  def initialize(method, *fixed_args, **fixed_kwargs)
    @method = method
    @fixed_args = fixed_args
    @fixed_kwargs = fixed_kwargs
  end

  def call(*args, **kwargs)
    all_args = [*@fixed_args, *args]
    all_kwargs = { **@fixed_kwargs, **kwargs }
    @method.call(*all_args, **all_kwargs)
  end
end

# 元の関数
multiply_and_add = ->(a, b, c:) { a * b + c }

# 部分適用:aを2に固定
double_and_add = PartialFunction.new(multiply_and_add, 2)

puts double_and_add.call(5, c: 10)  #=> 20 (2 * 5 + 10)
puts double_and_add.call(3, c: 7)   #=> 13 (2 * 3 + 7)

ケース5: REST APIのパラメータ処理

可変長のフィルター条件を処理します。

class APIClient
  def search(endpoint, required_param, *optional_filters, **query_params)
    url = "https://api.example.com/#{endpoint}"
    params = { q: required_param }

    # オプションフィルターを追加
    optional_filters.each_with_index do |filter, index|
      params["filter#{index + 1}"] = filter
    end

    # クエリパラメータをマージ
    params.merge!(**query_params)

    # URLを構築
    query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
    "#{url}?#{query_string}"
  end
end

client = APIClient.new

url = client.search(
  "products",
  "laptop",
  "electronics",
  "in-stock",
  limit: 10,
  sort: "price",
  order: "asc"
)

puts url
#=> https://api.example.com/products?q=laptop&filter1=electronics&filter2=in-stock&limit=10&sort=price&order=asc

注意点とベストプラクティス

注意点

  1. 配列とハッシュの展開は異なる演算子
# BAD: *を使ってハッシュを展開しようとする
def create_user(name:, age:)
  "#{name} (#{age})"
end

user_data = { name: "Alice", age: 25 }
# create_user(*user_data)  # ArgumentError

# GOOD: **を使う
puts create_user(**user_data)  #=> Alice (25)
  1. splat演算子の位置
# 可変長引数は最後の位置引数の前に配置できる
def method1(a, *b, c)
  "a=#{a}, b=#{b.inspect}, c=#{c}"
end

puts method1(1, 2, 3, 4, 5)  #=> a=1, b=[2, 3, 4], c=5

# キーワード引数と組み合わせる場合
def method2(a, *args, key: "default", **kwargs)
  "a=#{a}, args=#{args.inspect}, key=#{key}, kwargs=#{kwargs.inspect}"
end

puts method2(1, 2, 3, key: "value", extra: "data")
#=> a=1, args=[2, 3], key=value, kwargs={:extra=>"data"}
  1. nilの扱い
# nilを展開すると空配列として扱われる
arr = nil
result = [1, 2, *arr, 3]
puts result.inspect  #=> [1, 2, 3]

# ハッシュも同様
hash = nil
merged = { a: 1, **hash, b: 2 }
puts merged.inspect  #=> {:a=>1, :b=>2}

ベストプラクティス

  1. 引数転送には ... を使用(Ruby 2.7+)
# GOOD: Ruby 2.7以降では引数転送演算子を使う
def wrapper(...)
  actual_method(...)
end

def actual_method(a, b, c:)
  "a=#{a}, b=#{b}, c=#{c}"
end

puts wrapper(1, 2, c: 3)  #=> a=1, b=2, c=3
  1. 配列の結合は明示的に
# GOOD: 意図が明確
arrays = [[1, 2], [3, 4], [5, 6]]
combined = arrays.flatten
puts combined.inspect  #=> [1, 2, 3, 4, 5, 6]

# ALSO GOOD: splatを使った方法
combined = [*arrays[0], *arrays[1], *arrays[2]]
puts combined.inspect  #=> [1, 2, 3, 4, 5, 6]
  1. ハッシュのマージは優先順位を意識
# 後に指定したハッシュの値が優先される
defaults = { a: 1, b: 2, c: 3 }
custom = { b: 20, d: 4 }

# customの値が優先
config = { **defaults, **custom }
puts config.inspect  #=> {:a=>1, :b=>20, :c=>3, :d=>4}

# defaultsの値が優先
config = { **custom, **defaults }
puts config.inspect  #=> {:b=>2, :d=>4, :a=>1, :c=>3}

Ruby 3.4での改善点

  • Prismパーサーによる最適化 - splat演算子の解析が高速化
  • YJITの最適化 - splat演算子を使った配列・ハッシュ操作のパフォーマンス向上
  • 引数転送(...)の安定性向上 - Ruby 2.7で導入された引数転送がより安定的に
  • エラーメッセージの改善 - splat演算子の誤用時のエラーメッセージがより詳細に

まとめ

この記事では、splat演算子について以下の内容を学びました:

  • 基本概念と重要性 - ***の違い、配列とハッシュの展開
  • 基本的な使い方と構文 - 引数の展開、分割代入、マージ操作
  • 実践的なユースケース - 引数転送、設定マージ、配列結合、部分適用、APIパラメータ処理
  • 注意点とベストプラクティス - 演算子の使い分け、引数転送演算子、マージの優先順位

splat演算子を適切に使用することで、柔軟で簡潔なコードを書くことができます。

参考資料

Discussion