Tech Racho エンジニアの「?」を「!」に。
  • 開発

週刊Railsウォッチ(20170512)Rubyの不思議な挙動「シャドウイング」、コードレビュー作法を定めるDanger gemほか

こんにちは、hachi8833です。Rails 5.1でrails newすると忘れた頃にspringに邪魔されます。

GWをはさんだ2週間ぶりのRailsウォッチ、いってみましょう。

Rails: aggregated_resultsで任意の種類の引数を取れるよう修正

y-yagiさんという方の「なるようになるブログ」は、Railsのコミットログをひたすら読む記事を毎日のようにアップするというすごいブログなのですが、そのy-yagiさんがRailsにちょくちょくプルリクを送っていることに昨日初めて気づきました。

コミットログでお名前を少しだけ追いかけてみたところ、少なくとも今年3月ぐらいから続々淡々とプルリクしていて、kamipoさんに迫りそうな勢いです。上は現時点で最新のPRです。
いつもありがとうございます。

Rails: secretsに関する記述を修正

-building on top of the [sekrets](https://github.com/ahoward/sekrets) gem.
+inspired by the [sekrets](https://github.com/ahoward/sekrets) gem.

これ、私も記事を書いていて引っかかってしまいました。Sekrets gemがRailsに組み込まれたのではなく、Secretsにインスパイアを受けて独自に実装したというのが正解でした。

BPS WebチームのakioさんがRails 5.1でrails newしてみて「Sekrets gemが入ってませんねー」と知らせてくれたことでわかりました。ありがとうございます。

Rails: #map#flat_mapに変更

-        chain.map(&:scopes).flatten
+        chain.flat_map(&:scopes)

Rubyスタイルガイドでも推奨されている#flat_mapへの置き換えです。

参考: Rubyスタイルガイドを読む: #mapと#flattenではなく#flat_mapを使うこと

Rails: ActionMailer::Baseからのrequire "active_support"の脱落を修正

# actionmailer/lib/action_mailer.rb
+require "active_support"
 require "active_support/rails"

どこで抜けたのかと思って履歴を追ってみたところ、3f8409でaction_mailer/base.rbからrequire行が引越したのを見つけました。

+require 'active_support/core_ext/class'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/array/uniq_by'
+require 'active_support/core_ext/module/attr_internal'
+require 'active_support/core_ext/module/delegation'
+require 'active_support/core_ext/string/inflections'

そしてその前からrequire "active_support"はなかったようです。

-require 'active_support/core_ext/class'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/array/uniq_by'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/string/inflections'

Rails: RuntimeReflection#alias_nameに型変換を追加

以下が改修点です。

-        Arel::Table.new(table_name)
+        Arel::Table.new(table_name, type_caster: klass.type_caster)

そのままArel::Tableも見てみました。

def initialize(name, as: nil, type_caster: nil)
      @name    = name.to_s
      @type_caster = type_caster
...
end

type_casterは以下にありました。

...
    class Connection # :nodoc:
      def initialize(klass, table_name)
        @klass = klass
        @table_name = table_name
      end
...

Rails: bind_parambind_attributeActiveRecord::TestCaseに切り出す

kamipoさんによるテストコードの修正です。

-        result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new.to_sql}", nil, [[nil, post.id]])
+        result = @connection.select_all("SELECT * FROM posts WHERE id = #{bind_param.to_sql}", nil, [[nil, post.id]])
-        binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
+        binds = [bind_attribute("id", "10", Type::Integer.new)]

テストコードで上のように重複している多数のnewをリファクタリングして、以下のようにtest_case.rbに集約しています。

# activerecord/test/cases/test_case.rb
+    def bind_param
+      Arel::Nodes::BindParam.new
+    end
+
+    def bind_attribute(name, value, type = ActiveRecord::Type.default_value)
+      ActiveRecord::Relation::QueryAttribute.new(name, value, type)
+    end

一同でチェックしながら「たぶんテストコードのファイルの複製を繰り返しているうちにnewがかぶってきたんだろうねー」という声がありました。
kamipoさんの地道な修正に頭が下がります。

mini_racer: 軽快なJS V8エンジン gem

mini_racerは名前からうかがえるように、GoogleのJavaScriptエンジンであるV8をRailsで使うときの定番gemであるtherubyracerの縮小版です。

試しにmini_racerをインストールしてみると、libV8のバージョンがtherubyracerより随分進んでます。

READMEのベンチマークを見る限りはかなり軽快そうです。

# https://github.com/discourse/mini_racer より
$ bundle exec ruby bench.rb mini_racer
Benching with mini_racer
mini_racer minify discourse_app.js 9292.72063ms
mini_racer minify discourse_app_minified.js 11799.850171ms
mini_racer minify discourse_app.js twice (2 threads) 10269.570797ms

sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb node
Benching with node
node minify discourse_app.js 13302.715484ms
node minify discourse_app_minified.js 18100.761243ms
node minify discourse_app.js twice (2 threads) 14383.600207000001ms

sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb therubyracer
Benching with therubyracer
therubyracer minify discourse_app.js 171683.01867700001ms
therubyracer minify discourse_app_minified.js 143138.88492ms
therubyracer minify discourse_app.js twice (2 threads) NEVER FINISH

Killed: 9

ところで、therubyracerといえば以下の記事で知られているように昔のRailsでひどい目にあった方が結構いました。RailsのV8周りについては、mini_racerに置き換えるにしても一応注意する方がよさそうです。

私もGemfileのtherubyracerのコメントアウトをむやみに解除しないようにします。

sassc-ruby: 高速Sassコンパイラ gem

sassc-rubyはSassのコンパイルをC言語ライブラリで行えるgemです。

# https://github.com/sass/sassc-rub より
[1] pry(main)> Benchmark.bm { |bm| bm.report { Rails.application.assets["application.css"] } }
       user     system      total        real
   1.720000   0.170000   1.890000 (  1.936867)

# Using sass-rails

 [1] pry(main)> Benchmark.bm { |bm| bm.report { Rails.application.assets["application.css"] } }
       user     system      total        real
  7.820000   0.250000   8.070000 (  8.106347)

sassc-rubyのリポジトリでは★50個程度で「ありゃ」と思ったのですが、READMEに書いてあったsassc-railsの方は★400個達成しています。こちらはまだ2年ほどの比較的若いgemです。

急いでインストールする必要はないと思いますが、Sassの性能改善が不可避になったときに検討してみてもよいかもしれません。

Oj(Optimzed JSON) gem 3.0.0リリース(RubyWeeklyより)

高速を謳っているJSONパーサーgemです。akioさんがその場で早速動かしてみました。

require 'json'
v = {:a => {:b => :c}}
s = JSON.dump(v)                        # => "{\"a\":{\"b\":\"c\"}}"
JSON.parse(s, :symbolize_names => true) # => {:a=>{:b=>"c"}}

require 'oj'
v = {:a => {:b => :c}}
# s = Oj.dump(v, :mode => :compat)      # => "{\"a\":{\"b\":\"c\"}}"
s = Oj.dump(v)                          # => "{\":a\":{\":b\":\":c\"}}"
Oj.load(s)                              # => {:a=>{:b=>:c}}

# oj はいったん文字列にしてしまったキーはシンボルにはできないと思いきや :symbol_keys オプションでシンボルに戻せる
# ただ値は文字列のまま
s = Oj.dump(v, :mode => :compat)        # => "{\"a\":{\"b\":\"c\"}}"
Oj.load(s, :symbol_keys => true)        # => {:a=>{:b=>"c"}}

それにしても、JSONパーサーってどうして言語ごとにあんなにたくさんあるんでしょうか。せっかくJSONの仕様が定まっているのに実装がこんなに多いのは「とにかく今この問題を切り抜けたい」「既存のJSONパーサーの使い勝手が気に入らない」といった理由なのでしょうか。

なお私はbashスクリプトでjqをときどき使っています。癖は強いですが速いです。

bootsnap gem: Railsに新たなキャッシュを追加して高速化(RubyWeeklyより)

大規模Railsアプリをキャッシュで高速化するgemだそうです。
READMEで「Beta-quality」と書かれていることもあり、まだ発展途上のようですが、READMEはかなりみっちり書かれています。

やっていることは「$LOAD_PATHなどのスキャンを止める」「RubyのバイトコードコンパイルやYAMLをキャッシュする」など、きわどい感じです。一同から「えぇー、#requireにパッチ?!」と声が上がりました。

# https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L48 より
class << Kernel
  alias_method :require_without_cache, :require
  def require(path)
    if resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
      require_without_cache(resolved)
    else
      raise Bootsnap::LoadPathCache::CoreExt.make_load_error(path)
    end
  rescue Bootsnap::LoadPathCache::ReturnFalse
    return false
  rescue Bootsnap::LoadPathCache::FallbackScan
    require_without_cache(path)
  end
...

READMEで参考として挙げられていた中にRubyコミッターであるko1さんのライブラリがありました。

コマンドがkakidasuだったりと「こういう名前って英語圏ではどうなんでしょ?」と聞かれてしまいました。私は英語村出身ではありませんが、類推が効かなくてつらそうな感じですね。

ところで、READMEに書いてあるyomidasugemspeck以外のどこにも見当たりませんでした。もしかして修正漏れでしょうか?

Rubyのシャドウイングで生じたバグ(RubyWeeklyより)

この挙動には一同かなり驚きました。Rubyがこんなふうに振る舞うなんて。akioさんも早速ぶん回してみました。

# https://thomasleecopeland.com/2017/04/20/shadowing-bug-in-the-wild.html
class Foo
  def buz
    42
  end
  def bar
    unless buz
      buz = 21
    end
    buz
  end
end

p Foo.new.bar # => さて何が返るでしょうか?

Ruby 2.4.1での実行結果は以下でした。

はい、ご覧のとおりnilが返ります。

元記事で「シャドウイング」と呼んでいるこの動作は、Rubyの公式ドキュメントにもしっかり書かれています。知らずに踏みそうで怖いですね。ドキュメントをざっと翻訳してみました。

Ruby 公式ドキュメントより

ローカル変数は、代入の発生時ではなくパーサーが代入を検出したときに作成される。

a = 0 if false    # ここではaに何も代入されないことに注意

p local_variables # => [:a]

p a               # => nil

メソッド名とローカル変数が類似していると、次の例のようにコードで混乱が生じることがある。

def big_calculation
  42 # なにしろ「42」なので計算にものすごく時間がかかる
end

big_calculation = big_calculation()

ここではbig_calculationへの参照はすべてローカル変数と解釈され、キャッシュされる。メソッドを呼ぶにはself.big_calculationを使う。

上のように空の丸かっこ()をつけるかself.などのレシーバを明示的に使うことで、メソッドを呼び出せる。ただしレシーバを明示すると、メソッドがpublicでない場合にNameErrorが生じることがある。

もうひとつの紛らわしい例は、以下のように後置のifを使う場合である。

p a if a = 0.zero?

上を実行するとtrueではなく「undefined local variable or method 'a'」が出力される。Rubyはifの左にある単独のaをまず解析する。解析の時点では代入が行われていないため、Rubyはaをメソッドであると仮定する。その後aへの代入が行われると、Rubyはこれをローカルメソッドへの参照と仮定する。

この紛らわしい動作は、式が見た目とは違う順序で実行されていることによる。最初にローカル変数への代入が行われ、続いて存在しないメソッドを呼び出そうとしている。

Bundlerの新しいオプション

bundle update --conservativeというオプションを指定すると、gemのバージョンの進み方を極力少なくできるそうです。いいことを知りました。

見つけにくいスペルミスが原因でエラーになったお(RubyWeeklyより)

gaugesという名前を誤ってguagesにしてしまい、長年そのまま動いていたが、コード改修後のデプロイでビルドできなくなったことで発覚したというストーリーです。

レビュアーは何人もいたのに誰一人このスペルミスに気づかなかったという事実に著者も呆然としたそうです。

StringIO: ファイルのように振る舞うオブジェクト

StringIOを使って、ファイルのように振る舞うオブジェクトを使う方法を解説しています。StringIOはRubyの公式ライブラリです。

テストコードで「ファイル入出力をテストしたいが実際にファイルは作って欲しくない」場合などに便利そうです。

途中で「StringScannerは名前は似てるけど違いますよー」と書いてあります。

Friendly_id: URLの無味乾燥な数字をActiveRecordの値に置き換えるgem(RubyFlowより)

以下のように、URLのパラメータをActiveRecordの値に置き換えてくれるそうです。日本人にはあまり関係なさそうかなと思いました。
morimorihogeさんから「github.com/:usernameのようないわゆるslugでの使いみちがありますよー」と補足いただきました。

http://example.com/states/4323454
↓
http://example.com/states/washington

一同で見ながら「ブログタイトルなんかをこれで処理したら、タイトルが変わったときにURLがユニークじゃなくなっちゃうんじゃ?」というツッコミがありました。

RailsをCapybaraでリダイレクトさせる方法(RubyFlowより)

拍子抜けするほど短い記事です。英語の記事を読み慣れていない方はぜひチャレンジしてみてください。

Rails製CMSアプリのベストはどれだ(RubyFlowより)

Rails製CMSアプリっていったいいくつあるのでしょうか。こんなに嫌になるほど種類があって分散してしまうと「だったらWordPressでいいよ」となってしまいそうです。

Rubyで学ぶSOLIDオブジェクト指向(RubyFlowより)

みっちり書かれていてよさそうな感じの記事です。SOLIDについては以下をどうぞ。

我、明示的でないプログラミングを愛する(RubyWeeklyより)

DHHのエッセイです。

ざっとしか読んでませんが、「コードで常に何もかも明示的に書かなきゃいけないんだとしたらそんなのゴメンだ」「ActiveRecordのモデルみたいに簡潔な方がいいに決まってる」「コードを明示的に書くのが好きというヤツは、似たような定型コードを何度も何度も書かされるのが好きってことなんだろ、そのことを認識しないでそう言ってるんだとしたら不実じゃないのか」といったDHH節炸裂です。

原文のvalueは技術用語の「値」ではなく、本来の「値打ち」「価値」の方の意味ですね。

Danger: プルリクの書式や手順を揃えるgem(GitHub Trendingより)


http://danger.systems/より

CIでのコードレビュー作法を定めるgemだそうです。以下のような感じでコメントを付けます。


http://danger.systems/より

GitHub/GitLab/BitBucketなどのメジャーなリポジトリに対応しています。設定ファイルはそのまんまDangerfileです。以下のようなオプションがあります。

tableにコメント
message("アプリにgemを3つ以上追加したな(゚Д゚)ゴルァ!!")
CIワーニングの宣言
warn("CHANGELOGに何も書いてないぞ(゚Д゚)ゴルァ!!")
CIブロッキングエラーの宣言
fail("linterは大層お怒りのご様子です")
tableの下にmarkdownを出力
markdown("## ")
diff行にmarkdownを出力
warn("自分の名前書けや", file: "CHANGELOG.md", line: 4)

どなたか試してみたい勇者はいらっしゃいませんか。

Rooby->Goby: 100% Go言語で書かれたRuby実装

この間ウォッチで取り上げたgorubyはイマイチでしたが、このGobyはわたしもつい「もしや」と思ってしまいました。3か月ほどで早くも★が1100越えで、毎日のようにコミットが増えています。MacならHomebrewのcaskでもインストールできます。

ご多分に漏れず多くの機能が未実装で(まだirbもない)、最適化もこれからという状態ですが、ちょっと触ってみた限りでは動作が非常に安定しており、しかも「Ruby Under Microscope」やVMコードへのトランスパイルなどを下敷きにしたまっとうなつくりのようです。動作の猿真似ではなくちゃんと「すべてがクラス」になっています。

たとえば以下はRubyとGobyのどちらでも動きます。goby -cでバイトコードを出力できます(バイナリではなくテキスト)。

module Foo
  def ten
    10
  end
end

class Baz
  def ten
    1
  end

  def five
    5
  end
end

class Bar < Baz
  include(Foo)
end

b = Bar.new

puts(b.ten * b.five)

Gobyの立ち上がりはRubyに比べてかなり速いですが、現時点の機能の少なさと、外部ライブラリを読み込んでないオールインワンバイナリという点を差し引いておく方がよさそうです。個別のメソッドにはまだまだ遅いものもあるようです。

元々Roobyという名前でしたが、おととい 「Roobyという名前は米国人にとってRubyと発音がまったく同じなので紛らわしいんだよね: もちっと違う名前にしない?」というIssueが上がり、その翌日本当に名前をGobyに変えてしまいました


今週は以上です。

関連記事

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Github Trending

160928_1701_Q9dJIU


CONTACT

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