Railsガイドにきちんと目を通して新しい知識を得る - Active Support コア拡張機能編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Support コア拡張の章です。

railsguides.jp

deep_dup

リンクはこちら

dupでは、コピーした配列の中身がgsub!などで変化する場合、コピー元の配列の中身まで影響しますが、deep_dupでは影響を受けないようにできます。
このあたりは意図せぬバグを混入させてしまわないよう、きちんと理解しておく必要があるなと思いました。

try

リンクはこちら

&.tryは似ていますが、存在しないメソッドを指定した際に、前者はNoMethodErrorを返し、後者はnilを返します。
&.try!は挙動自体は同じようです。
ただし、try!&.と比べると遅いようなので、使うメリットがあまりなさそうにも思います。
メソッドが存在しないケースにnilを返したいならtryで、エラーを返したいなら&.を使うのが良いのかなと思いました。

コード 戻り値
object.try(:not_exist_method) nil
object.try!(:not_exist_method) NoMethodError
object&.not_exist_method NoMethodError

acts_like?でオブジェクトが特定の型のように振る舞うかどうかを判断する

リンクはこちら

acts_like?で、そのオブジェクトが特定のクラスのように振る舞うかを確認できます。

DateTime.new.acts_like?(:time)
#=> true

クラスにacts_like_xxx?を定義することで、acts_like?(:xxx)でtrueを返すようにできます。

class Dummy
  def acts_like_string?; end # メソッドの中身は関係ない(定義されているかどうか)
end

Dummy.new.acts_like?(:string)
#=> true

to_param

リンクはこちら

ActiveRecordインスタンスto_paramを使うと、デフォルトでは:idの値を返すようです。

book = Book.first
#=> <id: 2, title: "本1",content: "内容1",...>

book.to_param
#=> "2"

配列に対してto_paramすると、/でjoinするのも知らなかったです。

[book, false, nil, true].to_param
#=> "2/false//true"

to_query

リンクはこちら

引数をキーとし、レシーバにto_paramしたものを値としたクエリ文字列を返してくれます。
知ってたらスマートに書けそうな場面もありそうです。

book.to_query(:book) # Railsガイドの例では引数は文字列でしたが、hashでもOKでした。
#=> "book=2"

true.to_query('admin')
#=> "admin=true"

rubyのto_jsonrailsのto_json

リンクはこちら

rubyjsonライブラリでのto_jsonと、ActiveSupportの提供するto_jsonは結構異なるようです。
例えば、onlyなどのオプションはrubyでは使えません。
恥ずかしながら、あまり違いを意識していなかったので、勉強になりました。

  • irb環境で実行
hash = { a: 1, b: 2 }
#=> {:a=>1, :b=>2}
hash.to_json(only: :a)
#=> "{\"a\":1,\"b\":2}"
hash = { a: 1, b:2 }
#=> {:a=>1, :b=>2}
irb(main):003> hash.to_json(only: :a)
#=> "{\"a\":1}

警告や例外の抑制

リンクはこちら

silence_warnings, enable_warnings

特に設定しないと$VERBOSEfalseとなっており、重要な警告のみ出力します。
silence_warningsを使うと、ブロック内のコードでは警告が発生しなくなります。($VERBOSEnilが設定される)

silence_warnings do
  CONSTANT = 'constant'
  CONSTANT = 'CONSTANT'
end
# 特に警告は表示されない

enable_warningsのブロック内では、$VERBOSEtrueになり、すべての警告が表示されるようになります。

suppress

suppressのブロック内では、引数に指定した例外の範囲であれば例外を出さないようにできるようです。

suppress(TypeError) do
  1 + nil
  p 'abc'
end
# 例外は発生しないが、次の行が実行されるわけでもない
suppress(NameError) do
  1 + nil
end
#=> TypeError
# 指定した例外と異なるので例外が発生する

attr_internal

リンクはこちら

attr_internalを使ってライブラリのサブクラスとの名前の衝突を避ける工夫があるようです。
これを使って定義した属性値は、_から始まるインスタンス変数に保管されます。

class Parent
  attr_internal :dummy
  def initialize(str)
    self.dummy = str
  end
end

parent = Parent.new('abc')
#=> <Parent: @_dummy="abc">

module_parent, module_parent_name, module_parents

リンクはこちら

module_parentはModuleのクラスを返し、module_parent_nameはstringを返します。
module_parentsObjectクラスまで遡って、クラスの配列を返します。

X::Y::Zのmoduleに対しての戻り値

method 戻り値
module_parent X::Y
module_parent_name 'X::Y'
module_parents [X::Y, X, Object]

anonymous?

リンクはこちら

無名モジュールに対してはtrueを返すとのこと。

