その1はこちら
解答、ヒント、解説などは充分な余白を開けた上で表示するので、安心して読んでください。
問題
class String def to_proc puts self -> { self } end end ->(&b){b.()}.(&("Hello world" * 42))
このRubyプログラムに1文字追加して、"Hello world\n"を1度だけ出力するプログラムにしなさい。 なお、例外などで異常終了するものは正答とはみなしません。
RubyKaigi 2019のCookpadのパズルリスペクトです。
なお、hanachinさんが色々な人が出している問題をまとめてくれているので、もっと問題を解きたい方はそちらをご覧ください。
Okinawa.rb Ruby Puzzles 2019/5/15 - #rubykaigi のRubyPuzzleが面白かったので真似しました。Ruby 2.7でしか動きません。 · GitHub
ヒント
以下にヒントを少しずつ書いていきます。適当にスクロールしてください。 ヒント3まであり、少しずつ核心に近づいていきます。
なお、ヒントを読まずに解いてくれたら嬉しいなと思っています。自信作なので。ちなみにヒント1は基本的なことしか書いてないので、すっと読んでしまって良いと思います。
ヒント その1
まずは、プログラムを読めるようになりましょう。 各構文要素がなにをしているのかを理解することは、問題を解く第一歩となります。
something.()
は、something.call()
のSyntax Sugarです。
引数の&something
は、something
のto_proc
メソッドを暗黙に呼び出して、それの戻り値をブロックとしてメソッドに渡します。
また、->(){}
はlambdaの定義です。
つまり、このプログラムをわかりやすく書くと次のようになるでしょう。(問題を解くときは、わかりやすくしたプログラムじゃなくて元のコードを使って解いてくださいね)
class String def to_proc puts self lambda { self } end end lambda { |&block| block.call }.call(&("Hello world" * 42).to_proc)
ヒント その2
注目するところを変えてみましょう。十中八九、あなたが今いじっているところは答えから遠いと思います(近かったらごめんなさい)。
ヒント その3
メソッドを探しましょう。
答え
答えです。
class String def *to_proc puts self -> { self } end end ->(&b){b.()}.(&("Hello world" * 42))
解説
この問題の答えは、to_proc
メソッドの定義を*
メソッドの定義に変えてしまうことです。to_proc
というメソッド名だったはずのものは、引数名となります。
いかにもブロックを使う問題に見せかけて実は*
メソッドが答えであるという、意外性を狙って作りました。見事引っかかってくれたら嬉しいです。
答えをわかりやすく整形すると、次のようになります。
class String def *(to_proc) puts self lambda { self } end end lambda { |&block| block.call }.call(&("Hello world" * 42).to_proc)
このプログラムはまず、"Hello world" * 42
によって答えを出力します。
そして、*
メソッドはlambdaを返します。
lambdaのto_proc
はself
を返すため、以降のプログラムも正常に動きます。
問題の成り立ち
最初はこの問題は、backtickを定義する問題として考えていました。
# def `something に変えればHello worldが出力される def something puts something end `Hello world`
ですが、backtickがコード中に出てきた時点で狙いがわかり易すぎるため、流石にもう少し隠す必要がありました。
次に+
メソッドの定義を考えました。これは最終形に近いです。
class String def to_proc puts self -> { self } end end ->(&b){b.()}.(&("Hello world" + ", Yey!"))
この問題は最終形と同様の手法でも解けますが、どうしても別解が潰せずにいました。それは%
です。1
この問題では%
を+
の前に挿入することで解けてしまいます。
class String def to_proc puts self -> { self } end end ->(&b){b.()}.(&("Hello world"% + ", Yey!"))
これは、わかりやすく表現すると、"Hello world" % (+", Yey!")
のような表現になります。つまりString%
の引数に、String#+@
を呼び出したStringが渡されています。
String#+@
が定義されたのはRuby 2.3からなのでRubyのバージョンを限定することも考えましたが、流石に不格好です。
そのため最終形では単項演算子のない*
メソッドを使ってこの別解を防ぎました。思っていたよりも違和感なく*
を問題に埋め込めたのが嬉しかったです。
-
別解を潰す上で、
%
と#
が別解を生みまくるので強敵でした。↩