はじめに
こちらの記事の続きです。
私はC#やPythonを仕事で使っているため、これらの知識をベースにして理解を広げます。同じような境遇の方の理解の助けになれば幸いです。
特徴的なコードを実装してみる
ブロック
rubyに特徴的なものとして、"ブロック"があります。
まずは、以下のサンプルコードを見てください。
numbers = [1, 2, 3, 4, 5]
numbers.each do |number|
puts number * 2 # putsで出力
end
出力結果は以下で、numbersの各要素が2倍されたものが出力されています。
このように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メソッドに変更を加えます。
def test(number)
puts yield number if block_given? # 追加した行
puts number * 3
end
test(2)
実行結果は"6"です。変更を加える前と出力結果は変わっていません。
さて、次にtestメソッドを呼び出すコードにブロックを加えます。
def test(number)
puts yield number if block_given?
puts number * 3
end
test(2) do |number| # ブロックを追加した。
number * 2 # numberを2倍する
end
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
これがミックスインです。
使いどころ
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
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
感想
初めてDSLを作りましたが、確かに非プログラマでも記述できる設定ファイルを作れそうです。
設定ファイルを記述してもらう=>Rubyで処理してより複雑な設定ファイルを生成する=>他の言語がファイルを読み込みプログラムを実行、のような形で使える気がしました。
参考