【Ruby 3.0 Advent Calendar 2020】Ruby に右代入がやってくる【1日目】

Ruby 3.0 Advent Calendar 2020 1日目の記事になります。
もう今年も Advent Calendar の時期ですね。
並行して Ruby Advent Calendar 2020 も開催しているので興味があるひとはぜひ参加してみてください!
Ruby 3.0 Advent Calendar 2020 もまだ空きはいっぱいあるので興味があるひとは書いてね!!!
さて、この記事では右代入について簡単に書きます。

preview1 時点での右代入

Ruby の右代入は [Feature #15921] R-assign (rightward-assignment) operator のチケットで議論され Ruby 3.0 に入ることが決定しました。
このときの仕様は Ruby 3.0.0 Preview 1 で試すことができます。
Ruby 3.0.0 Preview 1 時点では左代入に対する右代入、みたいな形になっていました。

# 右辺の変数に左辺の値を代入する
value = 42

# 右辺の値を左辺の変数に代入する
42 => value

また、完全に対象的なわけではなくて次のような代入は右代入では動作しませんでした。

# これは OK
a = 1, 2

# これはシンタックスエラー
1, 2 => a

# [] を付けると OK
[1, 2] => a

Ruby 2.7 のパターンマッチと1行 in

さてさて、preview1 で使用できた右代入はかなりシンプルな挙動だったんですが最新版の右代入はかなり複雑な事ができるようになっています。
その前に Ruby 2.7 で実験的に導入されたパターンマッチと1行 in について説明します。
これは Array や Hash に対して複雑なマッチングを行うことができる機能です。

def str(user)
  # 各要素に対して型チェックのようなことを行っている
  case user
  in [String => name, Integer => age]
    "name:#{name}, age:#{age}"
  in { name: String => name, age: Integer => age}
    str([name, age])
  else
    "unknown."
  end
end

pp str(name: "homu", age: 14)
# => "name:homu, age:14"
pp str(["mami", 15])
# => "name:mami, age:15"
pp str(name: :mado, age: 15)
# => "unknown."

上記は型チェックのようなことをやっていますが単に各要素を束縛するだけも行えます。

def str(user)
  # 各要素を変数に束縛するだけ
  case user
  in [name, age]
    "name:#{name}, age:#{age}"
  in { name: , age: }
    str([name, age])
  else
    "unknown."
  end
end

pp str(name: "homu", age: 14)
# => "name:homu, age:14"
pp str(["mami", 15])
# => "name:mami, age:15"
pp str(name: :mado, age: 15)
# => "name:mado, age:15"

ここからが本題なのですが Ruby 2.7 ではパターンマッチと合わせて in 演算子という機能も実験的に入りました。
これは、次のようなパターンマッチを 1行だけで書くことができる機能です。

user = { name: "homu", age: 14 }
case user
in { name:, age: }
  pp "name:#{name}, age:#{age}"
end

# 上のパターンマッチを以下のようにかける

user in { name:, age: }
pp "name:#{name}, age:#{age}"
# => "name:homu, age:14"

ここまでくるとピンと来るひとがいるかもしれませんがこの1行 in っていうのはまさに右代入の機能になるんですよね。
なので、Ruby 2.7 でも in を使うと右代入っぽいことはできました。

# 左辺の値を右辺の変数に代入できた
42 in value

やったね!

最新版の右代入の挙動

ここまで preview1 の右代入と Ruby 2.7 の 1行 in に付いて説明してきました。
では、いまこの2つの機能はどうなっているのかって言うと『1行 in=> に変わりました』 つまり今まで 1行 in で使えてた機能がそのまま => で使えるようになります。
なので、Ruby 3.0 では => を使用して次のような代入を行う事ができます。

user = { name: "homu", age: 14 }

user => { name:, age: }
pp "name:#{name}, age:#{age}"
# => "name:homu, age:14"

in を使ったときよりは => のほうがより代入っぽいですね。
結果的に => で分割代入のような事が行えるようになりました。
ただし、Ruby 3.0 では => はまだ実験的な機能になるので使用すると warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby! という警告が出力されるので注意しましょう。

右代入の注意点

ローカル変数以外に代入できない

preview1 時点では専用の構文だったのでローカル変数以外の変数(インスタンス変数やグローバル変数)に代入する事ができました。

# Ruby 3.0.0 Preview 1 では動作していた
42 => @value
42 => $value

しかし、 in に置き換わった => ではローカル変数以外には代入できません。

# 最新版だとシンタックスエラーになる
42 => @value
42 => $value

これはそもそもパターンマッチの束縛がローカル変数以外に対応していないことに由来しています。

case [1]
# error
# インスタンス変数に対して束縛することはできない
in [@value]
end

このあたりの制限は今後どうなるのかはわかりませんが少なくとも Ruby 3.0 ではそのままの仕様だと思います、多分。

=> の優先順位

例えば次のようにメソッドの結果を右代入したいことがあると思います。

# func(42) の結果を result 変数に代入したい
func 42 => result

しかし、これはうまく動作しません。
なぜなら上のようなコードは以下のように Hash として定義されるためです。

# 42 をキーとした Hash として定義される
func({ 42 => result })

これは Hash の定義でも => という記号が使われているためです。
ちなみに func 42 => resultRuby 2.7 時点でも有効な書き方になります。
なので蒸気のようなことを行いたい場合は明示的に () を付ける必要があります。

# OK
func(42) => result

このあたりはしょうがないので気をつけて使っていきましょう。

所感

と、言うことで右代入について簡単にまとめてみました。
preview1 時点では優先順位ぐらいでシンプルな挙動だったんですが、 in に置き換わった途端かなり複雑な事ができるようになり一気に実用的になった感じがしますね。
Ruby 3.0 時点ではまだ警告が出るので実際のプロダクトで使うことはできないと思うんですが Ruby 3.1 あたりで正式に入ったらかなり便利に使うことができそうですね。
早く Ruby 3.1 がやってこないかな…。