こんにちは。EC事業部のarumaです。
PythonとしてもRubyとしても文法的に正しく実行もできるが、出力は異なるというコードを考えてみました。
この記事は、GMOペパボエンジニア Advent Calendar 2023の4日目のものです。
きっかけ
実話です。ある日、Rubyが好きな友人に「Python の "Hello, world!" はどう書くの?」と尋ねられた私は、以下のコードを書きました。
print("Hello, world!")
これを見て友人は「このコード、Rubyでもそのまま動くよ」と言いました。
Rubyを全く知らなかった当時の私はすぐにオンラインの実行環境で試し、確かに同じ結果が得られることに驚きました。
あれから数年、私は業務でもRubyを使うようになりました。 そして最近、ふとした瞬間にこの出来事を思い出したとき、こんなことを考えました。
「PythonとしてもRubyとしても文法的に正しく実行もできるが、出力が異なるコードは作れるだろうか?」
是非、みなさんも考えてみてください。
作ってみた
本記事に記載のコードは、以下の言語バージョンで動作確認を行いました。
- Python: 3.12.0
- Ruby: 3.2.2
この記事では、PythonとRubyの両方で実行可能なコードを「共通コード」と呼ぶことにします。
Python と Ruby の記法が共通な要素
まずは、PythonとRubyで記法が共通している要素を考えました。
整数や文字列の記述・代入などは、特殊なことをしない限りは共通コードになります。
# Python and Ruby
a = 1
a += 3
b = "Hello, world!"
可変長配列(Pythonの list
、Rubyの Array
)の作成及び要素へのアクセスも、共通コードにできます。
# Python and Ruby
a = [1, 2, 3]
a[1] = 5
また、関数呼び出しも以下の記法で共通コード化が可能です。
# Python and Ruby
func("sample")
Python と Ruby で記法が異なる要素
真理値や条件分岐、関数定義などは記法が異なります。 つまり、今回これらの要素は使えません。
# Python
def func(n, m): # コロンをつける
if n == 1:
return True # `True` (`true`ではない)
elif m == 2: # `elif` (`elsif`ではない)
return True
else:
return False
# Ruby
def func(n, m) # コロンをつけない
if n == 1
return true # `true` (`True`ではない)
elsif m == 2 # `elsif` (`elif`ではない)
return true
else
return false
end # `end` をつける
end
共通コードのうち異なる挙動をするものを探す
ここまで挙げた共通コードの中からPythonとRubyで挙動が異なるものを探してみると、1つ見つかりました。
list / Array に対する、 +=
演算子の挙動です。
Pythonにおける list
は、ミュータブルなシーケンス型の1つです。
通常、ミュータブルなシーケンス s
とイテラブル t
があるとき、s += t
はシーケンス s
を拡張して t
の要素を追加する操作、すなわち s.extend(t)
(Rubyでの s.concat(t)
)と等価です。
参考: Python 3.12.0 ドキュメント - ミュータブルなシーケンス型
一方、Rubyにおける s += t
は単なる自己代入、すなわち s = s + t
と等価です。
つまり、 s
と t
がともに Array
の場合、s
には新しく作成された Array
が代入されます。
参考: Ruby 3.2 リファレンスマニュアル - 自己代入
この挙動の違いを使えば、共通コード内の変数から言語によって異なるオブジェクトを参照させることができ、結果として言語によって異なる出力を得られます。
# Python
a = [1, 2]
b = a
a += [3, 4]
print(b is a) # True (bとaは同一オブジェクトを参照)
# Ruby
a = [1, 2]
b = a
a += [3, 4]
print(b.equal?(a)) # false (bとaは異なるオブジェクトを参照)
完成したコード
以上をふまえて、完成したコードを以下に示します。
Pythonでは Python
、Rubyでは Ruby
と1行出力されます。
# Python and Ruby
a = b = ["Ruby\n"]
a += ["Python"]
print(b[-1])
Pythonの print
はデフォルトキーワード引数 end='\n'
を持つため、末尾に改行文字が付いた出力となります。
出力形式を揃えるため、Ruby用の出力文字列には "\n"
をつけています。
実際に common.pyrb
というファイル名で保存しPythonとRubyで実行すると、以下のようになりました。
$ python common.pyrb
Python
$ ruby common.pyrb
Ruby
できる限り短いコードを考えてみた
最後に、できる限り短いコードを考えてみました。 いわゆるコードゴルフです。
レギュレーションは以下としました。
- コードはファイルに保存し、
python <ファイル名>
及びruby <ファイル名>
で実行すること - PythonとRubyのどちらの言語でも、エラーや警告を発生させずに実行ができること
- Pythonでは
Python
、RubyではRuby
と一行出力されること(末尾で改行されていること) - コード全体をテキスト形式で保存したファイルのサイズで競うこと
短くするためには、演算子の優先順位の差異を利用するのが良いだろうと考えました。
例えば |
と ^
はPythonでは ^
の方が高い優先順位を持ちますが、Rubyでは同じ優先順位です。
参考1: Python 3.12.0 ドキュメント - 演算子の優先順位
参考2: Ruby 3.2 リファレンスマニュアル - 演算子式
従って 1 | 1 ^ 1
というコードは、Python では 1
、Rubyでは 0
となります。
これを利用すると、以下のようなコードを作成できます。
print(["Ruby\n","Python"][1|1^1])
これで33バイトです。
さらに演算子について深掘りしたところ、and
と or
に辿り着きました。
Pythonでは、and
がより高い優先順位です。
一方Rubyにおいては &&
及び ||
より低い優先順位の演算子という特別な役割があり、これら2つの優先順位は同じです。
これを使い共通コードとなるように整えて、以下の32バイトのコードができました。
print ("Python"or 0 and"Ruby\n")
まとめ
今回は、PythonとRubyの共通コードで異なる出力を得る方法を考えてみました。また、同じ条件でコードゴルフも行いました。
このように特殊な条件を設定したコーディングのパズルにチャレンジすると、詳細な言語仕様や言語間の違いを知るきっかけとなることでしょう。
皆さんも、いろいろなルールを考えてチャレンジしてみてください。おまけとして、PHPを加えた3言語版の解の1つをこの下にHTMLコメントで記載しておきます。
最後までお読みいただきありがとうございました。