Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Crystal言語作者がRubyを愛する理由(6)ブロック(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

crystal-lang/crystal - GitHub

Crystal言語作者がRubyを愛する理由(6)ブロック(翻訳)

Rubyのブロックについて既にご存じの方は、この後の「Rubyのブロックは単なる無名関数ではない」までスキップいただいて構いません。ここではご存じない方向けに簡単なチュートリアルを行います。

Rubyのブロックについての簡単なあらまし

Rubyのメソッドは、ブロックを1つ受け取れます。以下は最もシンプルな例です。

def run
  yield
end

run do
  puts "Hello world!"
end

上のコード例では、runメソッドの定義内でyieldを行っています。このrunメソッドを呼び出すときに、doendで囲まれたブロックを渡しています。すると、渡したブロックの内容が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)にはifwhileforがあります。これらの構文要素にも一種の「ブロック」があり、やりたいことをその中に記述できます。ifの条件が成立すれば、ifの「ブロック」内にあるものがすべて実行されますし、同様にwhileも条件が成立している間は「ブロック」内にあるものが実行されます。forではより複雑な条件も指定できます。

これらはいずれも、「ブロック」内部からreturnすると、それを囲むメソッドからreturnします。しかし、これと同じように振る舞う別の構文要素は定義できません。「ブロック」の受け渡しはできても、「ブロック」内でreturnするとその「ブロック」からreturnするのであって、それを囲む外側のメソッドからはreturnしません。

Rubyではブロックを使うことで、ifwhileforのように振る舞う独自の構文要素を作成できます。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ループ内でnextcontinueを呼んだときと同じように次の反復に進めます。しかしこれは無名関数で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に隠された秘密のアルゴリズムについてお話しします。

関連記事

Crystal言語作者がRubyを愛する理由(7)秘密のアルゴリズム(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。