Rubyの定数の探索順序

Ruby技術者認定試験の勉強のため、この機会にちゃんと理解しておく。

バージョン

$ ruby --version
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]

継承

子クラスに定数がない場合、親クラスの定数を参照できる。

class A
  FOO = "foo"
  BAR = "bar"
end

class B < A
  BAR = "bar-B"
end

puts A::FOO #=> foo
puts A::BAR #=> bar
puts B::FOO #=> foo
puts B::BAR #=> bar-B

include, prepend

モジュールも継承と同様に参照できる。

継承関係で探索するため、 includeprepend が混ざった場合は prepend で追加したモジュールの定数が優先される。

module M1
  FOO = "foo"
  BAR = "bar"
end

module M2
  FOO = "foo-M2"
end

class A
  include M1

  BAR = "bar-A"
end

class B
  prepend M1

  BAR = "bar-B"
end

class C
  include M1
  include M2
end

class D
  prepend M1
  include M2
end

puts M1::FOO #=> foo
puts M1::BAR #=> bar
puts A::FOO  #=> foo
puts A::BAR  #=> bar-A
puts B::FOO  #=> foo
puts B::BAR  #=> bar-B
puts C::FOO  #=> foo-M2
puts D::FOO  #=> foo

スコープ

継承関係よりもネスト関係の方が優先されるが、トップレベルの定数はネスト外部とは対象外になる。詳細は 変数と定数 (Ruby 3.1 リファレンスマニュアル) を参照。

CONST = "top-level"

module M
  CONST = "M"

  class A
    CONST = "A"
  end

  class B < A
    puts CONST #=> M
  end
end

class M::A
  puts CONST #=> "A"
end

class M::A::C
  puts CONST #=> "top-level"
end

Rubyのフリップフロップ (flip-flop) 構文

Ruby技術者認定試験の勉強をしていて、初めて知った機能だったのでまとめておく。

公式ドキュメント

挙動

基本

公式ドキュメントから引用。

5.times{|n|
  if (n==2)..(n==3)
    p n
  end
}
#=> 2
#   3

5.times{|n|
  if (n==2)...(n==3)
    p n
  end
}
#=> 2
#   3

テキストの処理

複数行のテキストから範囲を取得する際に便利らしい。
以下の例ではマークダウン記法でRubyのコード部分を取得するために使用している。

input = <<-EOF
The reproduction code is as follows.

### pattern 1

  ```rb
  puts "hello"
  ```

### pattern 2

  ```rb
  puts "world"
  ```
EOF

input.each_line do |line|
  puts line if (line =~ /```rb$/)..(line =~ /```$/)
end
#=>  ```rb
#  puts "hello"
#  ```
#  ```rb
#  puts "world"
#  ```

アルファベットの1, 2, 10, 11, 20, 21文字目を表示

('a'..'z').each_with_index { |x, i| print x if (i%10==0)..(i%10==1) }
#=> abkluv

「..」と「...」の違い

公式ドキュメントから引用。

5.times{|n|
  if (n==2)..(n==2)
    p n
  end
}
#=> 2

5.times{|n|
  if (n==2)...(n==2)
    p n
  end
}
#=> 2
#   3
#   4

違いは分かるけど、有効な使い道がよく分からん...。

Ruby 2.6で非推奨になるも、Ruby 2.7で復活

クックパッドの記事は読んだはずなのに、フリップフロップ構文を使う機会が無さ過ぎて記憶に全く残っていなかった。

フリップフロップ構文を覚えて

資格試験のためだけに覚えるけど、仕事で使うことはないかな...

Ruby 3.1.xだと継承ツリーに後からincludeでモジュールを追加できる

Ruby技術者認定試験Goldの勉強をしていてRubyのバージョンによって変わっている挙動を知ったのでメモ。

検証コード

module M1
  def method_1
    __method__
  end
end

class C
  include M1
end

p C.new.method_1

module M2
  def method_2
    __method__
  end
end

module M1
  include M2
end

p C.new.method_2

Ruby 2.7.6

:method_1
Traceback (most recent call last):
a.rb:23:in `<main>': undefined method `method_2' for #<C:0x0000000148184c48> (NoMethodError)
Did you mean?  method
               method_1
               methods

Ruby 3.1.2

:method_1
:method_2

Rubyのensure節の挙動

Ruby技術者認定試験の対象バージョンがRuby 3.1.xになったので、試験勉強していて気づいた挙動をブログにメモっておく。

ensure節の値は無視される

ensure 節が存在する時は begin 式を終了する直前に必ず ensure 節の本体を評価します。
begin式全体の評価値は、本体/rescue節/else節のうち最後に評価された文の値です。また各節において文が存在しなかったときの値はnilです。いずれにしてもensure節の値は無視されます。

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

def foo
  "foo"
ensure
  puts "ensure"

  "bar"
end

puts foo
#=> ensure
#=> foo

