railsのwith_optionsについて

railsのwith_optionsについて、以下の記事で取り上げられているようにオプションが上書きされてしまうということを知らずに結構時間を溶かしてしまったので、勉強がてらrailsのコードを読んでいこうと思います。(rubyのProcとかもわかっていないので一緒に勉強していきます。)

blog.serverworks.co.jp

環境

ruby2.7.0
rails 5.2.0

準備

以下のようなコードを想定して読んでいこうと思います。

class Post < ActiveRecord::Base
  with_options presense: true, if: -> { body.present? } do
    validates :title
    vaildates :phone_number, if -> { country == 'ja' }
  end
end

読んでいく

with_options

まずは本命のwith_optionsの方を見て行きます。 rails/with_options.rb at 5-2-stable · rails/rails · GitHub
こちらはコードとしてはたった6行しかありませんね。メインはOptionMergerのほうなのかなと思いつつ、まずはこちらを見ていこうと思います。

class Object
  def with_options(options, &block)
    option_merger = ActiveSupport::OptionMerger.new(self, options)
    block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
  end
end

まず、with_options というのがObjectクラスに対して追加で定義しているインスタンスメソッドであることがわかります。(こういうのをモンキーパッチって言うんですね)

続いて引数として options&blockを受け取ってます。今回のコードは以下のような(){}が省略された形になっているので、options{presense: true, if: -> { body.present? }}が入っていて、それに続くProcオブジェクトが&blockに入っている形になります。

with_options({presense: true, if: -> { body.present? }}) do
  ...
end

そしてそのoptionsActiveSupport::OptionMergerに渡してインスタンスoption_mergerの生成を行っています。
このインスタンスは後で追うことにして先に進むと、Procクラスのインスタンスメソッドであるarityが出てきます。これは以下のようにProc オブジェクトが受け付ける引数の数を返します。

> proc = Proc.new{|a| a}
> proc.arity
# => 1
proc2 = Proc.new{|a,b,c| a}
proc2.arity
# => 3
> l = lambda { 1 }
> l.arity
# => 0

with_optionsではそれが0かどうかで処理を分けています。つまり、Procオブジェクトが引数を取る場合は先程のoption_mergerを引数にとって、そのProcオブジェクトを実行する。一方引数を取らない場合は、option_mergerのコンテキストの中で実行する。という形になります。 今回の例では引数を取らない場合に当たりますね。

それぞれの処理について見ていきます。
まずはoption_merger.instance_eval(&block)についてです。引数の先頭に&をつけることでブロックを引数として渡しています。つまり、blockとして渡ってきたProcオブジェクトを再度instance_evalに引数として渡しているということになります。 次にinstance_evalですが、これはeval族の一つで、今回でいうとOptionMergerインスタンスであるoption_mergerselfとしてそれに続くブロックの評価を行います。これにより、ブロック内でインスタンス変数にアクセスすることができます。
これら2つのことからinstance_eval(&block)option_mergerのコンテキストにおいて、渡したブロックを実行しているということになります。以下のような感じです。

class Foo
  def initialize
    @foo = 'This is a instance of Foo'
  end  
end

def hoge(&block)
  f = Foo.new()
  f.instance_eval(&block)
end

hoge do
  p @foo
end  
"This is a instance of Foo"
# => "This is a instance of Foo"

続いて、ブロックが引数を取る場合の処理であるblock.call(option_merger)についてですが、こちらはoption_mergerを引数としてこのブロックを実行しています。今回はこちらのロジックは通過しません。ちなみにブロックが引数を取る場合というと以下のような場合です。

> (0..5).each do |n|
>   p n  
> end  
0
1
2
3
4
5
# => 0..5

> def hoge(&block)
>   yield 'a', 'b', 'c'
> end  
> hoge do |x, y, z|
>   p x, y, z
> end  
"a"
"b"
"c"
# => ["a", "b", "c"]

以上がwith_optionsとなります。わかったこととしてはoption_mergerのコンテキストで渡されたブロックを実行するということです。ですが、これだけではなぜオプションが上書きされてしまうのかというのがよくわかりませんね。。。 なのでOptionMergerの方について見ていこうと思います。option_merger.instance_eval(&block)を想定して見ていきます。

ActiveSupport::OptionMerger

OptionMergerの実装もかなり小さめでした。 rails/option_merger.rb at 5-2-stable · rails/rails · GitHub

