はじめに
私は業務ではずっとJavaを触ってきたJavaプログラマです。しかし、他の言語を触ってみることももちろんあります。
色々な言語をつまみ食いしてきましたが、Java以外ではRubyがお気に入りです。初めて触ったスクリプト言語がRubyだったということもあり、Rubyからは様々なことを学びました。Rubyから学んだこと、Rubyを触ってみて感じたことなどと書き留めていきたいと思います。
実はRubyに初めて触れたのはもう何年も前のことで、なんで今さらそんなことを書こうと思ったのか自分でも謎ですが、書いちゃいけないってことはないと思うのでなんとか思い出しながら書きます。Rubyに興味がある人の参考になったら幸いです。
注意点
- 他の言語(主にJava、一部Python)の例を出して、それに対するRubyの優位性について言及したりもしますが、他の言語を貶めるような意図は全くありません。JavaもPythonも好きですし
- 私はRubyについてはちょっとした作業のためのスクリプトを書くといった程度の使い方しかしていません。業務での開発で使った経験はなく、個人開発で本格的なアプリケーションをRubyで作るといったこともしたことはありませんので、その程度の人間が書いていると思ってください
- 採り上げている事項は、必ずしもRubyに特有のものとは限りません
サンプルコードの動作環境
- Ruby 2.6.0
- Java 11.0.2
- Python 3.5.2(古い…)
本文
Rubyを触ってみて良いと思ったところ
短く書ける
基本的にRubyのコードはJavaと比べるとかなり短くなるので、ちょっとしたスクリプトを書くのに便利だと思いました。
あまり実用的じゃない例ですが、コマンドライン引数としてテキストファイルのパスを受け取り、「hoge」が含まれる行だけ表示するプログラムを書いて比較するとこんな感じになります。
Rubyなら数行で書けます。(その気になれば1行でもいけそうですが)
File.foreach(ARGV[0]) do |line|
puts line if line.include?('hoge')
end
追記
コメントにて、1行で書いた場合のコードを教えていただきました。
puts ARGF.each_line.grep(/hoge/)
Javaだとこんな感じでしょうか。(Java 11)
Javaも進化しているのでこれでも短くなったほうですが、それでもけっこう冗長になってしまいます。
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws Exception {
try(var lines = Files.lines(Path.of(args[0]))) {
lines.filter(line -> line.contains("hoge"))
.forEach(System.out::println);
}
}
}
私が初めて本格的に触ったバージョンであるJava6であればこんな感じでしょう。1
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(args[0])));
String line;
while((line = br.readLine()) != null) {
if(line.contains("hoge")) {
System.out.println(line);
}
}
} finally {
if(br != null) {
br.close();
}
}
}
}
もちろん、そのような用途にはJavaはそもそも向いていないわけですが、Javaしか知らなかった頃はこういうちょっとしたツールを作るのにもJavaで頑張っていたので、Rubyのコードの短さにはちょっと感動しました。
セミコロンを書かなくていい
ほんとにちょっとした違いですが、セミコロンを書くのって意外と面倒ですよね。(…ですよね?)
Rubyでは書かなくていいので地味ですがうれしいです。Rubyに慣れてからは、Javaを書く時にセミコロンが抜けたりすることがあるのが困り物ですが。
変数の型を書かなくてもいい
変数に型がないのはデメリットでもありますが、それどころか変数に型がない言語で大規模なアプリケーションを開発するのは個人的にはちょっと想像ができない世界ですが、ちょっとしたスクリプトを書くくらいなら楽チンで良いです。
全てがオブジェクトなので面白い
Javaにはプリミティブ型というオブジェクトではない値が存在しますが、Rubyは全部オブジェクトです。
数値リテラルもオブジェクトなので、Javaでは見かけないような書き方ができて面白いと思いました。
10.even? # true
5.times do
puts '5回繰り返す'
end
require 'prime'
13.prime? # true
13.next # 14
-10.abs # 10
nil(Javaでいうnull)でさえオブジェクトなので、一応メソッドを呼べます。
v = nil
v.nil? # true
v.to_s # ""
v.to_i # 0
基本的にメソッドなので混乱しにくい
Pythonでは、サブルーチンが関数である場合とメソッドである場合があり、自分がやりたいことを実現してくれるサブルーチンがどちらとして用意されているのか混乱することがあります。(少なくとも私は)2
慣れれば済む話なのでPythonが悪いとは全然思ってないのですが、個人的にはRubyの仕様のほうがしっくりきました。
Pythonの例
s = '12345'
# これは関数
len(s) # 5
int(s) # 12345
# これはメソッド
s.split('3') # ['12', '45']
s.replace('3', '9') # '12945'
Rubyの例
s = '12345'
# 全部メソッド
s.size # 5
s.to_i # 12345
s.split('3') # ["12", "45"]
s.gsub('3', '9') # "12945"
配列操作が便利
謎の例ですが、例えば数値の配列で、偶数だけ抽出して重複を排除してそれぞれ2倍にした上で全部足すという操作をしたい場合、Java6では下記のような感じです。
import java.util.*;
public class Main {
public static void main(String[] args) {
Integer[] a = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5};
Set<Integer> set = new HashSet<Integer>(Arrays.asList(a));
int sum = 0;
for(int n : set) {
if(n % 2 == 0) {
sum += n * 2;
}
}
System.out.println(sum);
}
}
ちょっと長いし、わかりにくいですね。
Rubyだとこんな感じで短く書けます。
a = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
sum = a.select{|n| n.even?}
.uniq
.map{|n| n * 2}
.sum
puts sum # 12
このselectやmapといったメソッドは、一般的には高階関数(higher-order-function)として知られているものです。まあRubyなので「高階メソッド」と呼ぶべきかもしれませんが。
高階関数とは、関数を引数にとったり戻り値として返したりする関数のことです。Rubyでは関数ではなく「ブロック」という、処理の塊を値として使えるものを扱うのですが、まあ処理の塊を扱うという意味では同じことですね。
selectメソッドは、ブロック内の式の評価結果が真になる要素だけを保持する配列を返します。ここでは偶数の要素だけを抽出するのに使っています。mapメソッドは、配列内の要素1つ1つを、ブロック内の式の評価結果の値に変換した配列を返します。ここでは、各要素を2倍にするために使っています。
このようなメソッドや関数はRubyだけではなく多くの言語で実装されていますが、私はRubyで初めてお目にかかったので、最初に知ったときはRubyスゲーじゃんとけっこう感動したものです。
Javaでも、Java8で実装されたStream APIを使用すれば同様の書き方ができます。Java11ではこんな感じでしょう。
import java.util.*;
public class Main {
public static void main(String[] args) {
int[] a = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5};
var sum = Arrays.stream(a)
.filter(n -> n % 2 == 0)
.distinct()
.map(n -> n * 2)
.sum();
System.out.println(sum);
}
}
このJavaのStream APIですが、私はそれほど苦労せずにキャッチアップできた記憶があります。Rubyで「予習」していたおかげです。「ああ、Rubyのアレと似たようなことがJavaでもできるようになったのねー」てな具合で。
メソッドの命名規則が面白い
既にいくつか例が出ていますが、Rubyでは真偽の判定のために使用するメソッドは、メソッド名の最後を「?」とする慣習になっています。
# 偶数かどうか
1.even? # false
2.even? # true
# 奇数かどうか
1.odd? # true
2.odd? # false
また、オブジェクトを破壊的に変更するメソッドについては、メソッド名の最後を「!」とするという慣習があります。破壊的に変更するとは、つまりそのオブジェクトが保持している値が変わってしまうということです。
a = [6, 9, 5, 3, 7, 1, 4, 2, 8]
# 破壊的に変更しない(戻り値として返すだけ)
p a.sort # [1, 2, 3, 4, 5, 6, 7, 8, 9]
p a # [6, 9, 5, 3, 7, 1, 4, 2, 8] <- 変更されていない
# 破壊的に変更する
p a.sort! # [1, 2, 3, 4, 5, 6, 7, 8, 9]
p a # [1, 2, 3, 4, 5, 6, 7, 8, 9] <- 変更されてしまっている
このように同じ処理を行うメソッドについて、破壊的に変更する版としない版がある場合、ベースのメソッド名は同じにした上で破壊的に変更する版について「!」を付けます。破壊的に変更するメソッド全てに「!」が付くわけではないという点に注意が必要です。「!」が付いていれば破壊的に変更するメソッドだと言えますが、「!」が付いていなければ破壊的変更をしない…とは言えません。
なお、Pythonで同等のコードを書くとこんな感じです。
a = [6, 9, 5, 3, 7, 1, 4, 2, 8]
# 破壊的に変更しない
print(sorted(a)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(a) # [6, 9, 5, 3, 7, 1, 4, 2, 8] <- 変更されていない
# 破壊的に変更する
a.sort()
print(a) # [1, 2, 3, 4, 5, 6, 7, 8, 9] <- 変更されてしまっている
Pythonだと、sort()とsorted()のどちらが破壊的に変更してどちらがしないのかわかりにくいですね。どっちが関数でどっちがメソッドなのかというのもありますし。こういったところではRubyは統一感があるなあと思います。
まあRubyにしろPythonにしろ、破壊的に変更するような関数やメソッドはできるだけ使わないほうがトラブルを避けられると思いますが。
ライブラリが豊富
たとえばJSONを扱いたい場合、JavaだとJacksonなどサードパーティのライブラリを使う必要がありますが、Rubyでは標準ライブラリだけで対応できて便利です。
下記は天気予報情報を取得できるAPIを使って福岡県久留米市の天気予報の概要を表示するスクリプトです。
require 'net/http'
require 'uri'
require 'json'
uri = URI.parse('http://weather.livedoor.com/forecast/webservice/json/v1?city=400040')
json = Net::HTTP.get(uri)
result = JSON.parse(json)
puts result['title']
puts result['description']['text']
また、サードパーティのライブラリを使うにしても、Rubyの場合は「gem install <ライブラリ名>」で簡単にインストールして簡単に使えますが、JavaだとJarファイルを落としてきてコンパイル時と実行時にクラスパスを設定しないといけなくて面倒です。
JavaでもMavenやGradleを使えば済むような話ですが、ちょっとしたスクリプトを書く程度の使い方であればRubyの仕組みのほうが便利です。
(追加)コンパイルなしですぐに動かせる
Rubyに限らずスクリプト言語はどれもそうですが(それどころか一部コンパイル型の言語でも対応していることがありますが)、コンパイルなしですぐに動かせるのは地味ですが便利です。特に、ソースを修正しては動かし、修正しては動かし…を繰り返す場合は非常にありがたいです。
いの一番に書くべき内容かもしれないですが、投稿後に書き漏らしに気づいたので下に追加しました。
Rubyで驚いた機能
Rubyには、Javaでは考えられないような機能が色々あります。スクリプトを書く程度では活用できることはあまりないのですが、勉強してみて面白いと思ったので書きます。
ダックタイピングによるポリモーフィズム
ポリモーフィズムはオブジェクト指向プログラミングにおける重要な概念ですが、JavaとRubyではアプローチが全然違います。
Javaの場合は、継承やインタフェースの実装を使って実現します。
もっとマシな例はないのかと自分でも思いますが、ご容赦ください…
interface Person {
void hello();
}
class Taro implements Person {
@Override
public void hello() {
System.out.println("こんにちは!");
}
}
class Bob implements Person {
@Override
public void hello() {
System.out.println("Hello!");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Taro();
person.hello(); // こんにちは!
person = new Bob();
person.hello(); // Hello!
}
}
変数の型がPerson型であることがポイントですね。TaroクラスやBobクラスはPersonインタフェースを実装しているため、インスタンスをPerson型の変数に代入することができます。3
一方、こちらはRubyで同じように動作する例です。
class Taro
def hello()
puts 'こんにちは!'
end
end
class Bob
def hello()
puts 'Hello!'
end
end
person = Taro.new
person.hello # こんにちは!
person = Bob.new
person.hello # Hello!
Javaの例にあったPerson型に相当するものがないのがポイントです。Rubyでは変数に型がないため、なんでもかんでも代入できてしまいます。そして、その変数に対してメソッドが呼ばれた時、その時に代入されているインスタンスがそのメソッドを持っていれば実行するし、なければ実行時エラーになるという動作になっています。同じシグニチャのメソッドさえ持っていれば、型としては何の関係もなくてもポリモーフィズムが実現できます。このような型付けをダックタイピングといいます。
変数に型がないので自然とそうなるのかもしれないですが、初めて知ったときはなるほどなーと感心したものです。
ちなみに、Rubyにも継承は存在しますが、上記の通り継承を使わなくてもポリモーフィズムを実現できます。そのため、Javaの継承とは違ってRubyの継承は純粋に実装を引き継ぐための機能ということになります。同じ機能でも言語によって全然違うのですね。
ゴーストメソッド
存在しないメソッドが呼ばれた時、あたかもそのメソッドが最初から存在していたかのように振る舞うようにするテクニックです。
Rubyは存在しないメソッドが呼ばれた場合、method_missingというメソッドが呼ばれます。method_missingは既定ではNoMethodErrorを投げるだけの実装になっていますが、これをオーバーライドしてごにょごにょすれば、あたかもその存在しなかったメソッドが最初から存在していたかのように動作させることができます。
class GhostSample
def method_missing(method, *args)
if method.to_s.end_with?('=') then
str_method = method.to_s
self.instance_eval <<-"DOC"
def #{str_method}(value)
@#{str_method.chop} = value
end
def #{str_method.chop}
@#{str_method.chop}
end
DOC
self.send method, args[0]
else
nil
end
end
end
sample = GhostSample.new
sample.namae = 'Taro'
p sample.methods.filter{|m| m.to_s.start_with?('namae')} # [:namae, :namae=]
puts sample.namae # Taro
ごちゃごちゃした例ですが、要するに存在しないnamaeメソッドとnamae=メソッド(代入式)が呼ばれたら、それらを動的に生成して呼び出しています。Javaではとても考えられないような機能です。
モンキーパッチ
既存のクラスに動的にメソッドを追加したり、既存のメソッドを上書きしたりするテクニックです。
下記は任意の文字列に対して草を生やすkusaメソッドをStringクラスに追加した例です。
class String
def kusa(count = 1)
self + ('w' * count)
end
end
s = 'ワロタ'
puts s.kusa # ワロタw
puts s.kusa(3) # ワロタwww
組み込みのStringクラスすら書き換えられるのですね…
例としては出してませんが、既存メソッドの上書きすらできます。乱用は禁物です。でも面白いですよね。
特異メソッド
特定のオブジェクトに対してのみ使えるメソッドを定義します。
a = 'foo_bar_buzz'
b = 'hoge_hoge_fuga'
def a.snake_to_camel()
str = self.split('_').map{|s| s.capitalize}.join('')
str[0].downcase + str[1..-1]
end
puts a.snake_to_camel # fooBarBuzz
puts b.snake_to_camel # undefined method `snake_to_camel' for "hoge_hoge_fuga":String (NoMethodError)
snake_to_camelメソッドがaについてのみ存在し、同じStringクラスのインスタンスであるbにはありません。
どんな時に使えばいいのかはよくわからないですが、これも面白いですね。
Rubyでちょっとイケてないと思ったところ
Windowsとの相性が悪い(悪かった?)
ファイルIOが遅い(遅かった?)
私がRubyを触り始めたくらいの頃は、WindowsでのファイルIOがかなり遅かった覚えがありますし、ブログ等でもそういう意見を見かけました。
しかし、この記事を書くために直近のバージョンで簡単に検証してみた限り、Windows版とMac版でそれほど変わらないという結果になりました。(Windowsのほうが遅いのは間違いないが、記憶にあるほどの大差ではなかった)
最近のバージョンでは問題はかなり軽減されているのかもしれません。4
C拡張を含むGemをインストールするのが面倒
GemはRubyだけでなく、部分的にC言語でも書かれている場合があり、そのようなGemをWindowsにインストールするのはけっこう面倒だった覚えがあります。Windowsには、標準ではCのコンパイラがありませんからね。
私は当初はプライベートでもWindowsを使っていましたが、この辺りのめんどくささが嫌でMacに乗り換えました。私が今Macを使っているのはRubyのせい(おかげ?)です。
動作が遅い
どんなプログラムだったか書いてませんが、性能測定用の巨大なテキストファイルを生成するプログラムだったと記憶しています。IOがボトルネックになるようなプログラムです。まあ、静的な言語より遅いのは仕方ないですよね… 重い処理、特にIOの負荷が高いような処理を含むスクリプトはRubyだと遅すぎて役に立たない可能性があるため、仕方なくJavaで書くことが多いです。ツイートした当時はScalaで書いたようですが、まあJVM言語という意味では同じですね。Rubyで3時間かかってたのが、Scalaで書き直しただけで30分で終わるようになってワロタ
— dhirabayashi (@dhirabayashi64) 2016年2月6日
追記
じゃあJRuby使えば速くなるのか?とも思ったのですが、使ったことがないのでわかりません。
公式のAPIリファレンスが読みづらい
例えば下記はファイルパスを扱うPathnameクラスのドキュメントです。メソッド名が区切り文字もなく横に並べられて記述されているのですが、正直読みにくいです。
https://docs.ruby-lang.org/ja/latest/class/Pathname.html
Javaではこんな感じです。
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/Path.html
Javaのみたいに縦に並べたほうが見やすいと思うのですが。まあ、メソッドが多くて縦長になってしまうのが嫌なのかもしれないですが。
その他のイケてないところ
コンパイルがないので楽な反面、コンパイラ先生によるチェックがない、変数に型がないので全く想定外の値が代入されるかもしれない、ゴーストメソッドやモンキーパッチの乱用によってわけがわからない動作をする可能性がある、下位互換性を重視しないバージョンアップ、などなど重大な欠点もいろいろありますが、それらはメリットと表裏一体ですし、また私のようにスクリプトを書く程度の使い方しかしないならそれほど問題になることはありません。なので軽く触れるに留めます。
まとめ
雑多な内容を並べただけの文章なのでまとめも何もないんですが、この記事を書いていて下記のようなことを感じました。
- 普段使っているのと別の言語、特にパラダイムが違う言語を学ぶことは有益(今回はRuby)
- ゴーストメソッドなど、Javaにはないテクニックを学べた
- ダックタイピングなど、Javaにはない概念を学べた
- mapメソッドやselectメソッドを学んだことが、結果的にJavaのStream APIの「予習」になった
- ちょっとしたスクリプトをサクッと書くのに便利なので、実用面でも有益だった
- Javaも進化している
- いくつかJava6版とJava11版と両方のサンプルを載せたが、基本的にJava11版のほうが短く簡潔に書けている
長文にお付き合いいただき、ありがとうございました。
-
エンコーディングの扱いや異常時の動作などが違うので、完全に等価なコードというわけではありませんが。 ↩
-
コメントにて、Pythonにおいてはメソッドも関数だというご指摘をいただきましたが、ここではPythonの内部実装について言及しているわけではなく、単純にコード上でどのように書くかという話をしているとご理解ください。
それに対し、Rubyでは基本的にメソッドで統一されているのであまり混乱せずにすみます。 ↩ -
正確にはインスタンスへの参照
そして、person変数に対してhello()メソッドが呼ばれると、その時点で代入されているインスタンスのクラスで定義されているhello()メソッドが呼ばれます。Javaのポリモーフィズムはこのようにして実現されます。 ↩ -
Mac版が遅くなったという可能性も無きにしも非ずですが…なお古いバージョンでの検証は
めんどくさいのでやっていません。 ↩