pockestrap

Programmer's memo

Ruby Puzzle その2

その1はこちら

pocke.hatenablog.com

解答、ヒント、解説などは充分な余白を開けた上で表示するので、安心して読んでください。

問題

class String
  def to_proc
    puts self
    -> { self }
  end
end

->(&b){b.()}.(&("Hello world" * 42))

このRubyプログラムに1文字追加して、"Hello world\n"を1度だけ出力するプログラムにしなさい。 なお、例外などで異常終了するものは正答とはみなしません。

RubyKaigi 2019のCookpadのパズルリスペクトです。

techlife.cookpad.com

なお、hanachinさんが色々な人が出している問題をまとめてくれているので、もっと問題を解きたい方はそちらをご覧ください。

Okinawa.rb Ruby Puzzles 2019/5/15 - #rubykaigi のRubyPuzzleが面白かったので真似しました。Ruby 2.7でしか動きません。 · GitHub

ヒント

以下にヒントを少しずつ書いていきます。適当にスクロールしてください。 ヒント3まであり、少しずつ核心に近づいていきます。

なお、ヒントを読まずに解いてくれたら嬉しいなと思っています。自信作なので。ちなみにヒント1は基本的なことしか書いてないので、すっと読んでしまって良いと思います。











































ヒント その1

まずは、プログラムを読めるようになりましょう。 各構文要素がなにをしているのかを理解することは、問題を解く第一歩となります。

something.() は、something.call()のSyntax Sugarです。

docs.ruby-lang.org

引数の&somethingは、somethingto_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_procselfを返すため、以降のプログラムも正常に動きます。

問題の成り立ち

最初はこの問題は、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のバージョンを限定することも考えましたが、流石に不格好です。

そのため最終形では単項演算子のない*メソッドを使ってこの別解を防ぎました。思っていたよりも違和感なく*を問題に埋め込めたのが嬉しかったです。


  1. 別解を潰す上で、%#が別解を生みまくるので強敵でした。