はじめに
この記事は Ruby Advent Calendar 2016 の 17日目です。
去年、Rubyのオススメの機能7選 という記事を書いて、バズって、はてブホッテントリ入りしました。それに気をよくして、今年も同じようなネタで勝負したいと思います。
みなさん、ストック、いいね、ブクマお願いします。
Range#bsearch 、 Array#bsearch
Array や Range はよく知られているクラスかと思います。その中でもあまり使われていないかもしれないメソッドとして bsearch メソッドがあります。
二分探索(binary search) が簡単に使えます。y = f(x)
があったとき y
から x
を高速に求めることができます。
たとえば、下記では、二乗根を求めています。
def square(x)
x * x
end
def root(y)
(0..Float::INFINITY).bsearch do |x|
y < square(x)
end
end
p root(2) #=> 1.4142135623730951
ゼロからプラスの無限大の間で二分探索しています。
この square の例では、Math.sqrt
という自明な逆関数があります。
しかし例えば、消費電力量から電気代を求める計算のように逆関数が自明でない場合もあります。
そんな場合でも Range#bsearch を使うと簡単に電気代から消費電力量を求めることができます。
Double Splat
しばしばよく使われるテクニックとして Splat があります。
実は下記の例で、find_element
の引数として Splat が登場します。
*
よりも無名かもしませんが、ハッシュの Splat を実現するテクニックとして **
があります。
たとえば、私も開発に関わっている電力比較サイトエネチェンジには、下記のようなコードがあります。
def zuttomo_3_energy_charges(kw)
summer = {month_from: 7, month_to: 9}
winter = {month_from: 10, month_to: 6}
stage1 = {kwh_from: 0, kwh_to: kw * 130}
stage2 = {kwh_from: kw * 130, kwh_to: nil}
[
{**summer, **stage1, rate: 16.91},
{**summer, **stage2, rate: 18.37},
{**winter, **stage1, rate: 15.37},
{**winter, **stage2, rate: 18.26},
]
end
これは夏と夏以外で単価が違う上、契約電力に応じて第一段階と第二段階の境界が切り替わるという複雑な仕様に対応するためのコードの例です。
Hash#merge を使っても同様のことは実現できますが、**
を使うことで記述がより簡潔になっています。
キーが Symbol である必要があるなど制約がありますが、うまくユースケースにマッチすると便利です。
[]
、 []=
の再定義
Ruby では演算子の再定義が可能です。
特に、 []
と []=
演算子は使い方次第で、便利に思います。
例を見てみましょう。
require 'selenium-webdriver'
class SeleniumFirefoxClient
attr_accessor :webdriver
def initialize
@webdriver = Selenium::WebDriver.for :firefox
@webdriver.manage.timeouts.page_load = 10
end
def [](name)
find_element(name: name).attribute("value")
end
def []=(name, value)
elem = find_element(name: name)
elem.clear
elem.send_keys(value.to_s)
end
def find_element *args
webdriver.find_element *args
end
def get *args
webdriver.get *args
end
end
client = SeleniumFirefoxClient.new
client.get("http://localhost:3000/")
p client["order[name]"] #=> ""
client["order[name]"] = "cuzic"
p client["order[name]"] #=> "cuzic"
上記の例では SeleniumFirefoxClient#[] で、ある name を持った DOM 要素の value を取得し、
SeleniumFirefoxClient#[]= である name を持った DOM 要素に対して、テキストを打ち込んでいます。
この例では、単なるテキストボックスであることを想定していますが、もう少し複雑な記述をすれば、
リストボックスやラジオボタンなどにも対応可能です。
[]
や []=
を使うことによって、より DSL 的になっています。
値を設定するコードは特に読みやすくなっていると感じます。
Set とそのメソッド群
Ruby の標準ライブラリには、Set
というクラスがあります。
Set
を使うことで、集合の和、積、差などを使えます。
下記は Set
を使った FizzBuzz の例です。
require 'set'
multiples_of_three = (1..100).select do |i|
i % 3 == 0
end.to_set
multiples_of_five = (1..100).select do |i|
i % 5 == 0
end.to_set
multiples_of_both = multiples_of_three & multiples_of_five
1.upto(100) do |i|
case
when multiples_of_both.include?(i)
puts "FizzBuzz"
when multiples_of_five.include?(i)
puts "Buzz"
when multiples_of_three.include?(i)
puts "Fizz"
else
puts i
end
end
上記の例は少し primitive ですが、例えば私は下記のような場合に Set を使っています。
- あるディレクトリにあって、 zip ファイルにないファイルを列挙したいとき
- CSV ファイルにあって、DB にない行を見つけてそれだけインポートしたいとき
- まだ done になっていないレコードに対してだけ処理を継続したいとき
- ActiveRecord オブジェクトにはあるが、あるページ上にない入力要素を見つけたいとき
Struct
Ruby の標準ライブラリに Struct
があります。
Struct を使うと initialize や dig 、inspect など便利メソッドが最初から定義されている分、ラクができます。
具体的な例を見てみましょう。
class Tree < Struct.new(:left, :value, :right)
def visit
left.visit if left
puts value
right.visit if right
end
end
tree =
Tree.new(
Tree.new(
Tree.new(nil, :a, nil),
:b,
Tree.new(
Tree.new(nil, :c, nil),
:d,
Tree.new(nil, :e, nil)
)
),
:f,
Tree.new(
nil,
:g,
Tree.new(
Tree.new(
nil,
:h,
nil
),
:i,
nil
)
)
)
tree.visit #=> a から i まで順に表示される
puts tree.dig(:left, :right, :value) #=> d
ここでは Struct
を継承した Tree
型を使っています。
Struct.new(:left、:value、:right)
となっているので、
Tree.new(left, value, right)
という形式で引数をとって
Tree オブジェクトを生成することができます。
def initialize
や attr_accessor
を記述する必要がない点が利点です。
また、ここでは Struct#dig
も使っています。Hash#dig
はよく知られているかもしれませんが、Struct#dig
はあまり知られていないかもしれません
この例は
tree&.dig(:left)&.dig(:right)&.dig(:value)
に相当します。 tree&.dig(:left)
は left が定義されていなければ nil
を返し、定義されていれば、 tree.left
の結果を返します。
ここでは root から考えて、左の木、右の木の値を取り出そうとしているので d
が表示されています。
ほかにも Struct には便利なメソッドがありますし、足りなければ、上記の例のように自分で実装するのも可能です。
オブジェクトの構築コストが高くなるため、性能面では不利ですが、プロトタイプ作成や使い捨てのコードでは便利だと思います。
Pathname
みなさんもお使いの Ruby on Rails には Rails.root
というメソッドがあります。みなさんもお使いになったことがあるでしょう。
実はこの Rails.root
は Pathname
を返します。
Pathname
には様々な便利メソッドが存在します。それを紹介します。
Pathname#children
.
と ..
を除いて、そのディレクトリのすべてのディレクトリ・ファイルを、ディレクトリ付きで返します。
Pathname#exist?
File.exist?(pathname) に相当。その名前のディクトリ・ファイル等が存在するときに true を返す。
Pathname#find
Find.find(pathname) に相当。そのディレクトリ以下のディレクトリ・ファイルを再帰的に深さ優先探索を行う。
Pathname#mtime
File.mtime(pathname) に相当。最終変更日時を返す。
Pathname#sub_ext(ext)
拡張子を別の文字列で置き換える。
pathname = Pathname("source.c")
p pathname.sub_ext(".o") #=> #<Pathname:source.o>
Pathname#read
IO.read(pathname)
に相当。ファイル全体を読み取り、文字列として返す。IO.read
と同様に読み取るバイト数などの引数をとることもできる。
Pathname#write(body)
IO.write(pathname, body)
に相当。ファイルに body を書き込む。IO.write
と同様に offset
などの引数をとることもできる。
ワンライナー
Ruby はワンライナーとして使うときも便利です。
特に便利な用法をいくつか紹介します。
NKF
NKF をインストールしなくても Ruby があれば日本語の文字コードの変換が簡単にできます。
たとえば、下記であれば標準入力を UTF-8 に変換します。
ruby -r nkf -pe '$_=NKF.nkf "-w", $_'
ruby -p
では、各行の内容が一旦、 $_
に格納され、
スクリプトの実行後の $_
の内容が出力されます。
特定の正規表現の範囲の出力
下記はユーザ名の一覧を出力する例です。 あまり知られていないかもしれませんが、ワンライナーでは $_ =~
は省略可能です。
cat /etc/passwd | ruby -ne 'puts $1 if /^(.+?):/'
-ane
の例
区切り文字を使って分割することもできます。ある行を分割した結果は、$F
の中に格納されます。
$ cat /etc/passwd | ruby -F: -ane 'puts $F.first'
なお、この場合、末尾の改行が邪魔になることがあります。
そういうときは、 -anle
とl
を追加で指定することで改行を取り除けます。
sed と同じような使い方
下記は、拡張子 scss
を css
に置き換える例です。
$ find app/assets/stylesheets/ | ruby -pe 'gsub /scss$/, "css" '
ここでは Kernel.gsub
を使っています。ruby -p
、ruby -n
のときにだけ定義される非推奨なメソッドですが、今でも使えます。
$_.gsub
とほぼ同じですが、置換が発生したときは、$_
の内容を置き換えますので、 -p
で使うと便利です。
HTTP サーバ
Ruby では、 HTTP サーバをワンライナーで簡単に立てることもできます。
これは un
ライブラリの一部です。
$ ruby -run -e httpd .
[2016-12-17 09:12:10] INFO WEBrick 1.3.1
[2016-12-17 09:12:10] INFO ruby 2.3.1 (2016-04-26) [x86_64-linux]
[2016-12-17 09:12:10] INFO WEBrick::HTTPServer#start: pid=13173 port=8080
この例ではカレントディレクトリをドキュメントルートとして、 8080番ポートで待ち受ける形で HTTPサーバを起動します。
-run
というのは un
ライブラリを require
するという意味です。
un
は Unix コマンドの基本ライブラリを代替するポータブルな機能を提供するためのライブラリですが、その中に HTTPサーバもあるのです。
最初の3行だけ出力
下記のコードで 最初の3行だけを出力することができます。
ここで if 1..3
は if (1..3).include?($.)
に等価です。
すなわち、行番号が1から3 の場合は true
になります。
$ cat Gemfile | ruby -ne 'print if 1..3'
source 'https://rubygems.org'
gem 'rails', '5.0.0.1'
おわりに
じっくり読むと、これまで気づいていなかったさまざまな便利な機能が存在していることに気づきます。
私自身、この投稿をするときに初めて気がついた機能がいくつもありました。
Ruby は非常に進化が速い言語ですので、これまで存在しなかった機能がどんどん追加されていきます。
常に情報をキャッチアップしていくことが大切です。