1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初心者向け】Ruby のまずいコード 25 本Advent Calendar 2021

Day 18

【Ruby のまずいコード】数値が全て一定範囲に収まっているか

Last updated at Posted at 2021-12-17

お題

引数として浮動小数点数(Float オブジェクト)の配列を受け取り,それらの数値がすべて 0 以上 1 以下であるかどうかを返すメソッドを書いてください。

コード

コード 1

def all_ge0_lt1?(numbers)
  numbers.each do |number|
    unless (0..1).cover?(number)
      return false
    end
  end
  true
end

メソッド名は「greater than 0, less than 1」から名付けました。もっと良い名前があるかもしれませんが,本記事では命名については不問とします。

コード 2

def all_ge0_lt1?(numbers)
  numbers.all?{ |number| (0..1).cover?(number) }
end

講評

コード 1

素朴な(ある意味で素直な)コードですね。
一つでも範囲外の要素が見つかったらそれ以降の要素については調べる必要がないので,その場ですぐ false を返しています。
ループの中で return してメソッドを抜けていますが,Ruby ではこのような書き方をしても問題ありません。
その点を除けば,大昔のプログラミング言語ではほぼこんなふうにループを回すコードを書いていたと思います。

しかし,Ruby をはじめイマドキのプログラミング言語1ならたいがい「全ての要素が〇〇を満たす」ことを判定する手段を持っています。Ruby でいえば,コード 2 で使っている Array#all? ですね2

コード 2

コード 2 では Array#all? を用い,大幅に簡素化しています。

これ以上簡潔に書く余地はあるのでしょうか? あります。

改善

Array#all? には,ブロックを与える用法のほかに,引数を与える用法もあります。
これを使うとこんなふうに書けます:

def all_ge0_lt1?(numbers)
  numbers.all?(0..1)
end

いったいどういうことでしょうか?
all? に引数を与えたときは,その引数を左辺とし,要素を右辺とし,演算子を === とした演算子式を評価した結果を用います。つまり,

numbers.all?(0..1)

は,イメージとしては

numbers.all?{ |element| (0..1) === element }

のように評価されるわけです。
ブロック内の式 (0..1) === element を理解するためには,Range#=== を知る必要があります。
=== という演算子メソッドはさまざまなクラスに定義されており,その動作はクラスによってさまざまですが,それらをひっくるめて大雑把に表現すると,「右辺が左辺に当てはまる」ことを判定する演算子と言えるでしょう3

範囲オブジェクトの場合,その範囲に入っているかどうかを判定することになります。つまり,cover? と同じです4

この原理と,各クラスの === の仕様が分かっていれば,たとえば,文字列の配列の全要素が数字を含むかどうかを

strings.all?(/\d/)

で判定できますし,何らかのオブジェクトの配列の全要素が数値(Numeric のサブクラスのインスタンス)であるかどうかを

objects.all?(Numeric)

で判定できます。

「全要素が〇〇を満たす」ではなく「少なくとも一つの要素が〇〇を満たす」であれば any? を使いますし,「全要素が〇〇を満たさない」なら none? を,「ただ一つだけの要素が〇〇を満たす」なら one? を使います。
演算子 === は,case 式だけでなく,これらの一連のメソッド,さらには grep の類でも暗黙に使われます。Ruby らしいプログラミングを陰で支えるメソッドと言えるでしょう。

  1. Ruby は誕生から既に四半世紀が過ぎており,むしろ歴史ある言語ですが。

  2. Array#all? のほかに,同じ働きの Enumerable#all? もあり,each を持つさまざまなオブジェクトに使えます。

  3. 何をもって当てはまったことになるのかが,左辺のクラスによってさまざまなわけです。

  4. ただし,Range#cover? は,引数が範囲オブジェクトの場合,「範囲に範囲が含まれるか」を判定するので,Range#=== とは動作が異なります。また,Ruby 2.7 未満の Range#=== は端点のクラスによっては少し動作が違っていました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?