Chienomi

狂おしくRuby

プログラミング::lang

Rubyは素晴らしい。

いきなりなんだ、と思うかもしれないが、私はあまり凝り固まるのが好きではなく、Rubyが特選言語であるからこそ積極的にRuby以外も使っていこうとしている。

もともと特選言語だったPerlはもちろん、最近はPython, Lua, Goにもトライしている。

だが、他の言語を使えば使うほどにRubyにしびれるのだ。

そこで、今回はRubyは使わないよっていう人、あるいはRubyはサブ言語の一部であんま詳しくないなぁという人のために、Rubyのどのあたりが良いのか、ほんの一部だけだけども紹介したいと思う。

圧倒的Enum

Enumというのは反復可能なコレクションであり、そのようなものがある言語はいくつかある。 例えばJavaとか。

RubyではEnumerableというモジュールになっていて、mix-inという形で機能拡張として利用できるようになっている。

ただ、この話は少しむずかしいから、先に最も典型的なArrayについて述べていこう。

もちろん、まず単純なイテレータがある。

ary.each do |i|
  puts i
end

これは、forとかforeachとかeachとかいう形で割とよくある。

で、それぞれイテレーションした際の戻り値からなる配列を返すmap

ary = ary.map {|i| i * i }

真を返した値だけからなる配列を返すselect

ary = ary.select {|i| i > 0 }

偽を返した値だけからなる配列を返すreject

ary = ary.reject {|i| i[0] = "C" }

あたりは、まぁ普通である。これだけでも相当便利だけれども。

さらにもうちょっと便利なのが、全てが真を返すかどうかのboolを返すall?

ary.all? {|i| i > 0 }

いずれかが真を返すかのboolを返すany?

ary.any? {|i| !i.kind_of?(String) }

真を返した値の配列と偽を返した値の配列による2要素配列を返すpartition

positive, negative = ary.partition {|i| i >= 0}

「後ろから」順番にイテレーションするreverse_each

ary.reverse_each do |i|
  str.concat i
end

なんてのがある。しかし、これはまだ序の口だ。

さらに強力なものとしては、重複要素を取り除いた配列を返すuniq… これは「ブロックを与えると、ブロックの返り値が重複した要素を取り除く」という点が特に強力。

col = col.uniq {|i| i % 113}

zipはイテレータを回すときに、他の配列からも1つずつとってイテレータを回す。つまり、

["Hello,", "Hi,", "Good-morning"].zip(["John", "Bob", "Jessy"]) {|greet, name| puts(greet + " " + name)}

とすると

Hello, John
Hi, Bob
Good-morning Jessy

と出力される。

with_indexは値と同時に0からカウントしたインデックスを渡す。 配列の場合はインデックスだけでカウントしてもなんとかなるが、インデックスで要素をとれないコレクションなどには特に有効で、コードをまとめる効果が高い。

injectは前回の戻り値を次に回すことができる。 典型的には次のように配列の値を全部足すのに使える

total = ary.inject(0) {|sum, i| sum + i}

ただ、これは典型的すぎるのでsumというのが用意してある。

total = ary.sum

ほかにも最大値や最小値を取るmax, minに加え、「何を以て」最大値、最小値を取るかを明示できるmax_by, min_byもある。例えば、次は文字列の配列で、「文字数が最大のもの」を選択する。

longest = ary.max_by {|s| s.length }

さぁ、ここからはヤバイやつらだ。 まず、コレクションを「n個ずつ」取って繰り返すeach_slice、そして「n個ずつ、1つずつずらしながら」繰り返すeach_consだ。両者の違いは、例えば

[1, 2, 3, 4, 5, 6]

で、each_sliceなら[1, 2, 3], [4, 5, 6]だが、each_consだと、[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]となるということだ。

配列限定ならもっとヤバイのもある。 combinationはその配列からn個の要素のすべての組み合わせからなる配列を返す。 parmutationは、その配列からn個の要素の並びのすべての組み合わせからなる配列を返す。 さらに、productは、その配列と、引数にとった1つ以上の配列からそれぞれ1個ずつ要素をとった組み合わせからなる配列を返す。こうした、「組み合わせ」「順序」をメソッド一発で作ってくれる。

なんだ、配列限定か…と思うかもしれないが、実はEnumerableにはto_aという、コレクションの要素からなる配列を返すメソッドがあり、

enum.to_a.combination(3)

とか書けるのである。

なんでもEnum

さぁ、ここまででEnumerableのヤバさがわかったことだろう。 だが、本当にヤバイのはその汎用性である。

Enumerableはモジュールであり、任意のクラスを拡張するために使うことができる。 その条件は反復処理を行うためのeachというメソッドを持っていること、だけである

組み込みでEnumratorを返すメソッドの例を上げると

  • Hash#each ハッシュのキーと値のペア
  • Hash#each_keys ハッシュのキー
  • Hash#each_values ハッシュの値
  • String#each_byte 文字列の1バイトごと
  • String#each_char 文字列の文字ごと
  • String#each_line 文字列の行ごと
  • Range#each 「範囲」
  • Integer#times 0からその整数まで
  • IO.foreach IOを行単位で読みながら
  • IO#each 同じくIOを行単位で読みながら
  • Dir#each ディレクトリエントリ

