パズルで学ぶRuby
Agenda
- 自己紹介
- Rubyパズル
- 問題紹介
- 解答と解説
自己紹介
Name
廣江 亮佑
所属
株式会社リゾーム
@buta_botti
RubyKaigi2020参加!
RubyKaigi2020
Date: 4月9日~4月11日
Location: 長野県松本市
岡山近辺から参加される方いましたら
お声がけください。
コロナウイルスで中止になるかも...😢
Rubyパズル
コードパズルの1種です。
このRubyパズルは
コードをできるだけ少なく追加して
"Hello world\n"
を出力させるのが目的です。
去年のRubyKaigi2019で
Cookpadさんが作成しました。
問題はこちら
以下に問題が掲載されています。
Compassの発表概要にもリンクがあります。
簡単な問題を一緒に解きましょう
リンク先にはExtra含め全部で12問ありますが、その中でも比較的簡単なものを解いてみましょう。
短時間で自力で解くというのは難しいかもしれないので、少しずつヒントを出します。
1問につき5分程度経過したら答え合わせをします。
1-1
# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"
終端なしRange
終わりがないRange (1..) が書けるようになりました。始点から無限大までのような範囲を直観的に表現できるようになります。
https://www.ruby-lang.org/ja/news/2018/12/25/ruby-2-6-0-released/
文字列のRangeも生成可能であるので、以下のような式が評価されるようになる。
'Goodbye'..
文字列中の式展開
ダブルクォート(")で囲まれた文字列式、コマンド文字列および正規表現の中では#{式}という形式で式の内容(を文字列化したもの)を埋め込むことができます。
https://docs.ruby-lang.org/ja/2.6.0/doc/spec=2fliteral.html#exp
#{} 内で式を区切った場合、最後の評価された式の結果が文字列となって埋め込まれる。
"#{'Hello'
'World'}"
#=> "World"
式の区切り
式と式の間はセミコロン(;)または改行で区切ります。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fprogram.html
def say
'Goodbye'; 'Hello'
end
p say
#=> 'Hello'
解答
# Hint: Use Ruby 2.6.
puts "#{"Goodbye" ..; "Hello"} world"
#{} 内の "Goodbye" .. "Hello" の間に ; を追加し、式を分ける。こうすることで、#{} 内で最後に評価される式が "Hello" となる。 そして "Hello world" を引数に puts が実行され、Hello World が出力される。
tips: 終端なしRangeの終端
1..
3
#=> 1..3
(1..) == (1..nil) #=> ture
1.. == 1..nil #=> false..
# (1.. == 1)..nil で評価されてた!
num = 1..
num.to_s #=> bad value for range
Ruby2.7では始端なしRangeも
# Ruby2.7
..3 == (nil .. 3) #=> true
nil_range = nil .. nil #=> nil..nil
['string', :symbol, 1, ture, Float::INFINITY]
.all?(&nil_range.method(:cover?))
#=> true
# ちなみに始端と終端両方とも省略することはできません。
(..) #SyntaxError
# # ruby2.6 以前
nil_range = nil .. nil
(nil .. nil).size #=> nil
['string', :symbol, 1, ture, Float::INFINITY]
.any?(&space.method(:cover?))
#=> false
1-2
puts&.then {
# Hint: &. is a safe
# navigation operator.
"Hello world"
}
Kernel.#puts
puts(*arg) -> nil
https://docs.ruby-lang.org/ja/latest/method/Kernel/m/puts.html
戻り値は必ず nil 。また、引数が文字リテラルやシンボル、インスタンス変数やグローバル変数の場合、スペースを置かなくても引数と認識される。
$greeting = :Hello
puts$greeting # Syntax OK
# Hello
#=> nil
Object#then
self を引数としてブロックを評価し、ブロックの結果を返します。
https://docs.ruby-lang.org/ja/latest/method/Object/i/then.html
nil に 対してもメソッド呼び出しが可能。
p nil.then { "Hello world" }
#=> "Hello world"
ぼっち演算子
object&.foo という形式のメソッド呼び出し形式が追加されました。これは object が nil でないときにメソッド foo を呼び出します。
https://docs.ruby-lang.org/ja/latest/doc/news=2f2_3_0.html
ぼっち演算子付きのメソッド呼び出しの時、レシーバが nil の場合は戻り値もそのまま nil となる。
グローバル変数
グローバル変数には Ruby 処理系によって特殊な意味を与えられているものがあります。これらを組み込み変数と呼びます。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#global
$ から始まる変数のことをグローバル変数と言います。その中に一部、特殊変数と呼ばれる組み込み変数もある。インスタンス変数やグローバル変数は、未初期化状態で呼び出した場合 nil を返す。
解答
&. の前に $ を追加します。組み込み変数 $& を利用して puts と then を切り離す。$& は最後に成功した正規表現のパターンマッチでマッチした文字列が入る組み込み変数だが、これまで何もマッチさせていないので nil 。nil.then { "Hello world" } の戻り値は "Hello world" なのでそのまま "Hello world" が出力される。
puts$&.then {
# Hint: &. is a safe
# navigation operator.
"Hello world"
}
tips: &. と Object#try
&. は ActiveSupport の Object#try と違い、 レシーバが nil 以外できちんと例外を投げます。
# Object#try
:foo.try(:+, 1) #=> nil
# safe navigation operator
:foo&.+(1) #=> NoMethodError
tips: Object#then の使い道
存在チェックをして引数に渡すようなコードをすっきりと書けます。
value_or_nil.present? ? foo(value_or_nil) : nil
value_or_nil&.then(&method(:foo))
1-3
include Math
# Hint: the most beautiful equation
Out, *,
Count = $>,
$<, E ** (2 * PI)
Out.puts("Hello world" *
Count.abs.round)
実際に解きません。問題紹介のみ。
スライドにはヒントと解答を載せているので、気になる方はあとで確認してください。
ポイント
- オイラーの等式
- Mathモジュール
- Complex.#rect
- Numeric#abs, #round
オイラーの等式
ネイピア数e、虚数単位i、円周率πの間に成り立つ解析学における等式のことである。
つまり...
Mathモジュール
Math モジュールにはさまざま数学関数がモジュール関数として定義されています。
数学関数の他に数学的な定数が使用可能になる。( = E, = PI )
E #=> 2.718281828459045
PI #=> 3.141592653589793
Complex.#rect
オイラーの公式には虚数 i が使用されている。Complex.#rect で虚数を表現するためのオブジェクトを生成することができる
rect(r, i = 0) -> Complex
実部が r、虚部が i である Complex クラスのオブジェクトを生成します。
https://docs.ruby-lang.org/ja/latest/method/Complex/s/rect.html
Complex.rect(0, 2)
#=> (0+2i)
Numeric#abs, #round
自身の絶対値を返します。
https://docs.ruby-lang.org/ja/latest/method/Numeric/i/abs.html
自身ともっとも近い整数を返します。
https://docs.ruby-lang.org/ja/latest/method/Numeric/i/round.html
1.abs #=> 1
-1.abs #=> 1
1.2.round #=> 1
1.5.round #=> 2
理解しづらい箇所
ここは以下のように書き換えてみると理解しやすい。
Out, *,
Count = $>,
$<, E ** (2 * PI)
Out = $>
* = $<
Count = E ** (2 * PI)
「*」は実は変数名です。
解答
オイラーの等式を利用し、Count の値がおよそ -1 を返すようにする。およそ -1 であれば出力時の Count.abs.round で整数の 1 が返ってくるようになる。虚数部分は Complex.rect(0, 2) の SyntaxSuger である 2i を用いることで1文字解答が可能。
include Math
# Hint: the most beautiful equation
Out, *,
Count = $>,
$<, E ** (2i * PI)
Out.puts("Hello world" *
Count.abs.round)
2-1
def say
-> {
"Hello world"
}
# Hint: You should call the Proc.
yield
end
puts say { "Goodbye world" }
ブロック付きメソッド呼び出し
do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。
yield
自分で定義したブロック付きメソッドでブロックを呼び出すときに使います。
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#block
def greeting
yield
end
greeting { "Hello" } #=> "Hello"
Proc#yield
proc { 'Hello' }.yield
#=> "Hello"
解答
"Hello world" の手続きオブジェクトの後ろに . をつけることで、次の行の yield をブロック呼び出しでなく、直前の手続きオブジェクトの実行(Proc#yield)に変えることができます。
def say
-> {
"Hello world"
}.
# Hint: You should call the Proc.
yield
end
puts say { "Goodbye world" }
tips: proc と lambda
proc と lambda どちらも Proc オブジェクトですが、性質は若干異なります。
# proc
Proc.new { |x| x }
proc { |x| x }
# lambda
lambda { |x| x }
->(x) { x }
ブロックパラメータの厳密さ
proc はブロックパラメータが足りないところには nil が入ったり、入りきらないところは無視してくれたり、配列を渡すと引数が足りない時に自動で展開されたりする。一方 lambda は引数の数が合わないとエラーになったり、勝手に展開されたりすることがない。
# proc
proc { |x, y| puts "x: #{x}, y: #{y}" }.call([1, 2])
# x: 1, y: 2
# lambda
lambda { |x, y| puts "x: #{x}, y: #{y}" }.call([1, 2])
# ArgumentError (wrong number of arguments (given 1, expected 2))
return や break の挙動
break は proc の場合例外が発生し、lambda の場合 return と同じ挙動をします。break はあまり使う機会がないので、ここでは return の挙動の違いを見てみましょう。
def foo
proc { return 'A' }.call
'B'
end
foo #=> 'A'
def foo
lambda { return 'A' }.call
'B'
end
foo #=> 'B'
2-2
e = Enumerator.new do |g|
# Hint: Enumerator is
# essentially Fiber.
yield "Hello world"
end
puts e.next
問題紹介のみ。
ポイント
- Enumerator#next
- Enumerator#new
- Fiber
Enumerator#next
現在までの列挙状態に応じて「次」のオブジェクトを返し、列挙状態を1つ分進めます
https://docs.ruby-lang.org/ja/latest/method/Enumerator/i/next.html
obj = [1, 2, 3].each
obj.next #=> 1
obj.next #=> 2
obj.next #=> 3
Enumerator#new
Enumerator オブジェクトを生成して返します。与えられたブロックは Enumerator::Yielder オブジェクトを引数として実行されます。
obj = Enumerator.new do |yielder|
yielder << 'Hello'
yielder << 'World'
end
obj.next #=> "Hello"
obj.next #=> "World"
Enumerator::Yielder#<< は Enumerator::Yielder#yield のエイリアス
解答?
e = Enumerator.new do |g|
# Hint: Enumerator is
# essentially Fiber.
g.yield "Hello world"
end
puts e.next
これではどう頑張っても2文字解答になってしまう。
Fiber
外部イテレータとしての機能は Fiber を用いて実装されている
def enum2gen(enum)
Fiber.new do
enum.each { |i| Fiber.yield(i) }
end
end
g = enum2gen(1..100)
p g.resume #=> 1
p g.resume #=> 2
p g.resume #=> 3
以下は内部イテレータを外部イテレータに変換する例です。実際 Enumerator は Fiber を用いて実装されています。
Fiber を用いて実装
/* Enumerator#next */
static VALUE
enumerator_next(VALUE obj)
{
VALUE vs = enumerator_next_values(obj);
return ary2sv(vs, 0);
}
ということは Enumerator#next は Fiber オブジェクトに対して resume メソッドを呼び出している?
Fiber を用いて実装
C言語が読めないので Rubinius で実装部分を見てみる。
def next
reset unless @fiber
val = @fiber.resume
raise StopIteration, "iteration has ended" if @done
return val
end
def reset
@done = false
@fiber = Fiber.new stack_size: STACK_SIZE do
obj = @object # ここには Enumerator#.new で渡されたブロックが入る
@result = obj.each { |*val| Fiber.yield *val }
@done = true
end
end
外部イテレータは、内部イテレータ + Fiber.yield で実装されてるっぽい。
解答
e = Enumerator.new do |g|
# Hint: Enumerator is
# essentially
Fiber.
yield "Hello world"
end
puts e.next
外部イテレータが Fiber で実装されていることを利用して、ブロック内で Fiber.yield とすることで要素を追加することができる...。
正直よくわからない
それでもなぜ Enumerator.new のブロック内で Fiber.yield をすると要素が追加できるのかはよくわからない。
許して🙏
Slackで反応してくださった皆さん
ありがとうございました。
それっぽい解説をしてみたが
いまいちわかってない。
出題側の解説では
Enumerator は Fiber のラッパのようなものなので、実はブロックの中で Fiber.yield を呼ぶことでも要素を渡すことができる
程度のことしか書いてない。
我こそはという方がいたらこの辺りの仕組みをわかりやすく教えてください🥺
2-3
$s = 0
def say(n = 0)
$s = $s * 4 + n
end
i, j, k = 1, 2, 3
say i
say j
say k
# Hint: Binary representation.
$s != 35 or puts("Hello world")
問題紹介のみ。
ポイント
- シフト演算
- or演算子
シフト演算
続いて say メソッドを見てみる。
def say(n = 0)
$s = $s * 4 + n
end
ヒントにあるように 35 の2進数表現を考えると「100011」となる。
メソッドの中身を読み替えると
$s = $s * (2**2) + n
「左に2ビットシフトしてnを足す」という処理をしていることがわかる。
or演算子
左辺を評価し、結果が真であった場合にはその値を返します。左辺の評価結果が偽であった場合には右辺を評価しその評価結果を返します。 or は同じ働きをする優先順位の低い演算子です。
https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#or
|| よりも優先度の低いものが or 。
以下に違いの出るコードを紹介する。
foo = nil || 'hello'
foo #=> "hello"
bar = nil or 'hello'
bar #=> nil
解答
「10」「00」「11」となるような順番は10進数だと「2」「0」「3」。say のデフォルト引数が 0 なので say if say j とすることで上記の順番で評価される。
$s = 0
def say(n = 0)
$s = $s * 4 + n
end
i, j, k = 1, 2, 3
say if
say j
say k
# Hint: Binary representation.
$s != 35 or puts("Hello world")
3-1
def say s="Hello", t:'world'
"#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.
puts say :p
Kernel.#p
Kernel.#puts と同じで戻り値は必ず nil 。
オブジェクトがわかりやすい形で出力されるのでデバッグ等でよく使用される。
他にはコードゴルフで数値を出力する場合にputsの代わりに使われる。
puts 'string' # string
p 'string' # "string"
puts :symbol # symbol
p :symbol # :symbol
解答
:p というシンボルの前に t を追加することで t: p となり、 キーワード引数 :t に Kernel.#p の戻り値を渡している。
通常の引数とキーワード引数、受け取り側と渡す側の条件によって受け取る場所が変わる。
def say s="Hello", t:'world'
"#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.
puts say t:p
tips: キーワード引数
Ruby2.7から、通常の引数からキーワード引数が分離されました。これにより2.7からhashオブジェクトからキーワード引数への変換やその逆が非推奨となり、警告が出るようになりました。Ruby3でこの変換が完全に削除されます。以下にいくつかパターンを挙げておきます。Contribute Chance !! 👊
tips: キーワード引数
def foo(key:); end
foo({ key: 'value' }) # warning
def foo(hash, key: nil); end
foo(key: 'value') # warning
def foo(hash, opt**); end
foo(key: 'value') # warning
def foo(hash); end
foo(key: 'value') # safe
def foo(h, key: nil); end
foo({key: 'value'}) # safe
3-2
def say s, t="Goodbye "
# Hint: You can ignore a warning.
s = "#{ s } #{ t }"
t + "world"
end
puts say :Hello
問題紹介のみ。
ポイント
- オプション引数とデフォルト式
- Ruby代入構文の評価順
オプション引数のデフォルト式
Rubyのメソッドの引数にはデフォルト式を持たせオプショナルな引数とすることができる。プチョン引数は、呼び出し側で明示的に値が与えられなかった場合、デフォルト式の値が引数の値となる。
def foo(var = 'example')
p var
end
foo #=> "example"
Ruby代入構文の評価順
Rubyの代入構文は独特な評価を行うため
foo = foo
のような式が成り立つ。
これは左辺の変数が未初期化変数の場合、1番最初に nil で初期化され、その後右辺が評価され、左辺の変数に代入されている。
つまり、右辺の foo は 左辺の foo が nil で初期化された後に評価されるので nil となり、その nil が左辺の foo に代入されて nil となっている。
解答
このコードは ruby 2.7 では SyntaxError となるため詳しい解説は省く、このコードが実行できるのは Ruby 2.6 以前なのでそちらで実行して確認してみてほしい。
雑な解説をすると、t のデフォルト式が s = "#{ s } #{ t }" となり、代入構文の評価順に従って t の値が "Hello " となっている。
def say s, t=#"Goodbye "
# Hint: You can ignore a warning.
s = "#{ s } #{ t }"
t + "world"
end
puts say :Hello
3-3
def say
"Hello world" if
false && false
# Hint: No hint!
end
puts say
問題紹介のみ。
ポイント
- %記法
%記法
%!STRING! : ダブルクォート文字列
%Q!STRING! : 同上
%q!STRING! : シングルクォート文字列
%x!STRING! : コマンド出力
%r!STRING! : 正規表現
%w!STRING! : 要素が文字列の配列(空白区切り)
%W!STRING! : 要素が文字列の配列(空白区切り)。式展開、バックスラッシュ記法が有効
%s!STRING! : シンボル。式展開、バックスラッシュ記法は無効
%i!STRING! : 要素がシンボルの配列(空白区切り)
%I!STRING! : 要素がシンボルの配列(空白区切り)。式展開、バックスラッシュ記法が有効
解答
%記法の区切り文字に改行が使えます。ですので if の後ろに % を置くことで false && false を文字列とすることができます。文字列は真として評価されるためメソッドの戻り値は "Hello World" となります。
def say
"Hello world" if%
false && false
# Hint: No hint!
end
puts say
Extra-1
Hello = "Hello"
# Hint: Stop the recursion.
def Hello
Hello() +
" world"
end
puts Hello()
問題紹介のみ。
ポイント
- 同名のメソッドと変数の使い分け
- String#%
同名のメソッドと変数
def foo; 'method'; end
foo = 'variable'
foo #=> "variable"
foo() #=> "method"
変数と同名のメソッドを引数なしで呼びたいときは、メソッド名の後ろに () をつけると呼べる。
String#%
p "i = %d" % 10 # => "i = 10"
p "i = %x" % 10 # => "i = a"
p "i = %o" % 10 # => "i = 12"
p "i = %#d" % 10 # => "i = 10"
p "i = %#x" % 10 # => "i = 0xa"
p "i = %#o" % 10 # => "i = 012"
p "%d" % 10 # => "10"
p "%d,%o" % [10, 10] # => "10,12"
printf と同じ規則に従って args をフォーマットします。
args が配列であれば Kernel.#sprintf(self, *args) と同じです。それ以外の場合は Kernel.#sprintf(self, args) と同じです。
https://docs.ruby-lang.org/ja/latest/method/String/i/=25.html
解答
Hello 後ろに % を追加しています。
こうすることで、Helloメソッド呼び出しをHello変数呼び出しにし、値の "Hello" を nil でフォーマットしています。 "Hello" % nil はそのまま "Hello" となります。
Hello = "Hello"
# Hint: Stop the recursion.
def Hello
Hello%() +
" world"
end
puts Hello()
Extra-2
s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
s == s.downcase or puts "Hello world"
問題紹介のみ。
解答
s = "Dz"
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
s == s.downcase or puts "Hello world"
ヒントのURL先を見に行くとわかりますが、1文字中に大文字と小文字を含む二重音字が存在します。それを変数 s に代入しました。
Extra-3
def say
s = 'Small'
t = 'world'
puts "#{s} #{t}"
end
TracePoint.new(:line){|tp|
tp.binding.local_variable_set(:s, 'Hello')
tp.binding.local_variable_set(:t, 'Ruby')
tp.disable
}.enable(target: method(:say))
say
問題紹介のみ。
解答1
def say\
s = 'Small'
t = 'world'
puts "#{s} #{t}"
end
TracePoint.new(:line){|tp|
tp.binding.local_variable_set(:s, 'Hello')
tp.binding.local_variable_set(:t, 'Ruby')
tp.disable
}.enable(target: method(:say))
say
say メソッドの s をオプショナル引数としています。これによってメソッド呼び出しの時に デフォルト式の "Small" が代入され、呼び出し直後のTracePointのフックで値が "Hello" に書き換わります。
解答2
def say
s %= 'Small'
t = 'world'
puts "#{s} #{t}"
end
TracePoint.new(:line){|tp|
tp.binding.local_variable_set(:s, 'Hello')
tp.binding.local_variable_set(:t, 'Ruby')
tp.disable
}.enable(target: method(:say))
say
s を文字列 "Small" でフォーマットしたものを再度 s に代入させています。左辺の s は TracePoint のフックにより "Hello" が入っています。文字列を文字列でフォーマットした結果は元の文字列がそのまま返ります。
最後に
Rubyは難しい、普段の業務では知り得ない技術やテクニックが山ほどある。
こういったテクニックはドキュメントを隅々まで読んだり、リリースノートを読んだり、Ruby製のライブラリのソースコードを読んでみたりすることで知ることができる。
業務で使うタイミングはほとんどないかもしれないが、話のネタやコードゴルフをする上で役に立ったりする。
Rubyの「変なところ」に興味を持つ仲間が
増えますように...🙏
パズルで学ぶRuby
By Ryosuke Hiroe
パズルで学ぶRuby
- 572