Crystal言語作者がRubyを愛する理由(6)ブロック(翻訳)
Rubyのブロックについて既にご存じの方は、この後の「Rubyのブロックは単なる無名関数ではない」までスキップいただいて構いません。ここではご存じない方向けに簡単なチュートリアルを行います。
Rubyのブロックについての簡単なあらまし
Rubyのメソッドは、ブロックを1つ受け取れます。以下は最もシンプルな例です。
def run
yield
end
run do
puts "Hello world!"
end
上のコード例では、run
メソッドの定義内でyield
を行っています。このrun
メソッドを呼び出すときに、do
〜end
で囲まれたブロックを渡しています。すると、渡したブロックの内容がyield
の位置で実行されます。
これを応用することで、少しだけ実用的な例を定義できます。
def twice
yield
yield
end
twice do
puts "Hello world!"
end
yield
が2つあるので、渡したブロックも2回実行されます。
指定の回数だけ何かを実行するという一般によく見られるアイデアは、Rubyでも以下のように実現できます。
3.times do
puts "Hello world!"
end
ブロックには||
で引数を渡せるので、以下のようにi
を引数として書くことも可能です。
3.times do |i|
puts "Hello number #{i}!"
end
上の結果は以下のようになります。
Hello number 0!
Hello number 1!
Hello number 2!
Rubyのブロックは単なる無名関数ではない
Rubyのブロックは、一見すると、他の多くの言語でもよく見かける「関数に無名関数を渡すと、その無名関数が関数内で実行される」という概念と似ていそうです。
それではGoでtimes
関数を定義してみましょう。
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
上の関数は、整数n
を受け取ると、block
という変数に渡された関数をn
回呼び出します。
プログラム全体は以下のようになります。
package main
import "fmt"
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
func main() {
times(3, func(i int) {
fmt.Printf("Hello number %v!\n", i)
})
}
結果は、上述のRubyスニペット3.times
の場合と同じになります。
コード量が少々増えることと、ややごちゃごちゃする点を除けば、Rubyのブロックはさほど特殊ではないように思えるかもしれません。実際、上のようなことは、GoでもJavaでもC#でもRustなど多くの言語でも同じようにやれます。
Rubyのブロックが他の言語と違う点をお見せするために、以下のプログラムをこしらえてみました。
def greet_20_numbers
seconds = Time.now.sec
20.times do |i|
puts "Hello number #{i + 1}!"
if i == seconds
puts "Oh no, time's up!"
return
end
end
puts "Bye!"
end
greet_20_numbers
上のコード例は大した意味はありませんが、ざっくり以下のようなことをやっています。
- "Hello number ...!"を20回出力してから"Bye!"を出力する
- ただし、上の出力中にたまたま挨拶文の数字が時計の秒数と一致した場合は、"Oh no, time's up!"を出力する。この場合はメソッドから
return
するので"Bye!"は出力されない。
上のプログラムをそのままGoに移植してみましょう。
package main
import "fmt"
import "time"
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
func greet20numbers() {
now := time.Now()
second := now.Second()
times(20, func(i int) {
fmt.Printf("Hello number %v!\n", i)
if i == second {
fmt.Println("Oh, time's up!")
return
}
})
fmt.Printf("Bye!")
}
func main() {
greet20numbers()
}
しかしこのプログラムの実行結果はRubyの場合と同じになりません。理由はおわかりでしょうか?
Goのreturn
は関数を終了するためにも使われます。しかしこのreturn
は、greet20numbers
関数そのものからreturn
するのではなく、times
関数に渡した関数からreturn
します。Rubyの場合、return
はそれを囲むブロックからreturn
するのではなく、「それを囲むメソッドからreturn
します」。
これこそが、Rubyのブロックと関数受け渡しが他の言語と最も異なる点です。
訳注
以下、Ruby以外の言語の場合は「ブロック」とかぎかっこ付きで表記し、Rubyの場合はかきかっこなしのブロックと表記します。
たとえば、多くの言語(C, Java, C#, Go, Rust)にはif
やwhile
やfor
があります。これらの構文要素にも一種の「ブロック」があり、やりたいことをその中に記述できます。if
の条件が成立すれば、if
の「ブロック」内にあるものがすべて実行されますし、同様にwhile
も条件が成立している間は「ブロック」内にあるものが実行されます。for
ではより複雑な条件も指定できます。
これらはいずれも、「ブロック」内部からreturn
すると、それを囲むメソッドからreturn
します。しかし、これと同じように振る舞う別の構文要素は定義できません。「ブロック」の受け渡しはできても、「ブロック」内でreturn
するとその「ブロック」からreturn
するのであって、それを囲む外側のメソッドからはreturn
しません。
Rubyではブロックを使うことで、if
やwhile
やfor
のように振る舞う独自の構文要素を作成できます。3.times do ... end
の内側でreturn
すると、(そのブロックではなく)そのメソッドからreturn
します。たとえば以下のように書いたとしましょう。
File.open("some_file.txt") do |file|
file.each_line do |line|
return if some_condition
end
end
このとき、2回ネストしているブロックの内側にあるreturn
は、それを囲むメソッドそのものからreturn
します。こうなると、ブロックはもはや無名関数ではなく、一種の言語拡張構文のように見えてきます。
Rubyのブロックではbreak
も書ける
Rubyのブロックなら可能で、他の言語にある普通の無名関数ではできないことがもうひとつあります。Rubyでは以下のようにbreak
も書けるのです。
3.times do |i|
puts "Hello number #{i}!"
break if i == 1
end
上のコード例は、"Hello number 0!"、"Hello number 1!"を出力してから停止します。
多くの言語ではwhile
ループをbreak
で中断できますが、Rubyではそれと同じメリットを得られる独自のループを作れるのです。return
でループから戻ることも、break
でループを中断するのも自由自在です。
こうなってくると、Rubyのブロックはますます無名関数というよりも言語の新しい構文要素のように思えてきます。
最後に、ブロック内でnext
を呼び出すと、他の言語のwhile
ループ内でnext
やcontinue
を呼んだときと同じように次の反復に進めます。しかしこれは無名関数でreturn
を呼べば同じことができます。
Rubyのブロックのその他の機能
Rubyのブロックはキャプチャして受け渡しすることも可能ですが、これは通常の無名関数や関数受け渡しとさして変わらないので、本記事では扱いません。ブロックを他のオブジェクトのコンテキストで実行することも可能ですが、これも扱いません。しかしこうしたことも可能なおかげで、Rubyのブロックの柔軟性と利便性と楽しさが高まっています。
Rubyのようなブロックが使える他の言語
もちろんCrystalにもRubyと同じように使えるブロックがあります。以下はCrystalで書かれたサンプルプログラムです。
def greet_20_numbers
seconds = Time.local.second
puts seconds
20.times do |i|
puts "Hello number #{i + 1}!"
if i == seconds
puts "Oh, it's time to get back to work!"
return
end
end
puts "Bye!"
end
greet_20_numbers
唯一の違いはTime.now.sec
ではなくTime.local.second
を使っている点ですが、それ以外はRubyとまったく同じです。
実はKotlinも、関数や「ブロック」を受け取れるメソッドで構文を拡張可能で、呼び出しを実際に囲んでいる関数からもreturn
で戻れるもうひとつの言語です。素晴らしい!
KotlinはRubyからインスピレーションを受けた実によい言語だと私は思います。もっともRubyと違ってbreak
で中断はできないようですが、これを回避するささやかな方法は存在します。
次回予告
最終回は、Rubyに隠された秘密のアルゴリズムについてお話しします。
概要
原著者の許諾を得て翻訳・公開いたします。