やわらかテック

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

idの絞り込みにfindを使わずfind_by!を使っていたのはなぜなのか

コードレビューをしていていた日のこと。あるテーブル(users)からidで絞り込んでレコードを1件取得する際に、以下のようなコードが頻出していることに気づきました。

User.find_by!(id: params[:user_id])

処理の内容としては、idカラムでusersテーブルから該当のレコードを1件取得するというものですが、存在しなかった場合に例外が発生するようにfind_by!を使っています。
不思議なのは「idで絞り込んで、存在しなければ例外を発生させたい」というのはfind関数で事足りる処理なのに、あえてfind_by!(id: params[:user_id])としている点です。

api.rubyonrails.org

find関数を知らないのかな」と思ったのですが、railsチュートリアルにも登場する関数なので知らない可能性は低いです。もしくは「find関数だとパッと見て何で絞り込んでいるのかが分かりにくい」からではないかとも考えられます。
言いたいことは分かるのですが、先程のコードのようにparams[:user_id]のような値が引数に指定されているのであれば、idで絞り込んでいるのは自明なため、やっぱりfind関数でいいじゃんと思ってしまいます。

idと一緒に他のカラムも検索する可能性がある」というのはidが識別子として仕事してないので、論外です。

また、rubocopのスタイルガイドにもidの絞り込みを行う場合は、findを使うことが推奨されています。

# bad
User.where(id: id).take!

# bad
User.find_by_id!(id)

# bad
User.find_by!(id: id)

# good
User.find(id)

github.com

find_by_id!なんてものがあるの初めて知りましたが、すでに廃止された関数のようでした。

細かいことですが、自明なことをあえて別の書き方をする必要性について考えさせられました。結論としては、僕の意見がどうかではなく、rubocopが推奨しているものが模範解答でいいかなと思います。なので、これからもidの絞り込みにはfind関数を使っていきます。

参考文献

Rspecのモック(double)はどのように作られているのか

普段の業務でrspecのモック機能には非常にお世話になっています。使い方は非常に簡単で、既存クラスに定義された関数の振る舞いを変更することが出来ます。 (※実際に既存クラスの定義が書き変わるわけではありません)

class Hoge
  def foo
    'foo!'
  end

  def bar
    'bar!'
  end
end

# ref: https://relishapp.com/rspec/rspec-mocks/docs/basics/test-doubles
RSpec.describe "A test double" do
  it "returns canned responses from the methods named in the provided hash" do
    dbl = double('Hoge', :foo => 'this is foo!', :bar => 'this is bar!')
    expect(dbl.foo).to eq('this is foo!')
    expect(dbl.bar).to eq('this is bar!')
  end
end

すごいですね。どうやって作られているのでしょうか。最近はOSSのコードを読む機会が多いのでせっかくなので、rspecのモック(double)がどのように作られているのかコードを読みながら深ぼってみました。

www.okb-shelf.work

結論としては、Doubleクラスのインスタンスを作成しclass_execdefine_methodを使用してシングルトンクラスの振る舞いを定義することで、モックを作成していました。

処理の流れ

Doubleクラスのインスタンス作成

まず呼び出されたdouble関数ではExampleMethodsdeclare_double関数が呼び出されます。またdeclare_double関数は第一引数で指定されたクラスのインスタンスを作成しています。

def double(*args)
  ExampleMethods.declare_double(Double, *args)
end

rspec-mocks/example_methods.rb at e643b40800eb1b7859215647c035cfd2ed8cce08 · rspec/rspec-mocks · GitHub

module ExampleMethods
  # @private
  def self.declare_double(type, *args)
    args << {} unless Hash === args.last
    type.new(*args)
  end
end

ここまででDoubleクラスのインスタンスが作られました。また、Doubleクラスは内部でTestDoubleモジュールを読み込んでおり、実質、Doubleクラスのインスタンス作成はTestDoubleモジュールの呼び出しとなっています。

class Double
  include TestDouble
end

連続的なインスタンス作成

次にTestDoubleの実装を見てみるとinitialize関数(インスタンス作成時に呼び出される)で2つの引数を定義しています。第一引数にはモックを作成したいクラス名(eg: Hoge)。第二引数の可変長引数には関数の振る舞いを記録したハッシュ(eg: :foo => 'this is foo!', :bar => 'this is bar!')が指定されます。

