delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Rubyでは!や+や-はメソッドとしても使える

タイトルの通りですが、Rubyでは!+-はメソッドとしても使えるということを知りました。

!"abc".include?("a")というコードは以下のようにも書けます。

irb(main):006> "abc".include?("a").!
=> false

もっと純粋化すると、!truetrue.!と同じであるということです。

irb(main):010> true.!
=> false

+-も同様です。

irb(main):008> 1.+ 2
=> 3
irb(main):009> 3.- 2
=> 1

既存のコードリーディングをしていたらこのような書き方を見かけて分からず、AIに解説してもらいました。

自分で書く機会はほとんどないと思いますが、実際にコードリーディングで出会ったので知ってはおこうと思いました。

Active Recordのmergeメソッドで結合先のモデルのscopeを条件に含む検索を実現する

前提として、以下のようにscopeが定義されたTagモデルを定義します。

class Tag < ApplicationRecord
  has_and_belongs_to_many :restaurants
  scope :south_india, -> { where(name: "南インド") }
end

Tag.south_indiaとすると以下のSQLが発行されます。

SELECT "tags".* FROM "tags" WHERE "tags"."name" = '南インド';

Tagモデルと多対多の関係にあるRestaurantモデルからTagモデルのscopeを使って絞り込むSQLを発行したいです。

その場合はmergeメソッドが使えます。

Restaurant.joins(:tags).merge(Tag.south_india)

以下のようなSQLが発行されます。

SELECT "restaurants".*
FROM "restaurants"
INNER JOIN "restaurants_tags" ON "restaurants_tags"."restaurant_id" = "restaurants"."id"
INNER JOIN "tags" ON "tags"."id" = "restaurants_tags"."tag_id"
WHERE "tags"."name" = '南インド';

Railsガイドにも「高度な条件指定や既存の名前付きスコープの再利用を行いたい場合は、mergeが利用できるでしょう。」とあります。

railsguides.jp

複雑なクエリを直観的に扱えて便利だなという印象です。

「委譲」の適切な使い方を『リファクタリング』を読んで学ぶ

最近、オブジェクト指向における「委譲」の適切な使い方について考える機会があったのでマーティン・ファウラーの『リファクタリング』を読みながら勉強してみます。サンプルコードはRubyです。

www.ohmsha.co.jp

サンプルコード

以下のようにNameクラスとPersonクラスがあるとします。PersonクラスはNameクラスに依存しています。

PersonクラスのコンストラクタにNameクラスのインスタンスを渡す方法もあり、そこでも議論ができそうですが本記事では割愛します。

Nameクラス

class Name
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def full_name
    "#{@last_name} #{@first_name}"
  end
end

Personクラス

class Person
  def initialize(first_name, last_name)
    @name = Name.new(first_name, last_name)
  end
end

PersonクラスのインスタンスからNameクラスのfull_nameメソッドを呼ぶ方法を考えます。以下の2パターンがありそうです。

1.Personクラスで委譲メソッドを提供する

1つ目は以下のようにPersonクラスのインスタンスから直接full_nameメソッドを呼ぶ方法です。

person = Person.new("太郎", "山田")
puts person.full_name # 山田 太郎

このためにはPersonクラスでName#full_nameメソッドに委譲するメソッドを提供する必要があります。

class Person
  # 省略
  def full_name
    @name.full_name
  end
end

2.nameフィールド経由で呼ぶ

2つ目は以下のようにPersonnameフィールド(=Nameクラスのインスタンス)を経由してfull_nameメソッドを呼ぶ方法です。

person = Person.new("太郎", "山田")
puts person.name.full_name # 山田 太郎

そのためにはPersonクラスにattr_readerを定義してnameフィールドを公開する必要があります。

class Person
  attr_reader :name
  # 省略
end

メリット/デメリット

それぞれのパターンのメリット/デメリットについて考えます。マーティン・ファウラーの『リファクタリング』に詳しく解説されています。

  1. 委譲の隠蔽(p.196)
  2. 仲介人の除去(p.199)

です。1.2.は対のパターンとして紹介されています。

1.委譲の隠蔽

「1.委譲の隠蔽」は「1.Personクラスで委譲メソッドを提供する」に該当します。

メリットは、クライアント側が委譲先のオブジェクトのことを知らなくてよいことです。