delegate_missing_to

リンクはこちら

ごそっとdelegateできる機能のようです。
丸ごとdelegateしなくてはいけないケースは設計上多くはなさそうですが、使えばスッキリかけますね。

redefine_method

リンクはこちら

すでに定義したいメソッドと同じ名前のメソッドが存在している場合にdefine_methodだと警告を出す*ところ、redefind_methodは名前の通り再定義を考慮されたメソッドなので、警告は発生しません。

  • デフォルトの重要な警告のみを表示する$VERBOSE = falseの状態だと、出ない警告のようです。
class Sample
  def hello
    'original hello'
  end
end

enable_warnings do # ブロック内はすべての警告を表示($VERBOSE = true)
  Sample.define_method(:hello) do
    'define hello'
  end
end
#=> warning: method redefined; discarding old hello
#=> warning: previous definition of hello was here
#=> 上書き自体はされるが、警告が表示される

enable_warnings do # ブロック内はすべての警告を表示($VERBOSE = true)
  Sample.redefine_method(:hello) do
    'redefine hello'
  end
end
#=> 警告は表示されず、上書きされる。

class_attribute

リンクはこちら

class_attributeはざっくり知っていたものの、細かいオプションや知らない仕様もあったので勉強になりました。

class A
  class_attribute :x
end

A.x = 'A'

A.x
#=> "A"
A.new.x
#=> "A"

インスタンスで使えなくするには、instance_reader: false, instance_write: false, instance_predicate: falseを使用可能。
?のメソッドも使えるようになります。

A.x = false
A.x?
#=> false

Stringの拡張

リンクはこちら

rawヘルパーとhtml_safe

html_safeを使うより、rawを使いましょうとのこと。
理由は書かれていないですが、rawであれば必ず最後のviewで使われるので、使いたい場面以外では使われにくいというメリットがありそうです。

remove

gsub(/xxx/, '')と同じような効果があります。(全く同じ、、?) removeだとメソッド名が明確なのでgsubを使って正規表現に一致する文字列を削除するよりも良いかもしれません。

truncate_words

truncateは知っていましたが、単語数で区切れるこちらのメソッドは知らなかったです。

'Hello, I am dummy user'.truncate_words(3)
#=>  "Hello, I am..."

'Hello, I am dummy user'.truncate_words(4)
#=>  "Hello, I am dummy..."

inquiry

文字列をStringInquirerオブジェクトに変換するメソッド。
StringInquirerオブジェクトというのをよく理解してなかったんですが、== 'string'をスマートにかけるオブジェクトのようです。

str = 'abc'.inquiry
str.abc?
#=> true

Rails.envもこのオブジェクトの子クラスになっているのも知りました。

Rails.env.class
#=> ActiveSupport::EnvironmentInquirer
Rails.env.class.superclass
#=> ActiveSupport::StringInquirer

strip_heredoc

ヒアドキュメントのインデントを削除するメソッドです。
コピペで文章を貼り付けた際にできてしまうインデントを削除するとか?あまりユースケースは思いつかないですが、面白いメソッドだなと思いました。

indentというインデントを追加するメソッドもあるようです。

at

rubyでは、配列でindexで要素を取得する際に使えますね。
rubyでは[]は使えるものの、atは使えないので、それが揃ったイメージかなと思いました。

from, to

指定したindexの位置から、もしくは位置までの文字列を返してくれます。
自分は範囲の書き方の方('abcde'[1..])がわかりやすいと思う派ですが、こっちの方が慣れれば見やすいかもですね。

first, last

名前通り、最初の文字や最後の文字を取得できるメソッドです。これは[0]とか[-1]とかやるより遥かにわかりやすいですね。

Timeの拡張(fortnights)

リンクはこちら

fortnightsという単語の意味を知らなかったのですが、「2週間」という意味なんですね。

2.fortnights #=> 6weeks

Integerの拡張(multiple_of?)

リンクはこちら

引数の倍数かどうかを判定してくれます。
余りが0か?といったロジックよりスマートになりますね。

15.multiple_of?(5) # true
0.multiple_of?(5) # true

index_by

リンクはこちら

ブロックで実行された値をキーとしたハッシュを返してくれます。

Book.all.index_by(&:id)
#=> { 1=> bookオブジェクト1, 2=> bookオブジェクト2 } 
#=> 上記はHash

index_with

リンクはこちら

index_byとは逆に、ブロックで実行された値をvalueとするハッシュを返してくれます。

Book.all.index_with(&:id)
#=> { bookオブジェクト1 => 1, bookオブジェクト2=> 2 }
#=> 上記はHash

rubywith_indexと混乱しそうな気もしますね(英語をきちんと考えれば明らかですが)。

