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に隠された秘密のアルゴリズムについてお話しします。
概要
原著者の許諾を得て翻訳・公開いたします。