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

Crystal言語作者がRubyを愛する理由(5)標準ライブラリが優秀(翻訳)

概要

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

crystal-lang/crystal - GitHub

Crystal言語作者がRubyを愛する理由(5)標準ライブラリが優秀(翻訳)

多くのプログラミング言語では、ファイルを読み込んでその内容を一行ずつ処理したいことがよくあります。私がRubyを知った頃のJavaでは、以下のような処理を書かなければならないのが普通でした。

BufferedReader reader;
try {
  reader = new BufferedReader(new FileReader("/path/to/file.txt"));
  String line;
  while ((line = reader.readLine()) != null) {
    System.out.println(line);
    // read next line
    line = reader.readLine();
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  reader.close()
}

このコードはもちろん動きますが、行数が多いので読むのも書くのも大変ですし、コードの意図もひと目ではわかりにくくなっています。

以下はGoの場合です(Stack Overflowの回答より)

file, err := os.Open("/path/to/file.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
// optionally, resize scanner's capacity for lines over 64K, see next example
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

もちろんこれも動きますが、それでも行数は少なくありません。

以下はRustの場合です(Stack Overflowの回答より)。

let file = File::open("foo.txt")?;
let reader = BufReader::new(file);

for line in reader.lines() {
    println!("{}", line?);
}

だいぶ短くなりましたが、それでもBufReaderが登場しています。

Rubyの場合はこうです。

File.foreach("/path/to/file.txt") do |line|
  puts line
end

シンプルかつ簡潔ですね。ファイルを1行ずつ読み出したければ、Rubyにそう支持指示するだけで済みます。中間のステップも不要ですし、バッファ付きリーダーのような中間概念も不要です。

はい、皆さんのおっしゃりたいことはわかります。JavaでもGoでもRustでも、そして他の言語でも、ヘルパー関数を定義すればRubyと同じぐらい簡潔なコードになります。

しかしここで問題にしたいのは、Rubyではこの機能が最初から標準ライブラリに備わっていていつでも利用できる点です。プロジェクトごとに同じヘルパー関数を何度も書く必要はまったくありません。

上述の例ではファイルを1行ずつ読み出しましたが、Rubyを使い始めてみると、標準ライブラリがいかに充実しているかを実感できます。

Javaに関する追伸

最近のJavaでは、ファイルを1行ずつ読み出せるヘルパー関数が標準ライブラリに追加されています。今は以下のように書けるそうです(Stack Overflowより)。

try (Stream<String> lines = Files.lines(file, Charset.defaultCharset())) {
  lines.forEachOrdered(System.out::println);
}

いいですね!ファイルを1行ずつ読み出す操作の利用頻度が高いことがやっと認められて、標準ライブラリに追加する意義が理解されたことを示していると思えます。Rubyはこれに関してパイオニアだったというわけです。

Rubyが舞台裏でどれほど働いているか

JavaやGoやRustでファイルを1行ずつ読み込むには、ファイルをオープンしたうえで何かを使う必要があります。JavaやRustではバッファ付きリーダーが必要でしたし、Goの場合はbufio.Scannerが必要でした。これはどういうことでしょうか?

さて、OSでファイルを読み込むときのインターフェイスは、「取得したデータでバイトのチャンク(塊)を埋めて、チャンクが何バイトかを知らせる」というものです。OSのこのインターフェイスが、以下のように既存のプログラミング言語にも影響していることがわかります。

つまり、ファイルを1行ずつ読み出すときは、このインターフェイスの上で行わなければならないということです。

しかし、このようなプリミティブな呼び出しにはシステムコールが必要であり、システムコールを多数呼び出すと大変なコストがかかります。バッファを使うと、ファイルから可能な限り情報を読み込んでメモリ内で処理できるので、システムコールの発生を最小限にとどめられます。

GoやJavaでオープンしたファイルは、ファイルへのハンドルでしかありません。ファイルを効率よく処理するには、その上にバッファを置く必要があります。

ではRubyはどうやっているのでしょうか?Rubyは、そうした諸々の面倒なことを知る必要をなくしたのです。Rubyでファイルをオープンすると、内部でちゃんとバッファが用意されます。他の言語で遅かれ早かれやりそうな複雑なことを、Rubyはライブラリで既にやってくれているのです。ありがとう、Ruby!

Rubyがそこまでやってくれる理由についてはわかりません。ひとつの推測ですが、ユーザーにとっての複雑さを軽減するためかもしれません(実際、Rubyは常にそうしようとしています)。もうひとつの推測は、ファイルをオープンするときは99%バッファリングが必要に決まっているのだから、デフォルトをバッファ付きにするのが当然と判断したのかもしれません。

RubyのIOオブジェクトはさらに強力

話はまだ終わりではありません。

ファイルを1行ずつ読み出す必要があり、しかしそのファイルが惜しくもUTF-8エンコーディングでないとしましょう(EUC-JPなど)。

ほとんどの言語では、そのために何らかの中間オブジェクトを新たに導入する必要が生じる可能性が高いでしょう。その分コードが数行増えるでしょうが、さして問題ではありません。

Rubyでは、読み書きするときのエンコーディングを以下のように直接指定できます。

File.foreach("/path/to/file.txt", encoding: "EUC-JP") do |line|
  puts line
end

これだけでおしまいです。

標準ライブラリにはまだまだパワーが秘められている

本記事で扱ったのは、ファイルを1行ずつ読み出すという機能だけでした。しかしRubyの標準ライブラリには、こういうありがたい機能があちこちに盛り込まれています。最終的にほとんどコードを書かなくても、しかも外部依存を使わなくても、標準ライブラリで相当いろんなことができるのです。

Rubyのコミュニティ

私の印象ですが、Rubyのコミュニティは、Rubyの標準ライブラリのそうしたあり方をgemで実践しようととしているように思えます。ユーザーの暮らしをシンプルにし、できるだけ少ない行数でやりたいことを実現できるように。

次回予告

ほぼRuby独自の機能である「ブロック」についてお話しします。

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

関連記事

Crystal言語作者がRubyを愛する理由(1)「等しさ」の扱い(翻訳)


CONTACT

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