module TestDouble
  # Creates a new test double with a `name` (that will be used in error
  # messages only)
  def initialize(name=nil, stubs={})
    @__expired = false
    if Hash === name && stubs.empty?
      stubs = name
      @name = nil
    else
      @name = name
    end
    assign_stubs(stubs)
  end

  def assign_stubs(stubs)
    stubs.each_pair do |message, response|
      __mock_proxy.add_simple_stub(message, response)
    end
  end

  def __mock_proxy
    ::RSpec::Mocks.space.proxy_for(self)
  end
end

第一引数のクラス名は@nameに記録され、第二引数のハッシュはassign_stubs関数に渡されます。この関数内では指定されたキーバリューのペアごと:RSpec::Mocks.space.proxy_for(self)に定義されたadd_simple_stub関数が呼び出されます。この部分の呼び出し階層がかなり深いため、情報を割愛しますが、最終的に::RSpec::Mocks.space.proxy_for(self)TestDoubleProxyクラスのインスタンスを返します。

class TestDoubleProxy < Proxy
  def reset
    @method_doubles.clear
    object.__disallow_further_usage!
    super
  end
end

rspec-mocks/proxy.rb at e643b40800eb1b7859215647c035cfd2ed8cce08 · rspec/rspec-mocks · GitHub

TestDoubleProxyクラスはProxyクラスを継承しており、resetの振る舞いだけオーバーライドしています。先程のadd_simple_stub関数はProxyクラスに実装された関数です。

class Proxy
  def initialize(object, order_group, options={})
    ensure_can_be_proxied!(object)

    @object = object
    @order_group = order_group
    @error_generator = ErrorGenerator.new(object)
    @messages_received = []
    @messages_received_mutex = Mutex.new
    @options = options
    @null_object = false
    @method_doubles = Hash.new { |h, k| h[k] = MethodDouble.new(@object, k, self) }
  end

  def add_simple_stub(method_name, response)
    method_double_for(method_name).add_simple_stub method_name, response
  end

  private

  def method_double_for(message)
    @method_doubles[message.to_sym]
  end
end

add_simple_stub関数ではmethod_double_for関数の呼び出しを通して@method_doublesに記録されたバリューを取得しています。initialize関数の@method_doublesHash.newにはブロック引数が指定されており、この指定があることでキーに対応するバリューが存在しなかった場合にブロック引数に指定された挙動が実行されます。今回は初回呼び出しのため、MethodDouble.new(@object, k, self)が実行されてMethodDoubleクラスのインスタンスが作成されます。

なのでMethodDoubleクラスに定義されたadd_simple_stub関数が呼び出される訳ですね。階層が深くて頭がこんがらがってきました。

シングルトンクラスとメタプログラミング

ここに来るまでに多くのクラスを中継してきましたが、いよいよ核部分であるMethodDoubleに辿り着きました。

  • ExampleMethods
  • Double(TestDouble)
  • TestDoubleProxy
  • MethodDouble

長くなるため、部分的に割愛しています。呼びだされたadd_simple_stub関数から色々な関数を中継していきます。重要なのはdefine_proxy_method関数です。他の関数やインスタンス変数はmutex等を使ってすでに記録した関数の振る舞いやモックとなったクラスに実装されている振る舞いなどを記録しています。

  • add_simple_stub
  • setup_simple_method_double
  • define_proxy_method
class MethodDouble
  attr_reader :method_name, :object, :expectations, :stubs, :method_stasher

  # @private
  def initialize(object, method_name, proxy)
    @method_name = method_name
    @object = object
    @proxy = proxy

    @original_visibility = nil
    @method_stasher = InstanceMethodStasher.new(object, method_name)
    @method_is_proxied = false
    @expectations = []
    @stubs = []
  end

  def add_simple_stub(method_name, response)
    setup_simple_method_double method_name, response, stubs
  end

  def setup_simple_method_double(method_name, response, collection, error_generator=nil, backtrace_line=nil)
    define_proxy_method

    me = SimpleMessageExpectation.new(method_name, response, error_generator, backtrace_line)
    collection.unshift me
    me
  end

  def define_proxy_method
    return if @method_is_proxied

    save_original_implementation_callable!
    definition_target.class_exec(self, method_name, @original_visibility || visibility) do |method_double, method_name, visibility|
      define_method(method_name) do |*args, &block|
        method_double.proxy_method_invoked(self, *args, &block)
      end
      # :
    end

    @method_is_proxied = true
  rescue RuntimeError, TypeError => e
    # :
  end
