delhi09の勉強日記

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

Rubyのcase式は型判定の分岐にも使える

以下の記事を読んで「えっ、こうやって書けるの?」と思ったサンプルコードがあったので自己理解のための記事です。

techracho.bpsinc.jp

驚いたサンプルコードは以下です。

def fetch(id_or_object, model_class)
  case id_or_object
  when Numeric
    model_class.find(id_or_object)
  when model_class
    id_or_object
  else
    fail "Object type mismatch #{model_class}, #{id_or_object}"
  end
end

目的がid_or_objectがid(=数値)かクラスかで処理を分岐することであるのはコードに雰囲気から分かるのですが、case式のwhenの条件に直接型を指定できるというのが驚きでした。

if文とPythonでいうisinstance相当の関数を使って実現するのだと思っていました。なぜこのコードが動くのか理解したいと思います。

1.Rubyではcase式のwhen条件では===で比較される

これは公式ドキュメントに記載があります。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#case

すなわちサンプルコードは以下のようにも書けるということです。

def fetch(id_or_object, model_class)
  if Numeric === id_or_object 
    # ...省略
  elsif model_class === id_or_object
  # ...省略
end

2.Rubyでは{値} === {クラス}は値がそのクラス型のインスタンスであるかを判定する

これはirbで実験すると分かります。

irb(main):003:0> String === "1"
=> true
irb(main):004:0> String === 1
=> false
irb(main):005:0> Numeric === 1
=> true
irb(main):006:0> class Animal end
=> nil
irb(main):007:0> dog = Animal.new
=> #<Animal:0x000000013b0d5c28>
irb(main):008:0> Animal === dog
=> true
irb(main):009:0>

ちなみに左右を逆にすると成立しません。

irb(main):001:0> "1" === String
=> false

以上でなぜこのサンプルコードで動くのかざっくりとは理解できました。

Railsでリクエストパス変数がわからなくなったらrails routesコマンドで確認できる

Railsではformのアクションを指定する場合などにリクエストパスをベタ書きすることは少なく、xxx_pathのような自動で定義されている変数を使います。

変数名には法則があるのですが、慣れないうちはわからなくなることが多いです。

その場合はrails routeコマンドで確認できます。

以下のような結果が表示されるので、使いたいパスを探してnew_admin_tag_pathのように末尾に_pathをつければOKです。

                        # 省略
                          new_admin_tag GET    /admin/tags/new(.:format)                                                                         admin/tags#new
                          edit_admin_tag GET    /admin/tags/:id/edit(.:format)                                                                    admin/tags#edit
                               admin_tag GET    /admin/tags/:id(.:format)                                                                         admin/tags#show
                                         PATCH  /admin/tags/:id(.:format)                                                                         admin/tags#update
                                         PUT    /admin/tags/:id(.:format)                                                                         admin/tags#update
                                         DELETE /admin/tags/:id(.:format)
                        # 省略

Rails開発最初の1ヶ月で使ったRubyの配列メソッド

Rubyは高機能な配列関数が多くてどこまで覚えるか悩ましいですが、とりあえず1ヶ月Railsで開発して使ったものは今後もよく使うだろうということでメモしておきます。

map

配列の各要素に対して何らかの処理をした結果の新しい配列がほしい場合に使うメソッドです。 docs.ruby-lang.org

例えば、以下のようにTagオブジェクトのリストからnameフィールドのリストを取得したい場合などに使えます。

class Tag
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
puts tags.map(&:name).join(", ") # タグ1, タグ2

include?

配列に要素が含まれるかを判定するメソッドです。

docs.ruby-lang.org

例えば画面にチェックボックスを表示する際に、チェック済みのチェックボックスcheckedの状態で表示したい場合などに使えます。

class CheckBox
  attr_accessor :checked
  attr_reader :id, :name

  def initialize(id, name, checked)
    @id = id
    @name = name
    @checked = checked
  end

  def display
    if @checked
      puts "[x] #{@name}"
    else
      puts "[ ] #{@name}"
    end
  end
end

check_boxes = [
  CheckBox.new(1, "インド", false),
  CheckBox.new(2, "ネパール", false),
  CheckBox.new(3, "スリランカ", false)
]

