DSL を書く
Ruby の基本的な文法を確認する
REPL を使って、Ruby の文法を把握しましょう。
Hash
Hash は、GDScript では Dictionary 型に相当します。 Symbol をキーにする場合の書き方は2通りあります。
{ foo: true, bar: false }
{ :foo => true, :bar => false }
どちらも結果は同じです。
=> { &"foo": true, &"bar": false }
Symbol 以外もキーにできます。
{ 'foo' => true, 1 => 'bar', [:buz] => { 1.0 => 2.0 } }
=> { "foo": true, 1: "bar", [&"buz"]: { 1.0: 2.0 } }
引数
Ruby では、メソッドに渡すキーワード引数には、()
だけではなく {}
も省略して渡せます。
以下はすべて同じ結果になります。
Alice says: 'Hello Ruby! ❤'
Alice(says: 'Hello Ruby! ❤')
Alice({ says: 'Hello Ruby! ❤' })
[ Alice ] method_missing: [{ &"says": "Hello Ruby! ❤" }]
メソッドを定義して、任意の数の引数を受け取りたい場合は以下のように書きます。
- Array クラスとして展開する場合は
*args
のようにアスタリスクを引数名の前に1つ - Hash クラスとして展開する場合は
**kwargs
のようにアスタリスクを引数名の前に2つ
def foo(arg, *args, **kwargs)
Godot.emit_signal :arg, arg
Godot.emit_signal :args, args
Godot.emit_signal :kwargs, kwargs
end
foo 1, 2, 3, foo: true, :bar => 'buz'
[ arg ] signal emitted: 1
[ args ] signal emitted: [2, 3]
[ kwargs ] signal emitted: { &"foo": true, &"bar": "buz" }
ブロック
ブロックは Ruby の特徴的な機能です。{}
もしくは do ... end
でブロックと呼ばれる特殊な引数をメソッドに渡すことが可能です。
ブロックで渡されたものは yield
で評価することができます。
def hello
result = yield
"Hello #{result}"
end
def world!
'world!'
end
hello { world! }
=> "Hello world!"
ブロックを引数として受け取る際は、&block
のようにアンパサンドを引数名の前に書きます。
@block = nil
def run(&block)
@block = block
end
run { block_called! }
# or
run do
block_called!
end
=> "#<Proc:0x1ee61f63fc0 -:->"
ブロックで渡された引数は Proc クラスと呼ばれる手続き型になります。
ブロックは以下の特徴を持つため、DSL でよく利用されます。
- メソッド呼び出し時には評価されない
- 変数に格納することができる
- 他言語で同等の機能を使う場合に
function
などプログラミング言語特有の名前を使わないといけないが、Ruby だと書かなくてよい
Proc クラスを評価するには以下のようにします。
@block.call
# or
@block.()
# or
instance_exec(&@block)
[ block_called! ] method_missing: []
instance_exec
は、Proc クラスの手続きを、そのメソッドが呼ばれたインスタンス(レシーバという)で評価します。
class Foo
def block_called!
1
end
end
foo = Foo.new
foo.instance_exec(&@block)
=> 1
self
Ruby はメソッド呼び出し時のレシーバを省略した場合は、self
が暗黙のレシーバになります。常に self
が何を指しているか意識することが重要になります。
class Foo
def self.whoami
self
end
def whoami
self
end
end
[self, Foo.whoami, Foo.new.whoami]
=> [main, Foo, #<Foo:0x1ee25c4fda0>]
クラスメソッド、クラス変数を定義する際は class << self
を使って定義することができます。
class Actor
class << self
attr_accessor :current
def greeting
"Hello! I'm #{current.name}"
end
end
attr_accessor :name
def initialize(name)
self.name = name
end
end
Actor.current = Actor.new('Alice')
Actor.greeting
=> "#<Actor:0x1ab31c75d90 @name="Alice">"
=> "Hello! I'm Alice"
method_missing
Ruby のオブジェクトは必ず何かしらのクラスに属しています。true
や false
, nil
であっても例外ではありません。
self.class # => Object
self.class.ancestors # => [Object, Kernel, BasicObject]
Foo.class # => Class
Foo.class.ancestors # => [Class, Module, Object, Kernel, BasicObject]
Foo.new.class # => Foo
Foo.new.class.ancestors # => [Foo, Object, Kernel, BasicObject]
true.class # => TrueClass
true.class.ancestors # => [TrueClass, Object, Kernel, BasicObject]
false.class # => FalseClass
false.class.ancestors # => [FalseClass, Object, Kernel, BasicObject]
nil.class # => NilClass
nil.class.ancestors # => [NilClass, Object, Kernel, BasicObject]
主なクラスの親子関係の樹形図を以下に示します。すべてのオブジェクトは元を辿れば BasicObject
を親に持ちます。
あるオブジェクトが呼び出せるすべての public および protected メソッドは methods
で取得できます。
1.methods # => [truncate, tap, hash, !, %, upto, ===, <=>, &, +, to_r, ...]
メソッド呼び出し時に自身のクラスにメソッドが定義されていない場合、Ruby はそのオブジェクトのクラス階層を順に探索します。
BasicObject
まで探索して存在しないメソッドだった場合、本来 NoMethodError
という例外が発生しますが、ReDScribe では method_missing
メソッドを定義して例外をフックしているため例外は発生しません。代わりに method_missing シグナルを発行するようにしています。
この method_missing
は、危険ですがとても強力なメタプログラミングの力を与えます。
例えば、以下のように利用することができます。
module IntegerExt
def to_kanji
chars = %w(〇 一 二 三 四 五 六 七 八 九)
to_s.split('').map{|i| chars[i.to_i] }.join
end
def method_missing(name, *args)
"#{to_kanji}#{name}#{args.map(&:to_s).join}"
end
end
Integer.prepend IntegerExt
2025.年 7.月 19.日
5.兆 300.億円
=> "二〇二五年七月一九日"
=> "五兆三〇〇億円"
method_missing
を使うメリットがあるのは、多くのメソッドを予め定義するのが難しい場合などです。
const_missing
メソッドが見つからない場合は method_missing
を使いましたが、定数が見つからない場合は const_missing
を使います。
Ruby は英字大文字で始まり引数が無い場合はメソッドではなくは定数として判断するため、名前の先頭が大文字(A-Z)のものを動的に定義するには const_missing
を使う必要があります。
Alice # const_missing
def Object.const_missing(name)
"#{name} was called"
end
Alice
=> "Alice was called"
Object#extend と Module#include
Object
にメソッドを追加する場合は extend
を使います。
Module
および Class
にインスタンスメソッドを追加する場合は include
を使います。
module Hello
def hello!
self.to_s + ' hello!'
end
end
module Yeah
def yeah!
self.to_s + ' yeah!'
end
end
class Bar
extend Hello
include Yeah
end
Bar.hello!
Bar.yeah!
=> "Bar hello!"
[ yeah! ] method_missing: []
=> <null>
Bar.new.hello!
Bar.new.yeah!
[ hello! ] method_missing: []
=> <null>
=> "#<Bar:0x1ee25c4c650> yeah!"
Module#prepend
Ruby の組み込みクラスのメソッドをオーバーライドするには、prepend
を使います。
prepend
は、include
とは違って継承ツリーの手前に追加されるため、メソッドを上書きすることができます。
''.to_i # => 0
1.to_s # => '1'
module Helper
def to_i
super + 100
end
def to_s
super + '!'
end
end
String.include Helper
Integer.prepend Helper
''.class.ancestors # => [String, Helper, Comperable, ...]
1.class.ancestors # => [Helper, Integer, Numeric, ...]
''.to_i
1.to_s
=> 0
=> '1!'
演算子オーバーロード
Ruby では、演算子オーバーロードもできます。
:first > :second # => false
:first > :second > :third # method_missing
- :Ruby # method_missing
class SymbolChain
attr_accessor :chain
def initialize(origin)
self.chain = [origin]
end
def add(sym)
tap { self.chain << sym }
end
alias_method :>, :add
end
module SymbolExt
def >(other)
SymbolChain.new(self).add(other)
end
def -@
"I love #{self}"
end
end
Symbol.prepend SymbolExt
:first > :second > :third
- :Ruby
=> "#<SymbolChain:0x1ab31c765a0 @chain=[:first, :second, :third]>"
=> "I love Ruby"
DSL の基本例
class Player
attr_accessor :name
def initialize(name)
self.name = name
end
def walk
emit :walk
end
def jump
emit :jump
end
private
def emit(key)
Godot.emit_signal key, { name: name }
end
end
def player(name, &block)
Player.new(name).instance_exec(&block)
end
player 'Alice' do
3.times { walk }
jump
end
[ walk ] signal emitted: { &"name": "Alice" }
[ walk ] signal emitted: { &"name": "Alice" }
[ walk ] signal emitted: { &"name": "Alice" }
[ jump ] signal emitted: { &"name": "Alice" }