今回の例では、full_nameメソッドを呼び出したい側はNameクラスの存在を意識せずに呼ぶことができます。

リファクタリング』では「(部下が)上司に対して会議に出席できるか尋ねると、手帳をみてから答えます(p.84)」というとても分かりやすい例で説明されています。

sequenceDiagram
    autonumber
    participant Subordinate as 部下
    participant Boss as 上司
    participant Calendar as 手帳(スケジュール)

    Subordinate->>Boss: 出席できますか?(会議日時)
    Boss->>Calendar: 空き確認する(会議日時)
    Calendar-->>Boss: 可否(OK/NG)
    Boss-->>Subordinate: 出席可否を回答(OK/NG)

この例では「上司」は「手帳」への委譲を隠蔽しているといえます。従って、「部下」は上司の手帳を見に行かなくても、上司に聞くだけでほしい回答を得ることができます。

このパターンは「直接の隣人にのみ話しかける」という「デメテルの法則」とも相性がいいです。

ja.wikipedia.org

デメリットはこのパターンを多用すると委譲するだけのメソッドが増えていくことです。例えば、Nameクラスにfull_name_kanaメソッドが追加されたら、Personクラスにもfull_name_kanaメソッドを追加する必要があります。

class Person
  # 省略
  def full_name
    @name.full_name
  end

  def full_name_kana
    @name.full_name_kana
  end
end

このように委譲を過剰に使用した結果、委譲先のオブジェクトに転送するだけのメソッドが多くを占めるようになったクラスをファウラーは「ただの仲介人」と呼んで批判しています。

加えて開発メンバーに「デメテルの法則」が好きな人が多いと、このアンチパターンに陥りやすいと述べられています。

2.仲介人の除去

「1.委譲の隠蔽」は「2.nameフィールド経由で呼ぶ」に該当します。これは委譲を過剰に使用したコードへのリファクタリング手法として紹介されています。

やることは単純で、委譲をやめて委譲先のオブジェクトのメソッドを直接呼ぶことです。

メリット/デメリットは1.の逆で、委譲先のオブジェクトがどのようなメソッドを提供しているかという知識は必要になりますが、「ただの仲介人」を削除することができます。

まとめ

「1.委譲の隠蔽」と「2.仲介人の除去」はそれぞれメリット/デメリットがあるため、混在してもよいとファウラーは主張しています。目安として、よく使われる委譲であれば「1.委譲の隠蔽」を用いてよいとのことです。

加えて、委譲をどの程度隠蔽すべきかはシステムの変化によっても変わるので、都度リファクタリングすることが重要とのことです。

RubyでAND条件とOR条件にand,orではなく&&,||を使う理由

Rubyでは基本的にAND条件とOR条件に&&||を使います。

他方でandorも構文ではサポートされています。従って、Python経験がある身としては、andorを使いそうになってしまうときがあります。

PythonではAND条件とOR条件にはandorを使う

note.nkmk.me

しかし、一般的なRubyのコードでandorを使っているケースはほとんど見ません。Rubocopの標準にもandorの使用をチェックするルールが存在します。

www.rubydoc.info

従って、なぜ原則は&&||を使ったほうがよいのかを確認したいと思います。

理由

&&||andorはそれぞれ演算子の評価の優先順位が異なり、andorは直観に反する挙動をする場合があるからです。

演算子の評価の優先順位は公式ドキュメントに明記されています。

docs.ruby-lang.org

andorは「低い」に分類されています。

挙動が変わる代表的な例としてAIが挙げたのは以下でした。

a = true && false # aには`false`が代入される
b = true and false # bには`true`が代入される

1個目は&&の方が=よりも優先的に評価されるため、aにはtrue && falseの評価結果であるfalseが代入されます。

これは直観的な挙動です。

他方で2個目は、=の方がandよりも優先的に評価されるため、先にb = trueが実行されます。次にb(true) && falseが実行されます。評価結果自体はfalseですが、この結果は捨てられるので、変数bの値は意図とは異なりtrueになってしまいます。

このようなバグのリスクがあるので、原則は&&||を使うようです。

【Ruby】rescue節のStandardErrorは省略できる

RuboCopに怒られて知ったのですが、Rubyでは以下のようにrescue節でStandardErrorを捕捉する場合はStandardErrorを省略することができます。

修正前