end

rspec-mocks/method_double.rb at e643b40800eb1b7859215647c035cfd2ed8cce08 · rspec/rspec-mocks · GitHub

define_proxy_method関数でdefinition_target関数が呼ばれることで、おそらく最後のobject_singleton_class関数が呼び出されます。

def definition_target
  @definition_target ||= usable_rspec_prepended_module || object_singleton_class
end

def usable_rspec_prepended_module
   :
end

def object_singleton_class
  class << @object; self; end
end

object_singleton_class関数の内部を見てみると何やら見慣れない記述が...。これは@objectのシングルトンクラスが取得できるそうで、Rubyでクラスメソッドを定義する際にclass << selfと書きますが、これも記述したクラスのシングルトンクラスに対して実装を行うという宣言のために記述していたということに今になって気づきました。

stackoverflow.com

class Hoge
end

singleton = class << Hoge; self; end
singleton.class_exec do
  define_method(:bar) { "bar!" }
end

puts Hoge.new.singleton_class.bar # bar!

そして、取得したシングルトンクラスに対してclass_exec関数とdefine_method関数を用いてメタプログラミングで関数の振る舞いを定義していきます。内部でproxy_method_invoked関数が呼び出されていますが、非常に処理が難解です。クラス変数の配列に値を記録したり削除したりして、振る舞いを管理しているようです。気になる方はぜひコードを見てみてください。

これで作成したモックが呼び出された時の振る舞いが定義されました。前回調べた、travel_toと同じような実装がされていました。 特定の振る舞いを固定したい場合はシングルトンクラスを使うのがよくあるパターンなのかなと感じました。

www.okb-shelf.work

参考文献

vec型に対してiter関数からslice::Iterが返ってくるのはなぜか

Rustの学習を進めています。前回に引き続き、イテレーターの章で気になった結果を発見しました。vec型の値に対してinto_iter関数を実行した場合にはvec型に定義された構造体vec::into_iter::IntoIterが返るのですが、iter関数を実行すると...slice型に定義された構造体slice::iter::Iterが返るのです。

fn type_of<T>(_: T) -> String{
    let a = std::any::type_name::<T>();
    return a.to_string();
}


fn main () {
  println!("type: {}", type_of(vec![1,2,3]));
  println!("type: {}", type_of(vec![1,2,3].into_iter()));
  println!("type: {}", type_of(vec![1,2,3].iter()));
}

// type: alloc::vec::Vec<i32>
// type: alloc::vec::into_iter::IntoIter<i32>
// type: core::slice::iter::Iter<i32>

自分のような素人発想では「iter関数を実行した時もvec::Iterが返ればいいんじゃないの?」と思ってしまいます。しかしながら、結果は先ほどの通り。一体なぜなのでしょうか。

www.okb-shelf.work

結論としては、slice型とvec型で同じようなデータを保持しており、Iter構造体を使い回すことが出来るのではないかという判断に至りましたが、決定的な情報がなかったため、1つの説という視点でご覧ください。

Iter構造体とIteratorトレイト

Iter構造体は以下のように3つのフィールドを持ちます。ptrはおそらくpointerの略称でしょう。

pub struct Iter<'a, T: 'a> {
    ptr: NonNull<T>,
    end: *const T, // If T is a ZST, this is actually ptr+len.  This encoding is picked so that
    // ptr == end is a quick test for the Iterator being empty, that works
    // for both ZST and non-ZST.
    _marker: PhantomData<&'a T>,
}

rust/iter.rs at master · rust-lang/rust · GitHub

それぞれのフィールドがどういう意図を持って用意されているものかは現時点では正確な判別は不能ですが、イテレーションの処理をするのに必要な情報だということは推測出来ます。

