Skip to main content

Writing DSL

Basic syntax of Ruby

Use the REPL to get familiar with Ruby syntax.

Hash

Hash is equivalent to the Dictionary type in GDScript.

There are two ways to write a Symbol as a key.

{ foo: true, bar: false }

{ :foo => true, :bar => false }

The results is the same either way.

Output
=> { &"foo": true, &"bar": false }

A Hash can use keys other than Symbols.

{ 'foo' => true, 1 => 'bar', [:buz] => { 1.0 => 2.0 } }
Output
=> { "foo": true, 1: "bar", [&"buz"]: { 1.0: 2.0 } }

Method arguments

In Ruby, keyword arguments can be passed without parentheses () or curly braces {}. All of the following examples produce the same result.

Alice says: 'Hello Ruby! ❤'

Alice(says: 'Hello Ruby! ❤')

Alice({ says: 'Hello Ruby! ❤' })
Output
[ Alice ] method_missing: [{ &"says": "Hello Ruby! ❤" }]

When defining a method that accepts an arbitrary number of arguments, you can write it as follows:

  • To collect arguments into an Array, use a single asterisk before the parameter name (e.g. *args)
  • To collect keyword arguments into a Hash, use two asterisks before the parameter name (e.g. **kwargs)
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'
Output
[ arg ] signal emitted: 1

[ args ] signal emitted: [2, 3]

[ kwargs ] signal emitted: { &"foo": true, &"bar": "buz" }

Block

Blocks are a distinctive feature of Ruby. You can pass a block—a special kind of argument—to a method using either curly braces {} or the do ... end syntax.

You can evaluate a block passed to a method by using yield.

def hello
result = yield
"Hello #{result}"
end

def world!
'world!'
end

hello { world! }
Output
=> "Hello world!"

To receive a Block as a parameter, prefix the variable name with an ampersand, like &block.

@block = nil
def run(&block)
@block = block
end

run { block_called! }
# or
run do
block_called!
end
Output
=> "#<Proc:0x1ee61f63fc0 -:->"

In Ruby, a Block passed to a method becomes a procedural object known as an instance of the Proc class.

Blocks are commonly used in DSLs because they offer the following characteristics:

  • They are not evaluated at the time of method invocation
  • They can be stored in variables
  • Unlike other languages that require explicit constructs like function to define behavior, Ruby allows concise and implicit block syntax.

To execute a Proc object, use the following pattern:

@block.call
# or
@block.()
# or
instance_exec(&@block)
Output
[ block_called! ] method_missing: []

instance_exec evaluates a Proc object in the context of the instance on which the method is called—known as the receiver.

class Foo
def block_called!
1
end
end

foo = Foo.new
foo.instance_exec(&@block)
Output
=> 1

self

In Ruby, when the receiver of a method call is omitted, self is used implicitly. It's important to always be aware of what self refers to in a given context.

class Foo
def self.whoami
self
end

def whoami
self
end
end

[self, Foo.whoami, Foo.new.whoami]
Output
=> [main, Foo, #<Foo:0x1ee25c4fda0>]

To define class methods and class variables, you can use 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
Output
=> "#<Actor:0x1ab31c75d90 @name="Alice">"

=> "Hello! I'm Alice"

method_missing

All values in Ruby are objects, and every object is an instance of a class. Even true, false, and nil belong to specific classes.

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]

The diagram below shows the parent-child relationships among major Ruby classes. All objects ultimately inherit from BasicObject.

You can use methods to retrieve all public and protected methods available to an object.

1.methods # => [truncate, tap, hash, !, %, upto, ===, <=>, &, +, to_r, ...]

When a method is called on an object and is not defined in its class, Ruby searches up the class hierarchy to find a metching method.

In the method is not found even after reaching BasicObject, Ruby normally raises a NoMethodError. However, in ReDScribe, this exception is intercepted by defining a custom method_missing method. Instead of raising an error, it emits a method_missing signal.

While method_missing can be risky, it provides powerful metaprogramming capablities.

For example, it can be used like this:

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.億円
Output
=> "二〇二五年七月一九日"

=> "五兆三〇〇億円"

method_missing is especially useful when it's difficult or inefficient to predefine many individual methods.

const_missing

In Ruby, method_missing is used when a method can't be found, and const_missing is used when a constant is missing.

Identifiers starting with an uppercase letter (A–Z) and without arguments are interpreted as constants rather than methods. Therefore, to define such names dynamically, you need to use const_missing.

Alice # const_missing
def Object.const_missing(name)
"#{name} was called"
end

Alice
Output
=> "Alice was called"

Object#extend and Module#include

To add methods to an Object, use extend. To add instance methods to a Module or Class, use 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!
Output
=> "Bar hello!"

[ yeah! ] method_missing: []
=> <null>
Bar.new.hello!
Bar.new.yeah!
Output
[ hello! ] method_missing: []
=> <null>

=> "#<Bar:0x1ee25c4c650> yeah!"

Module#prepend

To override methods in Ruby's built-in classes, use prepend.

Unlike include, prepend adds the module to the beginning of the inheritance chain, allowing methods to be overridden.

''.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
Output
=> 0

=> '1!'

Operator Overloading

Ruby also supports operator overloading.

: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
Output
=> "#<SymbolChain:0x1ab31c765a0 @chain=[:first, :second, :third]>"

=> "I love Ruby"

Basic DSL Example

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
Output
[ walk ] signal emitted: { &"name": "Alice" }
[ walk ] signal emitted: { &"name": "Alice" }
[ walk ] signal emitted: { &"name": "Alice" }
[ jump ] signal emitted: { &"name": "Alice" }