ensure節にreturnがあると無視されない

def foo
  "foo"
ensure
  puts "ensure"

  return "bar"
end

puts foo
#=> ensure
#=> bar

Rubyで1つのインスタンスに同名メソッドを複数保つ方法

きっかけ

よく考えたら as メソッドを定義するだけで実現できそうな気がしたので、実装してみた。

Swiftの話

Swiftを勉強してた頃に、プロトコル拡張で実装したメソッドをキャストした型によって呼び出し分けるのを試したことがある。

sinsoku.hatenablog.com

検証コード

module As
  class Proxy
    def initialize(this, mod)
      @this = this
      @mod = mod
    end

    def method_missing(name, *args)
      raise NoMethodError unless @mod.instance_methods.include?(name)
      @mod.instance_method(name).bind_call(@this, *args)
    end
  end

  def as(mod)
    raise NoMethodError unless self.class.ancestors.include?(mod)
    Proxy.new(self, mod)
  end
end

module A
  def call = puts "A"
end

module B
  def call = puts "B"
end

class Foo
  include As
  include A
  include B
end

foo = Foo.new

foo.call #=> B
foo.as(A).call #=> A
foo.as(B).call #=> B

コメントを読むと分かるように、普通は 後勝ちでBの実装 が優先される。

これを as メソッドを使うことで、任意のモジュールの実装を呼び出せるようになった。

メリット

モジュールに定義するメソッド名をシンプルに保つことができる。

例: データをAWSとDBに保存するメソッド

module AwsS3Record
  def save
    # S3にjsonを保存する処理
  end
end

class User < ActiveRecord::Base
  include AwsS3Record
end

user = User.new
user.as(ActiveRecord::Base).save
user.as(AwsS3Record).save

本来はsaveのように汎用的な名前は避けるが、実装を切り替えることが可能になれば使うことができる。

RubyのModule#includeで引数を複数指定すると逆順で追加される

Module#include は引数に複数のモジュールを受け取ることができる。

しかし、以下のサンプルコードで分かるように、モジュールの順序は3行で書いたものと逆になるので注意がいる。

module A; end
module B; end
module C; end

class Foo
  include A
  include B
  include C
end

class Bar
  include A, B, C
end

p Foo.ancestors
#=> [Foo, C, B, A, Object, Kernel, BasicObject]
p Bar.ancestors
#=> [Bar, A, B, C, Object, Kernel, BasicObject]

include を触っていた初めて知った挙動なので、ブログにメモっておく。

課題管理に対する考え方とテンプレ

自分の中の課題管理に対する考え方を整理するため、ブログにまとめておく。

課題を管理する

ソフトウェア開発ではGitHub IssueやRedmineなどで課題を管理する。

基本的に「課題を登録する量 >> 完了する量」であり、多くの課題が登録されるため、優先度をつけて順番に対応していく事になる。

課題に優先度をつける

課題が 一意に並ぶ ように優先度をつけて、上から順番に対応する。

「どの "緊急" の課題を対応すべきですか?」と質問しなくて済む状態にする。

課題の管理工数を意識する

課題が増えると管理する工数も増えるため、以下の2つを意識する必要がある。

  1. 情報を過不足なく、分かりやすく記載する
    • 課題の登録者に質問しないようにする
  2. 少ない工数で解決する
    • 工数の少ない代案を積極的に挙げる

また、代案を出しやすくするため、課題を登録するときに Why(なぜ)とHow(どのように)が混ざらない ように気を付ける。

課題として登録する内容

以下を課題として登録する。

  • 機能要望(Feature Request)
  • バグ修正(Bug Report)
  • 雑用(Chore)
    • ライブラリのアップグレード対応など

開発者の「Modelを実装する」などのタスクを登録し始めると管理工数が増えてしまうため、これらは登録しない。*1

仕様を確認するより、改善要望として登録する

機能が分かりづらいという事なので「UIを改善する」「コードにコメントを残す」などの改善を検討する。

課題に記載する項目

課題管理のテンプレで使えるように、マークダウン形式で項目を記載しておく。

「Howは課題の登録者の提案であり、唯一の解決方法ではない」ことを意識し、開発者は 少ない工数で済むように提案する ことを意識する。

機能要望

<!-- 現状の顧客の課題と要望する機能について記載してください。 -->

## 現状の課題(Why)

## 要望する機能の説明(How)

## 備考

バグ報告

<!-- 開発者がバグを再現・修正できるように、具体的な再現手順を記載してください。 -->

## 概要(Why)

## 再現手順

## 実際の挙動

## 期待する挙動(How)

## 備考

雑用

## 作業内容(How)

## 作業の利点、実施しない場合のリスク(Why)

## 備考

作業内容だけを書きがちなので、実施した時の利点や実施しない場合のリスクも書いておくこと。

*1:一覧に出ない形で別管理はアリ。課題のコメント欄に残すなど