狂おしくRuby
プログラミング::lang
序
Rubyは素晴らしい。
いきなりなんだ、と思うかもしれないが、私はあまり凝り固まるのが好きではなく、Rubyが特選言語であるからこそ積極的にRuby以外も使っていこうとしている。
もともと特選言語だったPerlはもちろん、最近はPython, Lua, Goにもトライしている。
だが、他の言語を使えば使うほどにRubyにしびれるのだ。
そこで、今回はRubyは使わないよっていう人、あるいはRubyはサブ言語の一部であんま詳しくないなぁという人のために、Rubyのどのあたりが良いのか、ほんの一部だけだけども紹介したいと思う。
圧倒的Enum
Enumというのは反復可能なコレクションであり、そのようなものがある言語はいくつかある。 例えばJavaとか。
RubyではEnumerableというモジュールになっていて、mix-inという形で機能拡張として利用できるようになっている。
ただ、この話は少しむずかしいから、先に最も典型的なArrayについて述べていこう。
もちろん、まず単純なイテレータがある。
.each do |i|
aryputs i
end
これは、for
とかforeach
とかeach
とかいう形で割とよくある。
で、それぞれイテレーションした際の戻り値からなる配列を返すmap
。
= ary.map {|i| i * i } ary
真を返した値だけからなる配列を返すselect
= ary.select {|i| i > 0 } ary
偽を返した値だけからなる配列を返すreject
= ary.reject {|i| i[0] = "C" } ary
あたりは、まぁ普通である。これだけでも相当便利だけれども。
さらにもうちょっと便利なのが、全てが真を返すかどうかのboolを返すall?
.all? {|i| i > 0 } ary
いずれかが真を返すかのboolを返すany?
.any? {|i| !i.kind_of?(String) } ary
真を返した値の配列と偽を返した値の配列による2要素配列を返すpartition
= ary.partition {|i| i >= 0} positive, negative
「後ろから」順番にイテレーションするreverse_each
.reverse_each do |i|
ary.concat i
strend
なんてのがある。しかし、これはまだ序の口だ。
さらに強力なものとしては、重複要素を取り除いた配列を返すuniq
…
これは「ブロックを与えると、ブロックの返り値が重複した要素を取り除く」という点が特に強力。
= col.uniq {|i| i % 113} col
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
は前回の戻り値を次に回すことができる。
典型的には次のように配列の値を全部足すのに使える
= ary.inject(0) {|sum, i| sum + i} total
ただ、これは典型的すぎるのでsum
というのが用意してある。
= ary.sum total
ほかにも最大値や最小値を取るmax
,
min
に加え、「何を以て」最大値、最小値を取るかを明示できるmax_by
,
min_by
もある。例えば、次は文字列の配列で、「文字数が最大のもの」を選択する。
= ary.max_by {|s| s.length } longest
さぁ、ここからはヤバイやつらだ。
まず、コレクションを「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
という、コレクションの要素からなる配列を返すメソッドがあり、
.to_a.combination(3) enum
とか書けるのである。
なんでも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.strip
line 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の特徴的な「ブロック」は、関数オブジェクトをひとつ、特別な形で渡せるものである。 「特別な形で」ということを除けば
.each(funcation(line) {
foo// ...
})
という形で可能なのだが、どうしてもカッコが邪魔になり、入れ子になるとさらに見づらくなっていく。 もちろん、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|
.each do |line|
rfile.print line.upcase
wfileend
end
end
メソッド側でブロックを特定のバインディングで評価させるというテクニックにより、ブロックの中身だけ特別な文法を与えるというようなこともできる。
.build do
pdoc"Hello, world"
h1 "This", "is", "paragraphs"
par end
こうしたブロックの威力が、なおさらEnumの強力さを引き立てている。
ちなみに、do
〜end
という形式はあまり見かけないが、{}
形式も使うことができる。
ただ、複数行に渡る場合、結合の強い{}
よりも、結合の弱いdo
〜end
のほうが良いことが多いだろう。つまり、
{
a b # ...
}
は a(b {})
であり、
do
a b # ...
end
はa(b) {}
なのである。
語彙豊富
プログラミング言語の使い勝手というのは文法ばかりで語れるものではない。 やはり備えている機能や関数/メソッド、ライブラリなどによって大きく変わってくる。
個人的にはあまりライブラリに依存するというのは好きではなくて、まさに目的を達成するためにライブラリを使うのは良いのだが、それがあることによって明確に使い勝手が増すような機能を持つライブラリは最初から持っていてほしいし、基本的な機能として使いたいものをわざわざ(標準ライブラリであっても)ロードするというのは好きではない。
Rubyは特別に語彙が豊富な言語だ。特にライブラリをロードすることなく、非常に強力な機能が取り揃えられている。
Rubyの機能の特徴として、「典型的な記述はそういう機能をつけよう」というのがある。 例えば、配列に対して何かを施した配列を必要とするようなケースというのはとても多い。
= Array.new
new_ary .each {|i| new_ary.push i.to_i }
ary= new_ary ary
だからそういう典型的なケースは機能として提供される。 書きやすく、読みやすい上に、このほうが速い。
= ary.map {|i| i.to_i } ary
さらに、特にEnumerable#map
においては、各要素を引数としてメソッドを呼び出す、というケースが多い。
「ケースが多い」ということは、Rubyはそれを一発でできるようにしているということだ。
= ary.map(&:to_i) ary
なお、これは特別な文法というわけではなく、&
で渡す引数はそれをブロックとして(つまり、特別な扱いをする関数オブジェクトとして)渡すということになる。
このとき、渡しているものが関数オブジェクトでないならば、to_proc
メソッドによって関数オブジェクトにする、という振る舞いをする。
そして、:to_i
で表されているSymbol
クラスにはto_proc
メソッドが存在し…という流れである。
なお、map
は便利だが、メソッドが複数の値を返す場合は困ってしまうことがある。
その複数の値を一体として扱うのならばいいのだが、単純に平坦に配列として追加してほしいということがあるのだ。つまり
= Array.new
new_ary .each {|i| new_ary.concat i.multivalue_return }
ary= new_ary ary
これは稀なことではなく、まぁよくあることだ。 そう、よくあることだから
= ary.flat_map(&:multivalue_return) ary
Rubyは一発で書かせてくれる。
Paizaでよくある、「1行にスペース区切りの数字が並んでいる」みたいなのは
= gets.chomp.split.map(&:to_i) a, b, c
でいける。
そうそう、最後にスペースがいくつあるかわからない、みたいなこともよくある。 あと、先頭がインデントされていて、先頭にスペースがあることも。
Perlだと
$line = <>;
$line =~ s/\s*$//;
とかやることになる。こういうのは、Rubyなら一発
= gets.rstrip line
左側の連続するスペースも一気に取り除くなら
= gets.strip line
いや、これgets
で読めたらいいけどEOFだったらどうすんねんって?
確かにwhile
だったりすると困る。もちろん、その場合もちゃんと用意されている。そう、Rubyならね。
= gets&.strip line
これで、Kernel.gets
の結果がnil
だったら、strip
を呼び出すことなくnil
を返す。
ちょっと注意してほしいのは、例えば
= gets&.strip.downcase line
みたいなことだ。これ、
= (_x = gets ? _x.strip : nil).downcase line
なので(一時変数は実際にはないが)、nil.downcase
が呼ばれてエラーになる可能性がある。
この場合は
= gets&.strip&.downcase line
あんまり長く、なおかつnil
になる可能性が最初にしかないなら、分離したほうがよい。
if line = gets
= line.strip.downcase
lien end
可読性
どういう言語が読みやすい、どういう特徴が読みやすい、というのは聖戦の元だが、少なくともプログラミングの全くできない、あるいは初心者の人に読ませる分にはRubyのコードは読みやすいと好評である。
そのポイントのひとつに要素の少なさというのがある。
Rubyは非常に語彙豊富なので、書くべき要素が少ない。 すなわち、読むべきコード量が減るのである。 例えば
= articles.max_by(&:date) latest_article
と書いてあったら、特に説明の必要はないだろう。しかし、同じことを意味していても
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)
という型アサーションというものがあり、
, ok := i.(string) t
とか書けるのだが、同じような見た目を持つ
switch v: = i.(type) {
case int:
// ...
case string:
// ...
default:
// ...
}
というのがある。一見するとi.(type)
はインターフェイスi
の型を返してくれそうに見えるのだが、実際はswitch
文でのみ書ける特別な書き方である。
こういう一定の法則に基づいておらず、特別扱いされるなんちゃらみたいなのが多様されると、個人的にはものかごくもやっとする。
基本的にはRubyの言語仕様がもたらす「意味」は広く一般化され、一般化された理屈の中で動作する。 だから、先のコードを仮にRubyで書くとしても、
def max_by_date(enum)
= nil
max .each do |i|
enumif !max || max["date"] < i["date"]
= i
max end
end
maxend
= max_by_date(articles) latest_article
といくらか読みやすい。
Rubyの場合、自由度がないわけではなく、書き方はたくさんあるにも関わらず「綺麗に書こう」とがんばらなくても読みやすいというのが特徴的だ。
年末の挨拶
これが今年最後の記事となると思うので、ご挨拶を。
本年も本当にお世話になりました。
月間5万PVくらいだったJournal de Akiから分化して、一時は月間1万PV程度まで落ち込んだのが一昨年のこと。 そして、月間15万PVくらいまで伸びた状態で新しいドメインをとって、WordPressをやめてPureBuilder Simplyに移行したのが今年。
またPV落ちるかなぁ、と思っていたのだけれど、すぐ10万PVを回復した。
このサイトには広告もついてなくて、手間も費用もかかっているから維持は結構大変なのだけど、 それでも続けていられるのは皆様が見てくれるからこそ、なんとかモチベーションを保っている。
本当にありがとうございます。
来年も、Chienomiをよろしくお願い致します。