Omotesando.rb #54 で『ActiveRecord のコードを読んでみる』をしてきた

Omotesando.rb #54 で LT してきました。
今回は「コードの読み方」というテーマだったせいかいつもよりもめっちゃ人が集まっていましたねー。人が多いけど懇親会でぼっちだったワイ
やっぱり「コードの読み方」っていうのは初心者から上級者まで興味があるテーマになるんですかねー。

ActiveRecord のコードを読んでみる

ActiveRecord のコードを読んでみる』ってことでスライドは使用する機能の説明だけ書いて実際にいつもどんな感じで ActiveRecord の実装を読んでいるのかのデモをしてきました。
pデバッグ出力したり grep で検索したり binding.irb を駆使したりと割と泥臭い機能を使ってコード読んでいます。
このあたり、使っているエディタによって結構違ってて RubyMine とかは高級な機能を使って便利そうにデバッグしていました。
最近 Vim の拡張はサボり気味なので Vim でもいろいろとやってみたいんですけどねえ…。
あ、あとわたしは binding.pry ではなくて binding.irb を使っているんですが『なんで binding.pry じゃないんですか』って何人かに聞かれました。
わたしも昔は binding.pry 使っていたんですが Railsbinding.pry を使うと変なところで止まっちゃうことが多くで確実に動作する binding.irb を使うようになりました。
binding.pry よりも高級ではないんですがより安定性があるから使っているって感じですねー Ruby 2.7 からは irb のかなり強化されているのでこれからに期待しています!!!

irb で実行結果を pp で出力する

irb で実行結果を pp で出力する手段がいくつかあるのでまとめ。

irb の起動オプションで変更する

irb の起動オプションに --inspect pp を追加することで pp の出力になります。

$ irb --inspect pp
irb(main):001:0> (1..10).to_h { |it| [it, it] }
 = > {1=>1,
 2=>2,
 3=>3,
 4=>4,
 5=>5,
 6=>6,
 7=>7,
 8=>8,
 9=>9,
 10=>10}

irb の起動中に変更する

conf.inspect_mode = :pp を実行することで以降の出力が pp になります。

$ irb
irb(main):001:0> conf.inspect_mode = :pp
irb(main):002:0> (1..10).to_h { |it| [it, it] }
 = > {1=>1,
 2=>2,
 3=>3,
 4=>4,
 5=>5,
 6=>6,
 7=>7,
 8=>8,
 9=>9,
 10=>10}

.irbrc で設定する場合

.irbrc ファイルで制御する場合、以下の設定を追加することで pp で出力できます。

# .irbrc
IRB.conf[:INSPECT_MODE] = :pp

pp 以外にも yamlmarshal 形式で出力することもできるので気になる人は調べてみるといいと思います。

参照

ActiveRecord で実行される sql を確認する

ActiveRecord で実行される sql を確認したい場合、 #to_sql メソッドが利用できます。

# このリレーションでレコードを読み込む場合に実行される SQL を文字列で取得する
p Blog.joins(:articles).where(articles: { name: "hoge" }).to_sql
# => "SELECT \"blogs\".* FROM \"blogs\" INNER JOIN \"articles\" ON \"articles\".\"blog_id\" = \"blogs\".\"id\" WHERE \"articles\".\"name\" = 'hoge'"

また、 puts で出力する場合は "エスケープされないのでより読みやすくなります。

puts Blog.joins(:articles).where(articles: { name: "hoge" }).to_sql
# => SELECT "blogs".* FROM "blogs" INNER JOIN "articles" ON "articles"."blog_id" = "blogs"."id" WHERE "articles"."name" = 'hoge'

ActiveSupport::TimeWithZone#+ に数値を渡したときの挙動

ActiveSupport::TimeWithZone#+ に値を渡すと『渡した時間を加算した時刻』を返します。

# 現在の時刻
current_time = Time.current
pp current_time                # => Mon, 06 Jan 2020 20:10:15 JST +09:00

# + 1日
pp current_time + 1.days      # => Tue, 07 Jan 2020 20:10:15 JST +09:00

# + 10秒
pp current_time + 10.seconds  # => Mon, 06 Jan 2020 20:10:25 JST +09:00

ActiveSupport::TimeWithZone#+ に数値を渡したときの挙動

ActiveSupport::TimeWithZone#+ に数値を渡した場合、数値を秒数として計算します。

# 現在の時刻
current_time = Time.current
pp current_time          # => Mon, 06 Jan 2020 20:11:28 JST +09:00

# + 10.seconds と同じ意味
pp current_time + 10     # => Mon, 06 Jan 2020 20:11:38 JST +09:00