次に、vec型のiter関数が構造体slice::iter::Iterを返しているコードを探してみます。fn iterでコードを検索してみても、それっぽい定義は見つかりません。…ということで、次はIteratorトレイトが構造体slice::iter::Iterに対して実装されているかどうか検索してみます。

rust/iterator.rs at master · rust-lang/rust · GitHub

こちらは明示的にIterator for Iterと定義された箇所はありませんでしたが、どうもマクロを利用して構造体Iterに対してIteratorトレイトを実装しているようです。

impl<'a, T> Iterator for $name<'a, T> {
            type Item = $elem;
            // 長いので省略
}

rust/macros.rs at master · rust-lang/rust · GitHub

マクロの呼び出し

iterator! {struct Iter -> *const T, &'a T, const, {/* no mut */}, {
    fn is_sorted_by<F>(self, mut compare: F) -> bool
    where
        Self: Sized,
        F: FnMut(&Self::Item, &Self::Item) -> Option<Ordering>,
    {
        self.as_slice().windows(2).all(|w| {
            compare(&&w[0], &&w[1]).map(|o| o != Ordering::Greater).unwrap_or(false)
        })
    }
}}

rust/iter.rs at master · rust-lang/rust · GitHub

これで構造体slice::iter::IterIteratorトレイトを実装していることが分かりました。また、マクロが定義されたファイルにはnext関数の定義もあったので、次はこちらを見てみます。

fn next(&mut self) -> Option<$elem> {
  // 長いので省略
  unsafe {
      assume(!self.ptr.as_ptr().is_null());
      if !<T>::IS_ZST {
          assume(!self.end.is_null());
      }
      if is_empty!(self) {
          None
      } else {
          Some(next_unchecked!(self))
      }
  }
}

rust/macros.rs at master · rust-lang/rust · GitHub

今回の問題の調査として、この関数を深掘りしたのですが、かなり情報量が多くなるため、今回は理解に必要な部分的な情報のみをピックアップします。このnext関数と関数内で呼ばれる処理の多くはself(Iter構造体)ptrフィールドを使って様々な処理が実装されています。つまりptrの情報がフィールドに記録されていれば、Iter構造体は生成可能で、イテレーションの処理が行えるのではないかと考えられます。そこで、slice型とvec型が持つ情報を見てみます。

※厳密にはptrとendと_markerの3つ

slice型とvec型

この2つの型は微妙に異なります。slice型はRustではDST(Dynamically Sized Types)として定義されており、コンパイル時にサイズが分からない型です。それに対してvec型はなんとDSTではありません。またarray型も同様にDSTではなくコンパイル時にサイズが分かっている型です。

「え、でもvec型って可変長で値の追加が出来るんじゃ...?」と思いますが、vec型は要素の格納をヒープ領域へ行います。array型はスタック領域に連続的に要素を確保していますが、slice型は状態によって、スタックかヒープを選択するそうです。

名前 DSTかどうか 可変長かどうか 要素の確保領域
array いいえ いいえ スタック
slice はい いいえ スタックかヒープ
vector いいえ はい ヒープ

その上で、slice型とvector型が持つ情報を見てみます。まず、slice型はドキュメントの情報から読み取るに2つの値を持ちます。

  • pointer to the data
  • length
a slice is a two-word object, the first word is a pointer to the data, and the second word is the length of the slice.

Arrays and Slices - Rust By Example

そしてvec型は3つの値を持ちます。

  • pointer to the data
  • length
  • capacity

Vectors - Rust By Example

            ptr      len  capacity
       +--------+--------+--------+
       | 0x0123 |      2 |      4 |
       +--------+--------+--------+
            |
            v
Heap   +--------+--------+--------+--------+
       |    'a' |    'b' | uninit | uninit |
       +--------+--------+--------+--------+

Vec in std::vec - Rust

結論

決定的な情報がなかったので、僕の個人的な結論にはなりますが、slice型とvec型はどちらもptrフィールドを持っており、Iter構造体を生成可能です。ここで、vec型とslice型で別のIter構造体を定義しなかったのは、slice型のIter構造体でvec型でも同様にイテレーションが出来るため、共通化をしたかったのではないかという判断に至りました。

少し自分の主観が入りましたが、調査の結果、上記を一旦の結論としました。 もし、詳しい情報をご存知の方がいましたら、せび教えていただきたいです。

参考文献