やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

OSSのコードを読んでたらコードの書き方が変わった

最近はRubyとRubyOnRailsを使って仕事をしています。RubyやRailsは日本語の情報量が特に多く、やりたいことの多くはGoogleで調べると既に誰かがやっていたり、ライブラリがあったりと誰かが作ったありがたい...コードを使わせて頂くことがよくあります。

しかしながら、時にはサンプルコードを元に少し手を加えてみたり、見たこともない書き方が使われているようなことがあります。今まで、そういったコードを見た時、スケジュールの都合や関心の無さから、深掘りして「これ、どういうコードなのかな」ということをサボっている時がありました。

最近では、以前のような無茶なスケジューリングが減り、友人との会話を通して興味を持たなくなること、どんなことでも関心を持ってやってみることの重要性を感じており、分からないこと、知らないことについては時間の許す限り調べてみるという動きをしていました。

それがタイトルにもあるOSSのコードを読んでみるという行動につながりました。

事の発端

ActiveRecordというORMのライブラリを使っている時のこと、外部のテーブルをincludesメソッドを使って、結合(※正確には絞り込みの有無で結合するか2つクエリを発行するか変わる)させてソートをしようとしたらエラーになりました。

User.includes(:branches).order('users.id ASC, branches.id ASC')
  User Load (2.8ms)  SELECT "users".* FROM "users" ORDER BY users.id ASC, branches.id ASC
  User Load (1.2ms)  SELECT "users".* FROM "users" ORDER BY users.id ASC, branches.id ASC LIMIT 11
=> #<User::ActiveRecord_Relation:0x5654>

# 評価してみるとエラーに...
User.includes(:branches).order('users.id ASC, branches.id ASC').first
  User Load (1.5ms)  SELECT "users".* FROM "users" ORDER BY users.id ASC, branches.id ASC LIMIT 1
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "branches"
LINE 1: ...ECT "users".* FROM "users" ORDER BY users.id ASC, branches.i...

回避策としては以下のように、orderメソッドを2回呼び出してあげればエラーになりません。

User.includes(:branches).order(id: :asc).order('branches.id ASC')

いつもであれば「動いたからヨシ!」としていましたが、上記に書いたように調べてみることにしました。調査のためにActiveRecordorder関数の実装を見てみました。

github.com

見慣れない構文や構造に戸惑いましたが、1つの結論に辿り着くことが出来ました。 内部で実行されている処理を1つ1つ繋げて実行してみると、外部参照をしているはずのbranchesが消えてしまうのです。

[User.sanitize_sql_for_order('user.id asc, branches.id asc')].flat_map { |a| a.is_a?(Hash) ? a.keys : a }.map { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }
=> ["user"] # ここは["user", "branches"]になる想定だった

この結論が正しいかは保証できませんが、自分の中で納得できる答えが見つかり、大満足でした。

その後...

このようなことが何度かあり、OSSのコードを何度か読む機会がありました。そしてある日、いつものようにコードを書いていると、自分が書いたコードの違いに気づきました。記述したのは配列からcsvを生成するwrite_csvという関数です。

def write_csv(save_path, &)
  string_io = CSV.generate(headers: true, encoding: Encoding::SJIS, row_sep: "\r\n") do |csv|
    yield csv
  end
  File.open(save_path, 'w', encoding: Encoding::SJIS, :invalid => :replace, :undef => :replace) do |file|
    file.write(string_io)
  end
end

いつもだったら、きっと以下のように書いてた

def write_csv(records, save_path)
  s_io = string_io(records)
  File.open(save_path, 'w', encoding: Encoding::SJIS, :invalid => :replace, :undef => :replace) do |file|
    file.write(s_io)
  end
end

def string_io(records)
  CSV.generate(headers: true, encoding: Encoding::SJIS, row_sep: "\r\n") do |csv|
    records.each do |record|
      csv << record
    end
  end
end

関数化の狙いはエンコーディングなどのオプション指定を共通化させたいという点です。その上で、1つの関数を呼び出せばcsvの生成が完了してくれると嬉しいです。しかしながら、いつも書くやり方だと、ヘッダーの挿入やデータ形式が変わった場合に元のコードに手を加えるか、新たに別の関数を定義する必要があります。

その点では、上のコードでは生成処理のコア部分はyieldを使って呼び出し側に責務を委譲させて、多くのケースに対応できる上にオプション指定が共通化出来ます。このyieldを使う方式はOSSのコードでよく見かけた記述で、いつの間にか使っていたようです。

最後に

「達人のコードを読む」ことの重要性についてよく問われていますが、マジだなと思いました。自分の中でレパートリーが増えて、今まで読めなかったコードが読めるようになっていく感覚はヤバいです。何よりも世界中で使われているコードの内部を知れるというのがアツい。

退屈なコードばかり読んでいる、書いているという方には本当にオススメなので、ぜひやってみてください。