checked_ids = [1, 3]

check_boxes.each do |check_box|
  check_box.checked = true if checked_ids.include?(check_box.id)
end

check_boxes.each(&:display)

実行結果

$ ruby index.rb
[x] インド
[] ネパール
[x] スリランカ

Railsではhas_manyの関連先のidリストをauthor.book_idsのように取得できるので、相性がいいです。

railsguides.jp

selectreject

selectは配列の要素を評価してtrueのものだけにフィルタした新しい配列を返します。

docs.ruby-lang.org

ids = ["1", "", "3"]
pp ids.select { |id| !id.empty? } # ["1", "3"]

reject はその逆です。

docs.ruby-lang.org

ids = ["1", "", "3"]
pp ids.reject { |id| id.empty? } # ["1", "3"]

リクエストパラメータを配列で受け取った時のサニタイズ処理などに使えます。

Railsでは独自拡張のblank?present?が合わせて使えます。

railsdoc.com

Rubyで配列関数に&とシンボルを渡す記法の意味を理解する

Railsを書いていたら、アクティブレコードのTagモデルのリストをループしてname属性をカンマ区切りで表示したい場面がありました。

AIに聞いたところ、以下のように書けるとのことでした。この書き方の&:nameの部分を理解したいと思います。

tags.map(&:name).join(", ")

ミニチュア版のコードを用意

まずは以下のようにRailsに依存しないミニチュア版のコードを用意します。

index.rb

class Tag
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
puts tags.map(&:name).join(", ")

このコードが期待通り動作することを確認します。

実行結果

$ ruby index.rb
タグ1, タグ2

素直な書き方

Rubyのmap関数は以下の仕様です。

  • ブロックを受け取る
  • 各要素に対してブロックを評価した結果を全て含む配列を返す

docs.ruby-lang.org

従って、素直な書き方は以下のようになります。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_names = tags.map do |tag|
  tag.name
end
puts tag_names.join(", ") # タグ1, タグ2

tagsからtag_namesを導出するところを1行にして以下のようにも書けます。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_names = tags.map {|tag| tag.name}
puts tag_names.join(", ") # タグ1, タグ2

Procを理解する

ここから先に進むにはProcというものを理解する必要があります。

公式ドキュメントには「ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。」とあります。

docs.ruby-lang.org

JSが分かる人は関数オブジェクトのようなものと理解するとよいようです。(私もJS経験者)

一番シンプルなProcの例は以下のようなコードです。

hello_proc = Proc.new {|name| puts "Hello #{name}!"}
hello_proc.call("山田")

実行結果

$ ruby index.rb
Hello 山田!

Procオブジェクトをmap関数に渡す

次にProcオブジェクトをmap関数に渡してみます。

先のサンプルコードでmap関数のブロックに記述していたコードをProc化します。

tag_name_proc = Proc.new {|tag| tag.name }

Procオブジェクトをブロックとしてmap関数に渡します。Rubyの記法として先頭に&をつけます。

tag_names = tags.map(&tag_name_proc)

実行部分のコード全体は以下のようになります。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_name_proc = Proc.new {|tag| tag.name }
tag_names = tags.map(&tag_name_proc)
puts tag_names.join(", ")

実行結果

$ ruby index.rb
タグ1, タグ2

シンボルのto_procメソッド

Rubyのシンボルはto_procメソッドを持っています。

docs.ruby-lang.org

to_procメソッドが返すProcオブジェクトは、callの第一引数に渡した変数をレシーバーとしてメソッドを実行します。

tag = Tag.new(1, "タグ1")
tag_name_proc = :name.to_proc
puts tag_name_proc.call(tag) # タグ1

ちなみに存在しないメソッド名を使うとエラーになりました。

tag = Tag.new(1, "タグ1")
tag_priority_proc = :priority.to_proc
puts tag_priority_proc.call(tag)

実行結果

$ ruby index.rb
index6.rb:12:in '<main>': undefined method 'priority' for an instance of Tag (NoMethodError)

最後に、Rubyではメソッドに&とともにシンボルを渡すと以下の挙動になるようです。(公式ドキュメントより引用)

  1. to_procが呼ばれてProcオブジェクト化される
  2. 1.がブロックとして渡される