def divide(m, n)
  begin
    m / n
  rescue StandardError => e
    "[ERROR] #{e.message}"
  end
end

puts divide(2, 2) # 1
puts divide(2, 0) # [ERROR] divided by 0

修正後

def divide(m, n)
  begin
    m / n
  rescue => e # 暗黙的にStandardErrorが捕捉される
    "[ERROR] #{e.message}"
  end
end

puts divide(2, 2) # 1
puts divide(2, 0) # [ERROR] divided by 0

公式ドキュメントにも明記されていました。

docs.ruby-lang.org

Rails開発でのDISABLE_SPRING=trueのおまじないの意味をある程度理解する

前提

今の案件に参画当時、Railsのローカルの開発環境でrails consoleを実行してActive RecordでのDB操作を試みたり、rails runnerでDB操作を伴うコマンドを実行したりすると以下のようなエラーが発生するという事象に遭遇しました。

rails serverは正常に実行できました。

エラー

objc[8247]: +[Swift.__SharedStringStorage initialize] may have been in progress in another thread when fork() was called.
objc[8247]: +[Swift.__SharedStringStorage initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug.

案件内では既知の事象で対応が確立されており、以下のようにrails console実行時に先頭にDISABLE_SPRING=trueを付与することで解消できました。

DISABLE_SPRING=true bundle exec rails console

この事象はRailの公式リポジトリ上でもissue化されています。

github.com

ただ自分の中で完全におまじない化しているので、この機会にこのオプションの意味をある程度理解してみたいと思います。

環境

AppleシリコンのMacを使っています。

DISABLE_SPRING=true の意味

DISABLE_SPRING=true は直接的にはSpringというライブラリを無効化しています。

github.com

Springについては、以下のREADMEの説明の通り、開発環境での性能を改善してくれるライブラリのようです。

Spring is a Rails application preloader. It speeds up development by keeping your application running in the background, so you don't need to boot it every time you run a test, rake task or migration.

※ Rails7以降はSpringはデフォルトではインストールされない

SpringはRails7以降はデフォルトではインストールされなくなっています。従って、Rails7以降を使っている場合は明示的にSpringを使っている環境が対象になります。

Spring is no longer on by default, as faster computers have made it less relevant on anything but the largest applications.

rubyonrails.org

github.com

なぜSpringを使うと冒頭のエラーが発生するのか

github.com

上記のissueのやり取りをAIに解説してもらったところ、ざっくり以下のような感じのようです。

  • Springは内部でforkというMac OSAPIを呼んでいる
  • Mac OS側でforkの使い方がセキュアではないと判断してエラーで落としている

そのため、issueではOBJC_DISABLE_INITIALIZE_FORK_SAFETY=YESとしてMacのセキュリティ設定の方を変更する対応も提案されています。 (私の環境では機能しませんでした)

加えて、Springが使われるのはconsole, runner, generate, destroy, test のコマンドのみで、server は対象ではないそうです。

kinoppyd.dev

たしかにrails consoleを実行した場合は、Running via Spring preloader in process xxxというメッセージが表示されますが、rails serverでは表示されません。

rails serverでは発生しなかった理由が理解できます。

まとめ

おまじないだったDISABLE_SPRING=trueの意味が多少は理解できました。

  • 冒頭のエラーの原因はSpringという開発環境の性能向上用のライブラリがMac OSと相性が悪いことに起因
  • DISABLE_SPRING=trueはSpringを無効化している

RubyのNumbered parametersについて

Ruby2.7でNumbered parametersという機能が導入されました。

github.com

ブロックのパラメータを以下のように省略して書けるというものです。

Numbered parametersを使わない場合

[1, 2, 3].map {|n| n * 2} #  [2, 4, 6]

Numbered parametersを使う場合

|n|を省略して既に_1という変数に配列の各要素が代入済みのものとして扱うことができます。

[1, 2, 3].map {_1 * 2} #  [2, 4, 6]

この記法に興味を持ったきっかけ

このような省略記法があるということはRubyの入門書で薄っすら知っていたものの、私は明示する方が好きなので敢えて使わなくてもいいかなーという気持ちで記憶に残りませんでした。

しかし、AIがこのようなコードを提案してきたことがあり、自分では好んで書かなくても理解はしておく必要があると思いました。

参考

以下の記事により踏み込んだ解説があるので、時間があるときに読みたいです。

tech.smarthr.jp