これ、意図としてわかりづらいので数値はエラーにしてしまったほうがいいんじゃないですかねー。

Vim 8.2 で Vim script に -> 演算子が追加された

Vim 8.2 で Vim script に -> 演算子が追加されました。
これは f(x, y)x->f(y) という風に書くための演算子になります。
例えば

echo map(filter(range(1, 10), { -> v:val % 2 == 0 }), { -> v:val * v:val })

というようなネストしている関数呼び出しを

echo 1
\   ->range(10)
\   ->filter({ -> v:val % 2 == 0 })
\   ->map({ -> v:val * v:val })

のように記述する事ができます。
これはユーザ定義関数でも利用する事ができます。

function! s:plus(a, b)
    return a:a + a:b
endfunction

echo 1->s:plus(2)->s:plus(3)

便利。

注意

組み込み関数によっては f(x, y)x->f(y) ではなくて y->f(x) になる場合があるので注意する必要があります。
例えば printf(fmt, a)a->printf(fmt) となります。

echo 42->printf("%04x")
" output: 002a

関数によって -> の挙動が異なるの、マジで頭がおかしいと思うのでどうにかしてほしい…なんでこんなひどい仕様にしてしまったのか…。
こういうのはちゃんと統一性を保ってほしいんですがどうにかなりませんかね…。
やっぱり Vim script はクソ

Vim 8.2 で Vim script の辞書のキーの定義が簡略化できるようになった

Vim script で辞書を定義する場合、次のようにキーを文字列リテラルで定義していました。

let dict = { "one" : 1, "two" : 2, "three" : 3 }
echo dict
" => {'one': 1, 'two': 2, 'three': 3}

Vim 8.2 では上記のような定義を以下のように定義する事ができます。

" #{ から始めることで `キー名: 値` で定義できる
let dict = #{ one: 1, two: 2, three: 3 }
echo dict
" => {'one': 1, 'two': 2, 'three': 3}

こんな感じで辞書リテラル#{ から始めることで キー名: 値 で定義できるようになります。
キー名には ASCII文字 + 数字 + - + _ が使用できます。

let dict = #{ one: 1, two_key: 1, three-key: 2, 444: 4 }
echo dict
" => {'one': 1, 'three-key': 2, '444': 4, 'two_key': 1}

また 444: 4 とした場合、キーは "444" になるので注意する必要があります。
これ自体は便利なんですがなぜ #{} なんていうクソダサいリテラルにしてしまったのか…。もっといい書き方はなかったんですかねえ。

C++20 で指示付き初期化子を使ったキーワード引数

C++20 で指示付き初期化子が実装されました。
指示付き初期化子はクラスを初期化する時に『任意のメンバ変数』を指定して初期化する事ができます。

struct pos{
    int x;
    int y;
    int z;
};

// .メンバ変数名 = で初期化できる
pos p{ .x = 1, .y = 2, .z = 3 };

これを利用してキーワード引数っぽい関数を定義してみました。

#include <iostream>

const auto disp = [] {
    // 引数を受け取るクラスを定義しておく
    // クラス定義を公開したくないので致し方なくクロージャ化
    struct pos_t { int x; int y; int z = 0; };

    return [](pos_t pos) {
        std::cout << "x = " << pos.x << std::endl;
        std::cout << "y = " << pos.y << std::endl;
        std::cout << "z = " << pos.z << std::endl;
    };
}();

int
main(){
    // 指示付き初期化子を利用してキーワード引数っぽく渡す
    disp({ .x = 1, .y = 2 });
    std::cout << "---" << std::endl;
    disp({ .x = 1, .z = 3 });

    return 0;
}
/*
output:
x = 1
y = 2
z = 0
---
x = 1
y = 0
z = 3
*/

引数として受け取るクラスを定義しておいて、そのクラスに対して指示付き初期化子で値を渡すようにしています。
本当は

auto
disp(struct { int x; int y; int z = 0; } pos) {
    // ...
}

みたいに引数で直接匿名クラスが定義できればいいんですが、こういう書き方はできないので仕方なくラムダのスコープ内でクラスを定義するようにしています。
あと指示付き初期化子の制限で『指示子の順序が宣言の順序と一致しない』とエラーになるので次のようにもかけません。

// Error: 指示子の順序が宣言の順序と一致しない
disp({ .y = 2, .x = 1 });

うーんキーワード引数で順序によってエラーになってしまうのはちょっと微妙ですねえ。
C++ で気持ちよくキーワード引数をかける日は来るのだろうか…。

参照