including, excluding

リンクはこちら

引数をレシーバに結合して配列を返してくれるようです。
配列で使うと+と同じような感じですが、hashがレシーバだと、レシーバの方は配列にされました。

[1,2].including(1,3) #=> [1,2,1,3]

{ a: 1 }.including({b: 2})
#=> [[:a, 1], {:b=>2}]

excludingは逆です。

pick

リンクはこちら

配列等から、最初の要素のうち引数にしていたキーの値を取得します。
SQLを見る限り、ORDER BYを使っていないのでそこは注意が必要かなと思いました。

Book.pick(:title)
# SELECT "books"."title" FROM "books" LIMIT $1  [["LIMIT", 1]]
# '本1'

Arrayの拡張

リンクはこちら

second_to_last

英語名をパッと見た感じだと、2番目の要素から最後までの要素(=[1..])と同じかな?と思ったのですが、違いました。
正しくは末尾(last)から2番目の要素を返す([-2])でした。

[1,2,3,4].second_to_last #=> 3

extract!

一見selectの逆かな?と思ったのですが、違いました。
戻り値としては、ブロックを評価してtrueを返したものの配列である一方、レシーバ自体は左記の配列を削除したものに変更されます。

array = [1,2,3,4]
#=> [1, 2, 3, 4]
array.extract! { _1.odd? }
#=> [1, 3]
array
#=> [2, 4]

extract_options!

配列の最後の要素がhashであればそれを戻り値では返しつつ、レシーバはそのhashを取り除いたものに変更します。

ary = ['a', 'b', c: '1', d: '2']
#=> ["a", "b", {:c=>"1", :d=>"2"}]
ary.extract_options!
#=> {:c=>"1", :d=>"2"}
ary
#=> ["a", "b"]

ary = ['a', 'b']
#=> ["a", "b"]
ary.extract_options!
#=> {}

to_sentence

配列を言語ごとにいい感じに結合した文字列にしてくれます。

I18n.with_locale(:en) do
  ['Apple', 'Orange', 'Grape'].to_sentence
end
#=> "Apple, Orange, and Grape"
ja:
  support:
    array:
      words_connector: "、"
      two_words_connector: "と"
      last_word_connector: "そして"
I18n.with_locale(:ja) do
  ['Apple', 'Orange', 'Grape'].to_sentence
end
#=> "Apple、OrangeそしてGrape"

wrap

概ねArray()と同じ挙動をしますが、細かいところで異なるので違いは意識しておく必要がありそうですね。

in_groups_of

配列を何個かずつ取り出すときにとても便利そうです。
オプションも豊富で使い勝手もいいですね。

[1,2,3,4,5].in_groups_of(2)
#=> [[1, 2], [3, 4], [5, nil]]
[1,2,3,4,5].in_groups_of(2, 'dummy')
#=> [[1, 2], [3, 4], [5, "dummy"]]
[1,2,3,4,5].in_groups_of(2, false)
#=> [[1, 2], [3, 4], [5]]

in_groups

in_groups_ofは引数の個数ごとに配列を作る、でしたが、こちらは引数の個数に配列を分ける、です。

%w(1 2 3 4 5 6 7 8 9 10).in_groups(2)
#=> [["1", "2", "3", "4", "5"], ["6", "7", "8", "9", "10"]]

split

引数をセパレータとして配列を分割します。

['Yamada', 'Ito', nil, 'Suzuki', nil, 'Sato'].split
#=> [["Yamada", "Ito"], ["Suzuki"], ["Sato"]]

Hashの拡張

リンクはこちら

reverse_merge

引数の方の先頭に持ってきてくれるメソッドです。mergeはよく使いますが、reverse_もあるんですね。

{ a: 1 }.reverse_merge({ b: 2 })
#=> {:b=>2, :a=>1}

assert_valid_keys

レシーバのhashが引数に渡されたものをキーにもつなら値を、持たない場合はArgumentErrorを返します。

deep_transform_values

ハッシュがネストされていても一律値をブロックの中身で処理できます。かなり便利に使えるケースがありそうです。

nested_hash = { a: 1, b: { b_a: 2, b_b: { c_a: 3, c_b: 4 } } }

nested_hash.deep_transform_values { _1 * 2 }
#=> {:a=>2, :b=>{:b_a=>4, :b_b=>{:c_a=>6, :c_b=>8}}}

missing_name?

リンクはこちら

引数の名前が原因でエラーが発生したかどうかを判定してくれます。

begin
  NoExistClass.some_method
rescue NameError => e
  if e.missing_name?("NoExistClass")
    puts "NoExistClass はないです"
  end
end
#=> NoExistClass はないです