はじめに: Rubyの数値リテラルの単項演算子
Rubyのリテラルのうち、少なくとも整数リテラルには、以下のように符号を複数持つ単項演算子を付けることもできます(小数リテラルなどの数値リテラルや変数などの単項演算子については調べていません)。
# 1つの行がひとつの単項です
1
1
-1
---1
-----1
-1
---1
-----1
- 1
--- 1
----- 1
+- 1
+--- 1
+----- 1
-+1
---+1
-----+1
- + 1
--- + 1
----- + 1
-1
-+-+-1
+- + + - -+ + + - -1
--1
----1
------1
+--+1
※Rubyの日本語ドキュメント↓を調べてみると、単項演算子を「+
符号や-
符号やスペース文字を0個以上含むもの」とする説明は見当たりませんでしたが、本記事では便宜上「+
符号や-
符号やスペース文字を0個以上含むもの」も単項演算子と呼ぶことにします。
参考: バージョン:3.0.0 > クエリ:単項演算子 | るりまサーチ (Ruby 3.0.0)
この単項演算子の挙動をRubyのirbで調べたところ、冒頭のリストの各行はいずれも単項として、たとえば以下のように計算式を含む式(以下単に計算式と呼びます)の項に追加できます。
irb(main):002:0> +- + + - -+ + + - -1 +- + + - -+ + + - -1----- + 1
=> -3
irbで調べた挙動を以下にまとめました。
- ひとつの整数リテラルには「
+
符号や-
符号やスペース文字を0個以上含む」単項演算子を付けられる - 偶数個ある
-
はキャンセルされる - 単項演算子内のスペースは無視される
- 最終的には1個の
+
または-
単項演算子として認識される
ご存知のように、符号なしの数値リテラルを以下の1 1
のように計算式の2個目以降の単項式に書くことはできません。
irb(main):001:0> 1 1
Traceback (most recent call last):
3: from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.0.0/bin/irb:23:in `<main>'
2: from /Users/hachi8833/.anyenv/envs/rbenv/versions/3.0.0/bin/irb:23:in `load'
1: from /Users/hachi8833/.config/anyenv/envs/rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):1: syntax error, unexpected integer literal, expecting end-of-input)
irb(main):002:0>
単項演算子で符号を複数書ける理由
Rubyの計算式に+- + + - -+ + + - -1
のように書ける理由を考えてみました。
まずRubyの挙動として明らかだと思う点を以下にリストアップしてみます。
- Rubyでは配列リテラルの要素やハッシュリテラルのキーおよびバリューなどは一般にカンマ「
,
」で区切られる - Rubyの式は、数値リテラルも含めて例外的に、カンマではなくスペース文字「
」で区切られる。
Rubyの計算式は、多くの言語と同様、学校で習うような計算式の書き方から大きくかけ離れない仕様になっていると私は認識しています。
1 + 1
1 - 1
-1 + 1
-1 - 1
だとすると、計算式を以下のようにも書きたいこともあるはずです。
1 + -1
1 - +1
さらにRubyでは、計算式の単項をスペースなしで以下のように記述することもできます。
1+-1
1-+1
このような計算式をRubyの内部で場合分けして解析するよりは、Ruby内部では整数リテラルの単項で以下のような書き方も認め、さらに「スペースを無視する」「プラス符号+
やマイナス符号-
はひとつだけ残す」「マイナス符号-
が偶数個ある場合はキャンセルする」ことで、計算式の中の単項を統一的に扱えるようにしたのだろうと思いました。
-+1
---+1
-----+1
- + 1
--- + 1
----- + 1
-+-+-1
+- + + - -+ + + - -1
その代わり、1 1
のような書き方だけは認めないということになります。
仮に1
という単項を必ず+1
と書かなければいけないルールにすればシンプルになりそうですが、+1 +1
と書かなければならなくなってRubyユーザーにとって不便なので、普通そのようなルールにはしないだろうと思います。
以上、Rubyの挙動を元にした推測でした。
符号を複数持つ単項が最終的にマイナスかどうかを判定する正規表現
今自分が推測したRubyの単項演算子の挙動の一部を正規表現でお遊び再現してみました。なお、このRubular.comで使われているRubyのバージョンは2.5.7です。
この正規表現は、整数リテラルの単項の前に-
が奇数個ある場合に、単項演算子全体にマッチします。+
やスペース文字が単項の前にあっても挙動は変わりません。--1
のようにキャンセルされる場合も判定します。あくまで行で区切られた単項が対象なので、単項の連続はそのままでは判定できません。
この正規表現のうち、*-[ +]*
の「-
」を削除すると以下のようにロジックが反転し、単項がプラスになる場合にマッチします。
ロジック反転に成功したとき、なぜか映画『オズの魔法使』の中盤でモノクローム映像がフルカラーに変わる瞬間を思い出してしまいました。見たこともなかったのに。
雑に解説
^[+ ]*
+
またはスペース文字で始まることを示す。これがないと+---1
のように先頭に+
があるパターンや、-
が1個しかないパターンがこぼれてしまいます。*
で量指定子を「0個以上」とするのがポイント。([+ ]*-[+ ]*-[+ ]*)*
-
が偶数個ある部分にマッチする。-
と-
の前後および中間にある+
とスペース文字もマッチに含めます。全体を()*
で囲んで、量指定子を「0個以上」とするのがポイント。-
- 最終的に残る
-
符号。これを消すとロジックが反転します。 [ +]*
- 最終的な
-
符号の後と数値の間の+
やスペースにマッチします。これも*
で量指定子を「0個以上」とするのがポイント。 (?=[0-9])
- 後ろに数値が続くことを指定する。
(?=)
は「先読み」(look ahead)という正規表現によくある拡張機能です。
TechRachoの正規表現記事にも何度も書いているように、普段の私は正規表現の*
メタ文字をできるだけ避けているのですが、今回の正規表現は自分としては珍しく*
を連発しています。*
がないと書けないはずなので別にいいんですが、軽く敗北感があります。今回は[^何とか]
のような否定表現を使わずに書けただけでもよかったと思うことにします。
おまけ
Rubyの単項演算子の符号を調べているときに、たまたま以下のツイートを見かけました。
「異符号の加法は絶対値の差に絶対値の大きい方の符号をつけます」は「結果としていえることのまとめ」であって「求め方のまとめ」ではないからだと思います。
「…ですから結果として、異符号の加法は絶対値の差に絶対値の大きい方の符号が付くと言えます」
— 結城浩 (@hyuki) January 26, 2021
ツイートは個別の単項の符号ではなく、異符号の加法における最終的な和の求め方についての議論なので本記事に直接関係ありません。ちょうどRubyの単項演算子の符号を考えているところだったのでドキッとしたのでした。