# frozen_string_literal: true

require "active_support/core_ext/hash/deep_merge"

module ActiveSupport
  class OptionMerger #:nodoc:
    instance_methods.each do |method|
      undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
    end

    def initialize(context, options)
      @context, @options = context, options
    end

    private
      def method_missing(method, *arguments, &block)
        if arguments.first.is_a?(Proc)
          proc = arguments.pop
          arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
        else
          arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
        end

        @context.__send__(method, *arguments, &block)
      end
  end

まずクラスの定義の最初に以下のようにインスタンスメソッドを消していってます。

instance_methods.each do |method|
  undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
end

これによって以下のようにインスタンスメソッドが限られる形になります

[1] pry(main)> ActiveSupport::OptionMerger.instance_methods
=> [:html_safe?,
 :pry,
 :`,
 :__binding__,
 :with_options,
 :unloadable,
 :require_or_load,
 :load_dependency,
 :require_dependency,
 :pretty_print_instance_variables,
 :pretty_print,
 :pretty_print_inspect,
 :pretty_print_cycle,
 :class,
 :class_eval,
 :debugger,
 :byebug,
 :pretty_inspect,
 :object_id,
 :__send__,
 :__id__,
 :instance_eval]

(こういうメソッドの定義とか削除とかもeachイテレーションで回せるのか)
そしてinitializeメソッドによって引数として渡されるcontextoptionsを同名のインスタンス変数に格納しています。 今回の例で行くと、@contextにself、つまりwith_optionsを使おうとしてるActiveRecordを継承したクラス(Post)を、@optionsにはwith_optionsに渡しているオプション(presense: true, if: -> { body.present? })を渡しています。
そして最後にprivate methodとして method_missingを定義しています。このメソッドはBasicObjectクラスに定義されているメソッドでNoMethodErrorが発生した際に呼び出されるものになります。ここではこのメソッドをオーバーライドしている形ですね。

class Foo
  def method_missing(name, *args)
    p name
    p "There is no such method."
  end  
end

f = Foo.new()
f.no_method
:no_method
"There is no such method."
# => "There is no such method."

また、引数もBasicObjectの場合はメソッド名とその引数の2つだけなのに対して、今回オーバーライドすることで、ブロックも受け取れるようにもしているようです。
その中で何をやっているのかというと、argumentsの最初の要素がProcであれば、ラムダ式argumentsにpushする。このときのラムダ式の内容としては、ラムダ式の引数をそのProcに渡して実行した結果を@optionsにdeep_mergeするというもの。一方、Procでなければ、argumentsの最後の要素がto_hashメソッドを呼び出せるものかどうかに依って、呼び出せる場合はargumentsの最後の要素を@optionsにdeep_mergeした上でargumentsにpushし、呼び出せない場合は@optionsをduplicateしたものをpushするというもの。(ロジックを文字起こししただけですね(汗))
そして、これらのうちどちらかの操作を終えた後、再度 __send__メソッドによって@contextに対して、つまり今回の例で行くとPostクラスにおいて同じメソッドを呼び出しています。

まとめ

以上をまとめると、まずoption_mergerのcontextにおいて&blockで渡したブロックが実行されます。今回でいうとblockの中身は以下2つのvalidatesメソッドになります。

validates :title
vaildates :phone_number, if -> { country == 'ja' }

このvalidatesメソッドはoption_mergerにおいて定義されていませんので、オーバーライドされたmethod_missingを通ります。
一つ目に関してはmethod:validatesが入り、argumentsには[:title]が入ります。そして、arguments.first.is_a?(Proc)にも(arguments.last.respond_to?(:to_hash)にも当てはまらないため、最終的には

arguments << @options.dup
@context.__send__(method, *arguments, &block)

という処理が実行されます。ここで確認しておくと、@contextには(Postが、@optionsにはpresense: true, if: -> { body.present? }が入っています。 なので結果的には、 Post内で validates :title, presense: true, if: -> { body.present? }したことと同じになります。

続いて、二つ目に関してはmethod:validatesが入り、argumentsには[:phone_number, if -> { country == 'ja' }]が入ります。そして、arguments.first.is_a?(Proc)には当てはまりませんが、(arguments.last.respond_to?(:to_hash)には当てはまるため、最終的には

arguments << @options.deep_merge(arguments.pop)
@context.__send__(method, *arguments, &block)

という処理が実行されます。 そして、ついにここで@options内if: -> { body.present? }if -> { country == 'ja' }がマージされてしまい、前者の条件が消えてしまうということになります。 なので結果的にPost内でvalidates :phone_number, presense: true, if: -> { country == 'ja' }したことと同じになります。

感想

上記のように一つづつ追っていくと勉強になりますね。あんまり理解できていなかったブロックに関しても学ぶことができてよかったなと思います。

机を買う?いやいや,作るものでしょ.

この記事はfreee 20卒内定者 季節外れのアドベントカレンダー 8日目の記事です.

前回の記事はこちら.ショートカットキーって知ってるだけでちょっとお得な感じしますよね.あと,分割キーボードいいですね.キーボード入力してるときって(とくにラップトップで)姿勢悪くなりがちなので分割タイプ使ってみたいなぁと思ってたんですよね.来年お古でいいから貸してくれないかなぁ.

なぜ今日か

突然ですが,僕が今日を選んだのには理由があります.

それは...今日が誕生日だから! (*^∇^)_∠※☆PAN! おめでとう!

はい,ということで自己紹介の9割を終えたわけなんですが,流石にこれだけだと何もわからないので残り1割の自己紹介していこうと思います.(まだ,名前も書いてなかった.)

自己紹介

改めまして,こんにちは,2020年度よりfreee株式会社でエンジニアとしてお世話になりますサヨと申します.よろしくおねがいします. 今は福岡に住んでおりまして下の写真のような景色が一望できるドいなk...とてものどかなキャンパスに通っています.加えて人混みが苦手なので来年から東京で暮らすことに今から怯えています.

f:id:aki34:20191008181345j:plain
とても”のどか”ですね.
学生時代は部活(陸上)がメインでちょこちょこバイトしたり,ほんのちょっと勉強したりしていて,最近は研究をやっています.分野としてはコンピュータビジョンという分野で画像を使って色々とやってます.まぁ最近よく聞く「AI」とか「人工知能」とかで流行っている分野ですね.日本だとあまり馴染みのない名前かもしれませんが,分野としては世界的に注目を集めています. そんな感じなのでWeb界隈とかに関してはド素人ですので,絶賛勉強中で「はじめての~」とか「~入門」とかっていう書籍を買ってもらっています.(前回スエキくんも書いていましたが,freeeでは内定者向けに書籍の購入をサポートしてくれます.) 趣味は写真撮ったり,映画観たり,本読んだり,まぁありきたりなやつですね.

みなさんどんな机使ってますか?

そんなこんなで基本的に平日は学校にいるのですが,最近,家で作業することもままあるようになり,一人暮らし6年目にして初めて(勉強)机がほしいなぁと思うようになりました. もちろんテーブルはあるのですが長時間地べたに座ってると腰が痛いんですよね...

そこで先日,机の購入を検討しました.
Amazon楽天市場を見たり,Google先生Pinterestで調べたり,ナフコニトリに行ったりとまぁ色々探したわけですが,ふと部屋を見て思いました.
置く場所ないやん...
しかも机を買う=椅子を買うということになり費用もかさむなぁと.

そんなときPinterestで「スタンディングデスク」をおすすめされました.
ご存知の方も多いかと思いますが読んで字のごとく立って使う机のことですね.これなら本棚の上に机が来るように置けて省スペースですし,座り続けるのは体に悪いという研究もあるので最高の選択なのではないかと思いました. ところがどれも高価なんですよね...

どうしようかなぁと悩みながら色々調べているとGoogle 検索のなかに「スタンディングデスク 自作」という文字が. そのリンクを開いてみるとblog記事がたくさん引っかかってきます.
これだけやっている人がいるならまぁ自分にもできるだろうと考えちゃったわけです.

そこからは早かったです.
まずは自分の身長にあった高さを調べて(今どき簡単に計算できますね.),ゼロから作るのは大変だなぁと考え,3段ボックスの上に置く形で作ることに. 2x4材とシェルフリンクスと天板を買ってきて(シェルフリンクスっていうのは下の写真の黒いやつで脚と天板をつなげる役割のやつです),組み立てて,完成! (ちなみに「組み立てて」の5文字は1文字あたり1時間半ぐらいの重みがあります)

f:id:aki34:20191008220816j:plain:h200f:id:aki34:20191008220900j:plain:h200
組立途中&組立後
f:id:aki34:20191008220952j:plain:w300
完成!
最後に色塗っていい感じに仕上げました(もうちょい濃ゆい感じにしたかったけどまぁいいや).
f:id:aki34:20191008221052j:plain
色塗りまでしたらいい感じに馴染みますね

という感じで正味2日かけて机を作りました.

作って1ヶ月ぐらい経つんですが,感想としては(自作机とスタンディングデスクについて),

  • 愛着が湧く
  • 高さ丁度いい
  • スタンディングデスクだと自然と脚を動かすので考え事しやすい
  • 椅子のある普通のタイプの机と遜色なく使える
  • 愛着がすごく湧く

という感じで非常に気に入っています.

当然いくつかデメリットもあります.

  • 俺専用
  • 一日中は使えない(脚が疲れてくるので)

1つ目はまぁそれが目的で作ったのでいいとして,2つ目は結構クリティカルな問題ですね. ポジティブに捉えれば小休憩を自分に強制するということにもなりますけどね. そして,webを調べてみるとまぁネガティブ記事がいくつか出てきます. 出てきますが,目的と使う時間などを適当に選べばいいツールになるのではないかと僕は思っています.

さいごに

こうやって何かを作って,フィードバックして,そこからまた,よりよいものを考えていく,作っていくっていうのは楽しいですよね. そんな風に来年度から自分が関わったサービスに愛着を持って「カイゼン」を繰り返していければいいなぁと思っています.

ちなみにスダンディングデスクの相場は2万円ぐらいですが,今回作った机は3段ボックス込みでも5000円ぐらいでできました.安上がりですね. そこから愛着分引いたら実質0円ですね.(何言ってるんだ?)

ここまで読んでいただきありがとうございました.

次は10月11日,せみちゃんでーす.

「実例で学ぶRaspberryPi電子工作」を始めてみた

以前から気にはなっていたものの中々手を出していなかったRaspberry Pi.
最近Raspberry Pi4が登場したことをきっかけに買ってみよう!と思いました. でも最新版だとWebで検索してもあんまり情報ないかなぁなどと考え今回はRaspberry Pi 3 ModelB+を購入しました.

これにセンサつけていろいろ遊びたいなぁと思っています.

何はともあれ初心者なのでまずは「真似ぶ」ところから始めようと思い,「実例で学ぶRaspberryPi 電子工作」を図書館で借りてきて動かし始めたところです.

ジャンパー線やらLEDライトやらはこちらを買いました.

2章 Raspberry PiでLEDを点滅させてみよう(Lチカ)

まずは電子工作の基本(らしい)であるLチカをやってみます. この書籍をベースにやっているとプログラムがついてくるのでとっつきやすくていいですね. 詳細は書籍を見てもらうのがいいと思うので割愛するとして,配線,実行結果はこんな感じになります.
抵抗は330Ωのものを用いています.
Raspberry Pi側ではGND( - )とGPIO25( + )を使っています.

f:id:aki34:20190805173714j:plain
配線図.RaspberryPi側はGNDとGPIO25を使用.

気をつけること

ブレッドボードから飛び出してショートすることが無いようにLEDや抵抗の端子をカットする必要があります. その際,LEDの端子についてはもともとアノード( + )とカソード( - )によって長さが異なっています(写真一番左). ですのでその状態を維持しながらカットを行わないといけません(写真左から2番目). 一方,抵抗についてはそのような長さの違いはありませんので均等に切ってブレッドボードにいい感じに収まるようにするのがベストでしょう(写真右2つ).

f:id:aki34:20190805172350j:plain
LEDと抵抗の端子カットの様子(LEDについては+/-によって長さに気をつける必要がある)

学んだこと

  • LEDの点滅に関してはものすごく簡単に実行できる!
  • 配線も接触に気をつけていれば簡単!

  • (RaspberryPiとは関係ないけど,)はてなブログに動画をUpするにはYoutubeTwitterで動画をUpする必要がある...

気になったこと

書籍の中で流せる電流について制限がある,という記述があるのですが,電流の限界まで用いた回路だとどのようなものが考えられるのか気になりました.

これを応用してやりたいこと

光タイマーとか作れそう.
スイッチとかつけて反射神経の測定とかもできるかも?