となかなか色々あり、これらがそのまま前述のEnumerableとして扱うことができるのだ。

さらにいえば、String#scanなんかは配列を返すので、間接的にEnumerableである。

こうしたことにより、例えばある言葉がアナグラムかどうかを調べようと思ったとして、

WORDS = File.read("/usr/share/dict/words").strip.each_line

ARGF.each do |line|
  line = line.strip
  if line.each_char.permutation(line.length).each do |w|
    puts "#{line} is a anagram to #{w}." if WORDS.include?(w)
  end
end

みたいなことができる。 (Array#include?もなかなか威力のあるメソッドである)

ブロックの威力

Rubyの特徴的な「ブロック」は、関数オブジェクトをひとつ、特別な形で渡せるものである。 「特別な形で」ということを除けば

foo.each(funcation(line) {
  // ...
})

という形で可能なのだが、どうしてもカッコが邪魔になり、入れ子になるとさらに見づらくなっていく。 もちろん、Rubyでもそういう渡し方もできる。

ARGF.each(&->(line) {
  #...
})

けれど、1つだけであれば特別な渡し方をできることで、非常に多い「ひとつだけコードチャンクを渡したい」というケースにおいて可読性が高くすっきりした書き方ができるというのがポイントだ。

そしてイテレータは当然ながらコードチャンクを必要とする。 多くの場合、イテレータを実装する言語では特別な書き方になっている。例えばPerlでは

foreach $i (@array) {
  # ...
}

Pythonでは

for i in array
    #...

みたいな形である。 いずれも言語として一般的な形式ではなく、それ専用の文法になっている。

一方、Rubyにおけるイテレータは文ではなく、単純にeachというメソッドになっている。 イテレータがメソッドとして用意されているメリットは、一番には汎用性である。 特別な文法としてイテレータが用意されることになると、イテレータを応用した機能を追加することができない。 そのほかにも、通常のメソッド呼び出しの形式になることで、違和感のある「違う見た目」を用意する必要がなくなる。

もちろん、デメリットは(言語開発の上での要素を無視するならば、カッコなど引数の読みづらさの問題だが、Rubyの場合特別なブロック形式を持つことにより専用文法以上にすっきりと書くことができる。

それだけでなく、お手軽かつ分かりやすくかけるブロックの効果により、ブロックの活用により幅が広がっている。例えば、File#openにブロックを与えれば、そのブロックの間だけファイルをオープンした状態とし、ファイルを自動クローズしてくれる。

File.open("foo", "r") do |rfile|
  File.open("bar", "w") do |wfile|
    rfile.each do |line|
      wfile.print line.upcase
    end
  end
end

メソッド側でブロックを特定のバインディングで評価させるというテクニックにより、ブロックの中身だけ特別な文法を与えるというようなこともできる。

pdoc.build do
  h1 "Hello, world"
  par "This", "is", "paragraphs"
end

こうしたブロックの威力が、なおさらEnumの強力さを引き立てている。

ちなみに、doendという形式はあまり見かけないが、{}形式も使うことができる。 ただ、複数行に渡る場合、結合の強い{}よりも、結合の弱いdoendのほうが良いことが多いだろう。つまり、

a b {
  # ...
}

a(b {})であり、

a b do
  # ...
end

a(b) {}なのである。

語彙豊富

プログラミング言語の使い勝手というのは文法ばかりで語れるものではない。 やはり備えている機能や関数/メソッド、ライブラリなどによって大きく変わってくる。

個人的にはあまりライブラリに依存するというのは好きではなくて、まさに目的を達成するためにライブラリを使うのは良いのだが、それがあることによって明確に使い勝手が増すような機能を持つライブラリは最初から持っていてほしいし、基本的な機能として使いたいものをわざわざ(標準ライブラリであっても)ロードするというのは好きではない。

Rubyは特別に語彙が豊富な言語だ。特にライブラリをロードすることなく、非常に強力な機能が取り揃えられている。

Rubyの機能の特徴として、「典型的な記述はそういう機能をつけよう」というのがある。 例えば、配列に対して何かを施した配列を必要とするようなケースというのはとても多い。

new_ary = Array.new
ary.each {|i| new_ary.push i.to_i }
ary = new_ary

だからそういう典型的なケースは機能として提供される。 書きやすく、読みやすい上に、このほうが速い。

ary = ary.map {|i| i.to_i }

さらに、特にEnumerable#mapにおいては、各要素を引数としてメソッドを呼び出す、というケースが多い。 「ケースが多い」ということは、Rubyはそれを一発でできるようにしているということだ。

ary = ary.map(&)

なお、これは特別な文法というわけではなく、&で渡す引数はそれをブロックとして(つまり、特別な扱いをする関数オブジェクトとして)渡すということになる。 このとき、渡しているものが関数オブジェクトでないならば、to_procメソッドによって関数オブジェクトにする、という振る舞いをする。 そして、:to_iで表されているSymbolクラスにはto_procメソッドが存在し…という流れである。

なお、mapは便利だが、メソッドが複数の値を返す場合は困ってしまうことがある。 その複数の値を一体として扱うのならばいいのだが、単純に平坦に配列として追加してほしいということがあるのだ。つまり

new_ary = Array.new
ary.each {|i| new_ary.concat i.multivalue_return }
ary = new_ary

これは稀なことではなく、まぁよくあることだ。 そう、よくあることだから

ary = ary.flat_map(&)

Rubyは一発で書かせてくれる。

Paizaでよくある、「1行にスペース区切りの数字が並んでいる」みたいなのは

a, b, c = gets.chomp.split.map(&)

でいける。

そうそう、最後にスペースがいくつあるかわからない、みたいなこともよくある。 あと、先頭がインデントされていて、先頭にスペースがあることも。

Perlだと

$line = <>;
$line =~ s/\s*$//;

とかやることになる。こういうのは、Rubyなら一発

line = gets.rstrip

左側の連続するスペースも一気に取り除くなら

line = gets.strip

いや、これgetsで読めたらいいけどEOFだったらどうすんねんって? 確かにwhileだったりすると困る。もちろん、その場合もちゃんと用意されている。そう、Rubyならね。

line = gets&.strip

これで、Kernel.getsの結果がnilだったら、stripを呼び出すことなくnilを返す。

ちょっと注意してほしいのは、例えば

line = gets&.strip.downcase

みたいなことだ。これ、

line = (_x = gets ? _x.strip : nil).downcase

なので(一時変数は実際にはないが)、nil.downcaseが呼ばれてエラーになる可能性がある。 この場合は

line = gets&.strip&.downcase

あんまり長く、なおかつnilになる可能性が最初にしかないなら、分離したほうがよい。

if line = gets
  lien = line.strip.downcase
end

可読性

どういう言語が読みやすい、どういう特徴が読みやすい、というのは聖戦の元だが、少なくともプログラミングの全くできない、あるいは初心者の人に読ませる分にはRubyのコードは読みやすいと好評である。

そのポイントのひとつに要素の少なさというのがある。

Rubyは非常に語彙豊富なので、書くべき要素が少ない。 すなわち、読むべきコード量が減るのである。 例えば

latest_article = articles.max_by(&)

と書いてあったら、特に説明の必要はないだろう。しかし、同じことを意味していても

sub max_by_date {
  my $max;
  foreach (@_) {
    if (!max || $max->{date} < ->{date} ) {
      $max = ;
    }
  }
  return $max;
}

my $latest_article = max_by_date(@articles);

は「こっちのほうが読みやすい!」と主張することは、まぁ考えにくいだろう。 どちらかといえば、初心者には説明が必要になるようなコードである。

だから、Rubyの機能の豊富さは自分で書く必要がないから書きやすいだけでなく、一目瞭然であるからとても読みやすい。 第三者が提供するライブラリの知識を求められるのと比べると、言語処理系に標準でついてくるものは覚えているから要求としても(例えそのために覚えることが多いとしても)易しい。

さらに、必要なことを書く必要がない上に総じて明瞭で統一的であるため、なおさらわかりやすい。

Goを使っていて思ったのだが、Goは外見上同じなのだが、ある文脈でのみ使用可能な特別な意味のものというのがたくさんある。

例えば i.(T) という型アサーションというものがあり、

t, ok := i.(string)

とか書けるのだが、同じような見た目を持つ

switch v: = i.(type) {
  case int:
    // ...
  case string:
    // ...
  default:
    // ...
}

というのがある。一見するとi.(type)はインターフェイスiの型を返してくれそうに見えるのだが、実際はswitch文でのみ書ける特別な書き方である。 こういう一定の法則に基づいておらず、特別扱いされるなんちゃらみたいなのが多様されると、個人的にはものかごくもやっとする。

基本的にはRubyの言語仕様がもたらす「意味」は広く一般化され、一般化された理屈の中で動作する。 だから、先のコードを仮にRubyで書くとしても、

def max_by_date(enum)
  max = nil
  enum.each do |i|
    if !max || max["date"] < i["date"]
      max = i
    end
  end
  max
end

latest_article = max_by_date(articles)

といくらか読みやすい。

Rubyの場合、自由度がないわけではなく、書き方はたくさんあるにも関わらず「綺麗に書こう」とがんばらなくても読みやすいというのが特徴的だ。

年末の挨拶

これが今年最後の記事となると思うので、ご挨拶を。

本年も本当にお世話になりました。

月間5万PVくらいだったJournal de Akiから分化して、一時は月間1万PV程度まで落ち込んだのが一昨年のこと。 そして、月間15万PVくらいまで伸びた状態で新しいドメインをとって、WordPressをやめてPureBuilder Simplyに移行したのが今年。

またPV落ちるかなぁ、と思っていたのだけれど、すぐ10万PVを回復した。

このサイトには広告もついてなくて、手間も費用もかかっているから維持は結構大変なのだけど、 それでも続けていられるのは皆様が見てくれるからこそ、なんとかモチベーションを保っている。

本当にありがとうございます。

来年も、Chienomiをよろしくお願い致します。