ちょっと前に、Ruby 2.3.0 の最初のプレビュー版である、Ruby 2.3.0-preview1 がリリースされました

新機能が追加されているようなので、NEWS に書いてあることをかいつまんで、実際に試してみました。

Frozen String Literal Pragma

新しいマジックコメントまたはコマンドライン引数を指定することで、ソースコード中の全ての文字列リテラルを freeze するというもの。

Ruby 3 では全ての文字列リテラルが immutable (frozen) になるそうなので、2.3.0 では先行してそれを有効にできるようなったようです。

# string-literal-frozen.rb
a = "foo"
b = "foo"

puts a.object_id
puts b.object_id

従来通りだと、別のオブジェクトとなります。

$ ruby string-literal-frozen.rb
70124014993580
70124014993560

マジックコメントで

# frozen_string_literal: true

を指定すると、同じ文字列のリテラルは同じオブジェクトになります。

$ ruby string-literal-frozen.rb
70095964209440
70095964209440

マジックコメントをつけずに、コマンドライン引き数で --enable=frozen-string-literal を指定することもできます。

$ ruby --enable=frozen-string-literal string-literal-frozen.rb
70287689548120
70287689548120

freeze されているので、文字列を破壊的に変更するようなメソッド呼び出しは、当然エラーになります。

foo = "foo"
foo.upcase!
# can't modify frozen String (RuntimeError)

なので、必要に応じて、.dup を呼び出して複製するか、String.new でインスタンスを生成する必要があります。

foo = "foo".dup
foo.upcase!

bar = String.new
bar << 'Ruby'

Safe navigation operator

メソッドをコールするときに &. というオペレーターを使う、新しいシンタックスが追加されています。

Rebuild.fm #118 で、「ぼっちオペレーター」とか言われてました。なるほど。

object&.foo のようにメソッド呼び出しをした際に、objectnil でなければメソッドがコールされますが、objectnil の場合は nil になります。

これまでだと、

buz = nil
if foo != nil && foo.bar != nil
  buz = foo.bar.buz
end

みたいに書かないといけなかったのが、

buz = foo&.bar&.buz

だけでよくなります。便利ですね。

Array#dig, Hash#dig

ArrayHash に、dig というメソッドが追加されました。

ArrayHash がネストされている場合に、深い階層にある要素を取り出すのに使います。

a = [[[1, 2, 3]]]
num = a.dig(0, 0, 1)
# => 2
num = a.dig(0, 1, 1)
# => nil

h = {foo: {bar: {buz: 'qux'}}}
str = h.dig(:foo, :bar, :buz)
# => "qux"
str = h.dig(:hoge, :bar, :buz)
# => nil

ぼっちオペレーターもそうでしたが、このメソッドを使うと、途中の階層の要素が存在するかどうかのチェックをしなくても良くなります。

Array#bsearch_index

配列の要素を二分探索するメソッドとして、Array#bsearch がありますが、このメソッドは戻り値として、見つかった要素を返します(見つからなかった場合は nil を返します)。

Array#bsearch_index は、要素を返すのではなくインデックスを返します。見つからなかった場合は nil を返します。

a = ['foo', 'bar', 'buz']

a.bsearch {|s| s =~ /b/}
# => "bar"

a.bsearch_index {|s| s =~ /b/}
# => 1

Enumerable#grep_v

もともと存在する Enumerable#grep は、パラメーターと要素を === で比較し、マッチした要素の配列を返しますが、新しく追加された Enumerable#grep_vマッチしなかった要素の配列を返します。つまり、grep の -v オプションです。Unix っぽいですね。

a = ['foo', 'bar', 'buz', 'qux']

a.grep(/b/)
# => ["bar", "buz"]

a.grep_v(/b/)
# => ["foo", "qux"]

Enumerable#chunk_while

Enumerable#slice_when は、隣り合う要素が前方から順に指定されたブロックに渡され、ブロック内で評価した結果が偽になるところでチャンクを切ります。メソッドは、チャンク分けされた要素を持つ Enumerator が戻り値として返します。

Enumerable#chunk_while は、既存の Enumerable#slice_when の逆バージョンです。

a = [1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21]

a.slice_when {|i, j| i + 1 != j}.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]

a.chunk_while {|i, j| i + 1 == j}.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]

Hash#fetch_values

Hash#fetch_values は、パラメーターで複数のキーを指定して、各キーの値を配列で返します。

キーが存在しない場合は、ブロックで評価された値を使用します。ブロックを指定しなかった場合は KeyError になります。

Hash#values_atHash#fetch を合体させたみたいな感じです。

h = {a: 1, b: 2, c: 3}

h.fetch_values(:a, :c)
# => [1, 3]

h.fetch_values(:a, :d)
# => KeyError: key not found: :d

h.fetch_values(:a, :d) {|key| 0}
# => [1, 0]

Hash#⇐, Hash#<, Hash#>=, Hash#>

2つのハッシュを比較する演算子が追加されました。

なかなか言葉で説明しづらい……。

{a: 1, b: 2} >= {a: 1}
# => true

{a: 1, b: 2} >= {a: 2}
# => false

{a: 1, b: 2} >= {a: 1, b:1}
# => false

{a: 1, b: 2} >= {a: 1, b:2}
# => true

{a: 1, b: 2} > {a: 1}
# => true

{a: 1, b: 2} > {a: 2}
# => false

{a: 1, b: 2} > {a: 1, b:2}
# => false

こんな感じです。

Hash#to_proc

Hash#to_proc は、ハッシュのキーをパラメーターとして受け取り、その値を返す Proc になります。存在しないキーの場合は nil が返ります。

これにより、ブロックを受け取るメソッドに、Hash を渡すことができるようになります。

h = {a: 1, b: 2, c: 3}
[:a, :b, :c].map(&h)
# => [1, 2, 3]

Numeric#positive?, Numeric#negative?

Numeric#positive? は数値が正の値なら true、それ以外なら false を返します。

Numeric#negative? は数値が負の値なら true、それ以外なら false を返します。