以上より、2つは同じ意味になります。

素直なコード

tag_names = tags.map {|tag| tag.name}

シンボルとto_procを使った省略記法

tag_names = tags.map(&:name).join(", ")

参考文献

『プロを目指す人のためのRuby入門[改訂2版⁠]⁠ 言語仕様からテスト駆動開発デバッグ技法まで』の 「10.5.2 &とto_procメソッド」を参考にさせていただきました。(P431〜p432)

gihyo.jp

Railsでローカルでは動作するコードがHerokuデプロイ後にエラーになる場合はHerokuを再起動するとなおる場合がある

結論

Railsでローカルでは動作するコードがHerokuデプロイ後にエラーになる場合は、以下のコマンドでHeokruを再起動すると解消する場合がある。

heroku restart

事象

RailsでのWEBアプリを開発で以下の改修をした。

  1. TagモデルにTagGroupモデルへの関連を追加する
  2. 管理画面でタグを作成する際に、タググループを付与できるようにする

すると、2.の動作確認中にローカルでは正常に動作していたコードがHeroku上ではエラーになるという事象が発生した。

エラーメッセージ

ActiveModel::UnknownAttributeError (unknown attribute 'tag_group_id' for Tag.)

解決方法

まずはheroku run rails db:migrate:statusマイグレーションの適用状況がローカルと同じになっているか確認したが問題なかった。

次に以下のコマンドでtagsテーブルのスキーマを確認したがtag_group_idも存在しており問題なさそうだった。

heroku run rails console
> ActiveRecord::Base.connection.columns(:tags).map(&:name)
=> ["id", "created_at", "name", "position", "updated_at", "tag_group_id"]

ChatGPTに聞いたら「高確率で「デプロイ後のアプリコードが古い/再起動が反映されていない」状態です。つまり Heroku の dyno(Rails プロセス)が古いアプリコードをキャッシュしたまま動いている 状況が考えられます。」とのことだった。

heroku restartで再起動することを勧められたので、やってみたらその通りで解消した。

iTerm2で起動時のディレクトリを設定する

ターミナルにiTerm2を使っています。

iTerm2ではターミナル起動時の初期ディレクトリを設定できます。よく使うディレクトリがある場合は設定しておくと便利です。

設定方法

Settingsを開きます。

Profilesを開きます。デフォルトはHome directoryになっています。

Directoryを選択してよく使うディレクトリを設定します。これでOK

Macでペアワイズ法サポートツールのPICTを使う

Macでペアワイズ法サポートツールのPICTを使ってみます。 github.com

PICTはWindows環境用のツールであり、Macで実行できるようにするのは面倒そうという先入観がありました。

が、今日ではbrewで提供されているので簡単にインストールできました。

formulae.brew.sh

brew install pict

文字コード問題

以下のようなインプット用のファイルを作成します。

input.txt

インド料理: バターチキン, サンバル, マサラドーサ
ネパール料理: チョエラ, スクティ, モモ
スリランカ料理: パリップ, ポルサンボル, ワタラッパン

使い方的にはこれでpict input.txtを実行すればいいはずなのですが、生成結果が意図しないものになってしまいました。

調べたところ、「日本語が含まれる際は文字コードEUCにしないと化ける」という情報がありました。

techblog.kayac.com

従って、以下のようにUTF-8EUC-JPに変換して渡すことで解決しました。ChatGPTに教えてもらったのですが、「プロセス置換」という方法でワンライナーで書けるようです。

pict <(iconv -f UTF-8 -t EUC-JP input.txt)

文字コード問題は2台のMacの内の片方でしか発生せず、発生条件が謎なままです。(もう一台はUTF-8のまま問題なく読み込めた)

結果

以下のようにパターンを生成できました!

インド料理  ネパール料理  スリランカ料理
マサラドーサ  スクティ    ポルサンボル
サンバル    スクティ    パリップ
マサラドーサ  モモ  ワタラッパン
サンバル    チョエラ    ワタラッパン
サンバル    モモ  ポルサンボル
バターチキン  スクティ    ワタラッパン
マサラドーサ  チョエラ    パリップ
バターチキン  チョエラ    ポルサンボル
バターチキン  モモ  パリップ