1
1

休日に新しい言語に触れたい ~Ruby編 特徴的なコードを実装してみる~

Last updated at Posted at 2024-09-15

はじめに

こちらの記事の続きです。

私はC#やPythonを仕事で使っているため、これらの知識をベースにして理解を広げます。同じような境遇の方の理解の助けになれば幸いです。

特徴的なコードを実装してみる

ブロック

rubyに特徴的なものとして、"ブロック"があります。

まずは、以下のサンプルコードを見てください。

numbers = [1, 2, 3, 4, 5]
numbers.each do |number|
  puts number * 2  # putsで出力
end

出力結果は以下で、numbersの各要素が2倍されたものが出力されています。
image.png

このようにdo |引数| ~endで囲まれたコードの部分がブロックです。今回の例では、numbersの各要素がこのブロックの引数numberに渡されています。

do~endの部分は、一行で{~}として書くこともできます

numbers = [1, 2, 3, 4, 5]
numbers.each { |number| puts number * 2 }
# 上の結果と同じく、2,4,6,8,10が出力される

ブロックを使ったメソッドを作り、理解を深める

まず、testというメソッドを作成しました。与えられた数字を3倍して出力するだけのメソッドです。

def test(number)
  puts number * 3
end

test(2)

testに引数2を渡したため、6が出力されました。
image.png

では、次にtestメソッドに変更を加えます。

def test(number)
  puts yield number if block_given? # 追加した行
  puts number * 3
end

test(2)

実行結果は"6"です。変更を加える前と出力結果は変わっていません。
image.png

さて、次にtestメソッドを呼び出すコードにブロックを加えます。

def test(number)
  puts yield number if block_given?
  puts number * 3
end

test(2) do |number| # ブロックを追加した。
  number * 2 # numberを2倍する
end

実行結果は、"4 6"となりました。
image.png

4が出力された原因は、メソッドに加えたputs yield number if block_given?の部分にあります。
if block_given : このメソッドにブロックが与えられたかどうかを判定する
yield number : 与えらえたブロックに、引数numberを渡して実行する。

今回は、numberを2倍にするブロックを渡していたため、"4"が出力されるようになっています。

ブロックのメリット

ブロックを使うことで、メソッドを使う側が独自の処理をメソッド内に入れ込むような設計が可能になります。上の例だったら、"2倍する"という独自の処理をtestメソッド内に入れ込んでいます。

Pythonの似たコードから理解を深める

Pythonだったら、lambdaで作った独自の関数で配列を並び替えることがあると思います。

# python
people = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35}
]

# ageキーでソートする
sorted_people = sorted(people, key=lambda person: person["age"])
print(sorted_people)
# => [{'name': 'Bob', 'age': 25}, {'name': 'Alice', 'age': 30}, {'name': 'Charlie', 'age': 35}]

rubyだったら、ブロックで同様のことができるようになります。

# ruby
people = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 }
]

# 年齢でソートする
sorted_people = people.sort_by { |person| person[:age] }
puts sorted_people.inspect
# => [{:name=>"Bob", :age=>25}, {:name=>"Alice", :age=>30}, {:name=>"Charlie", :age=>35}]

ミックスイン

rubyでは、クラスにモジュールのメソッドを取り込む"ミックスイン"という機能があります。
といっても分かりづらいので、以下の例をみてください。

class Person
  def initialize(name)
    @name = name
  end
end

taro = Person.new('Taro')

Personというクラスを作成し、taroというインスタンスを生成したコードです。
Personクラスは、コンストラクタでnameを引数にとり、自身のインスタンス変数(C#でいうところのプロパティに相当)としてnameを保持しています。

この時点では、Personはnameというデータを持っているだけで、メソッドは定義していません。

次に、Greetableというモジュールを作成し、greetというメソッドを定義します。
そして、このモジュールをPersonクラスでincludeします。

module Greetable
  def greet
    puts "Hello! I\'m #{@name}"
  end
end

class Person
  include Greetable # ミックスイン  

  def initialize(name)
    @name = name
  end
end

そうすると、Personクラスのインスタンスで"greet"メソッドが実行できるようになります。

taro = Person.new('Taro')
taro.greet

出力結果
image.png

これがミックスインです。

使いどころ

rubyでは、クラスの多重継承ができません。そこでミックスインを使います。

以下の例では、Personの派生クラスであるParent, Childを作成し、ParentのみにGreetableをミックスインしています。

module Greetable
  def greet
    puts "Hello! I'm #{@name}"
  end
end

class Person
  def initialize(name)
    @name = name
  end
end

# ParentクラスはPersonクラスの派生で、Greetableをミックスイン。挨拶できる。
class Parent < Person
  include Greetable
end

# ChildクラスはPersonクラスの派生で、Greetableをミックスインしない。挨拶できない。
class Child < Person
end

parent = Parent.new("Taro")
parent.greet # => Hello! I'm Taro.

child = Child.new("Kenji")
# child.greet # この行を実行するとエラー

オープンクラス

rubyでは、既存のクラスに新たなメソッドを追加/上書きすることができます。これを"オープンクラス"と呼びます。

以下の例では、文字列のStringクラスに対し、repeatというメソッドを追加しています。

class String
  def repeat
    self + self # 文字列を2回繰り返して返す
  end
end

puts 'Hello'.repeat

出力結果です。
image.png

DSLの実装

rubyでは、DSL(domain-specific language, ドメイン固有言語)を実装しやすいことが知られています。動的な型・メソッドの定義ができることや、メタプログラミング(動的なコードの操作)ができることが理由だそうです。

例として、ここでは人物の設定ファイルのようなDSLを作ります。

class Person
  def initialize(name)
    @name = name
    @age = nil
    @hometown = nil
  end

  def age(val)
    @age = val
  end

  def hometown(val)
    @hometown = val
  end

  def show_info
    "#{@name} => age:#{@age}, hometown:#{@hometown}"
  end
end

class PersonManager
  def initialize(&block) # blockを受け取る
    @persons = []
    instance_eval(&block) # blockをPersonManagerクラス内で実行する
  end

  def person(name, &block)
    person = Person.new(name)
    person.instance_eval(&block) # blockをPersonクラス内で実行する
    @persons << person # personsリストに追加
  end

  def puts_persons_info
    @persons.each { |person| puts person.show_info }
  end
end

上記のPersonManagerを使用することで、以下のようなDSLが記述できるようになります。見た目は、XMLやJSONのようなただの設定ファイルですね。

PersonManager.new do
  person 'Taro' do
    age 20
    hometown 'Tokyo'
  end

  person 'Hanako' do
    age 30
    hometown 'Kanagawa'
  end

  person 'Takeru' do
    age 35
    hometown 'Hokkaido'
  end

  puts_persons_info
end

実際には、それぞれのブロック内でいろいろなメソッドが実行されます。

PersonManager.new do 
# このdo~endのブロック内の内容は、instance_evalとして評価される。
# つまり、PersonManager内のメソッドが直書きできる。
  # PersonMangerのpersonメソッドに引数'Taro'を渡して実行
  person 'Taro' do  
  # このdo~endのブロック内の内容は、person.instance_evalとして評価される。
    # personクラスのage,hometownメソッドを実行
    age 20
    hometown 'Tokyo'
  end
end

出力結果です
image.png

感想

初めてDSLを作りましたが、確かに非プログラマでも記述できる設定ファイルを作れそうです。
設定ファイルを記述してもらう=>Rubyで処理してより複雑な設定ファイルを生成する=>他の言語がファイルを読み込みプログラムを実行、のような形で使える気がしました。

参考

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1