概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Advent of Ruby 3.0 - Day 01 - Report Repair - DEV Community
- 原文公開日: 2020/12/28
- 著者: Brandon Weaver -- RubyとJSとキツネザルと言葉遊びとアートを愛しています。
- 新着記事ニュースレター: The Start of a Pattern Match • Buttondown Brandonさんの新着記事情報がメールで配信されます
Ruby 3.0でアドベント問題集を解く(1日目):修正のレポート(翻訳)
Ruby 3.0が先頃リリースされました。3.0に盛り込まれた楽しい機能をじっくり時間をかけて試すには絶好の時期ですね。
本シリーズでは、Ruby 2.7や3.0のさまざまな機能がAdvent of Codeの問題を解くうえでどんなふうに活用できるかを見ていくことにします。なお、ここに載せる解答例は新機能のデモが目的であり、必ずしも効率が最大とは限りませんのでご了承ください。
記事がだんだん長くなってきたのでカレンダーの日付ごとに分割します。読むのに10分以上かかる記事を増やすのも気が引けますので。
何はともあれ、早速やってみましょう!
⚓ 0日目: ランナースクリプト
「Rubyの記事なのに何でbashが?」とお思いかもしれませんが、まあそう言わずにちょっとご覧ください。
私はコードを手っ取り早く動かせるようにこちらのbashスクリプトを使っています。巧妙に書かれたbashのコードにありがちな、ちょっと怖そうなハックが使われています。
#!/bin/bash
# ここではbashを使っています。
# 数字で始まるファイルを検索する(大したことはしてません)
# 実際のスクリプトファイルにはちゃんと番号を付けてあります
matching=`find . -type f -name "$1_*.rb" | head -n 1`
# ファイル名の拡張子を削除する
name=`basename $matching .rb`
# 続いてRubyに処理を投げる:
# inputs/ディレクトリにある入力のファイル名は変えず、拡張子を.txtにする
ruby ./"$name".rb ./inputs/"$name".txt
入力元は./inputs
ディレクトリにある.txtファイルたちで、Rubyの出力と同じファイル名になります。これらは捨ててもいい前提ですが、ここでやりたいのはRubyを動かすことなので、このぐらいにして話を元に戻します。
⚓ 1日目: 修正のレポート
最初は、足すと2020
になる2つの数字を入力ファイルの中から探す問題です。
完全な解答は以下をご覧ください。
参考: advent_of_code_2020/01_report_repair.rb at main · baweaver/advent_of_code_2020
⚓ 2つの数列を対応付ける
この種の問題で最もよく使われる解法は、値と値を足すと2020
になるのに必要な対応付けを(Hash
で)作ることです。
TARGET = 2020
n = 1500
duals = {}
duals[TARGET - n] = n
# => { 520 => 1500 }
なぜこのような順序でペアを作るかと言うと、次の数字を与えればいつでも以下のように値を取得できるからです。
n = 520
duals[n]
# => 1500
このハッシュには、足すと2020
になることがわかっているペアがひとつ入っているので、その時点で処理を終えられます。このアイデアを完全に実装すると次のような感じになるでしょう。
# 数字を「探索しない」この方法に名前があったら教えてください
# ただし出力関数がエラー時にクラッシュしないよう
# 互換性を取ること
NOT_FOUND = [-1, -1]
def duals(input, target: 2020)
# 入力をintに変換してハッシュに乗せる
input.map(&:to_i).each_with_object({}) do |v, duals|
# 数字のペアが見つかったら、それと`v`を返す
# それが答えになる
return [v, duals[v]] if duals[v]
# 見つからない場合はペアを登録して次回見つけられるようにする
duals[target - v] = v
end
# それ以外の場合は"not found"を返す
NOT_FOUND
end
⚓ end
レスメソッドをラッパーにする
今回の出題では、2つの数値の積も求める必要があります。このduals
関数の中で積を求めることもできなくはありませんが、関数に余計な責務を負わせてしまいます。それに、積ではなく単に数値のペアが欲しいときもあるかもしれません。そういうわけで、この関数はこのままにしておくべきです。
積を求める別の関数に上の関数をラップするというのはどうでしょう?Ruby 3.0に導入されたend
レスメソッドは、こういうちょっとしたコーディングにうってつけです。
def dual_product(input) = duals(input).reduce(1, :*)
原注:
上でreduce
に1
を渡している理由がおわかりでしょうか?もし仮に空の配列を渡したときにもまともな値を返して欲しいですよね。加算ならば0
に何を足しても同じ数値になりますが、乗算でも1
で同じことがやれます。
なおこの概念には「identity(単位)」または「empty(零)」という名前があることが前提ですが、詳しくは別記事に譲ります。
end
レスメソッドを使ったことで、duals
関数を汚すことなく「2つの入力の積を取る」という概念を手軽にラップできるようになりました。とは言うものの、個人的には同じ引数を2回も入力するのは好きではありません。しかしRuby 3.0にはそういうときのための機能もあるのです。
⚓ 引数の転送
Ruby 3.0には、以下のような...
という引数を転送する機能(argument forwarding)が導入されました(訳注: 引数の転送は厳密にはRuby 2.7.0で導入され、3.0で改良されました)。
def product_duals(...) = duals(...).reduce(1, :*)
上では、product_duals()
のすべての引数を次の関数にまるっと転送しています。なお、以下の書き方が無効なのがちょっぴり気がかりです。
def product_duals(input) = duals(...).reduce(1, :*)
# SyntaxError ((irb):22: unexpected ...)
個人的にはこの書き方は可能であるべきだと思います。さもないと、その関数が具体的にどんな引数を取るかを明示的にできず、ひたすらパススルーするだけになってしまいます。これはバグレポートにできそうなので、何か方法を見つけるか作り出せたらここにリンクするつもりです。
今の話題はそのまま次のセクションにつながります。次ではこの入力を取得する必要があります。
⚓ 次は番号指定パラメータを使う
訳注: 番号指定パラメータはRuby 2.7.0で導入されました(NEWS for Ruby 2.7.0)。
私のスクリプトでは、Advent of Codeに掲載されている入力データを含むテキストファイルの取り込みにARGV[0]
を使っています。
File.readlines(ARGV[0]).then { puts product_duals(_1) }
上で最初に行っているのはコマンドライン引数の取得(ARGV[0]
)で、ここで入力ファイルを受け取ります。File.readlines
でファイルのすべての行をArray
として取得し、それをthen
という興味深い関数にパイプしています。
then
はRuby 2.6でyield_self
のエイリアスとして導入されたもので、tap
と対照的な操作と考えるとよいでしょう。
訳注:
Object#yield_self
はRuby 2.5.0で導入され(NEWS for Ruby 2.5.0)、Ruby 2.6.0でObject#then
というエイリアスが追加されました(News for Ruby 2.6.0)。
1.tap { |v| v + 1 }
# => 1
1.then { |v| v + 1 }
# => 2
tap
は元のオブジェクトをそのまま返しますが、then
はそのブロックによる処理結果を返します。ところで、File.readlines
を単純にproduct_duals
でラップすることも一応可能ですが、それでは左から右にスムーズに読めなくなってしまいます。ここでは出力結果には興味がないので、必要に応じてtap
とthen
のどちらでも使えます。
さて、上のコードに_1
というものがあることにもお気づきかと思います。これはRubyの「番号指定パラメータ(numbered parameter)」と呼ばれるもので、ブロックの最初の引数を暗に指しています。_1
のほかにも_2
や_3
などをもっと増やすことも一応できますが、そこまですることはめったにないでしょう。
ここでは、ファイル入力をパイプで関数に渡すためだけに使っています。
then { puts product_duals(_1) }
そして出力がめでたくSTDOUT
からコマンドラインスクリプトに渡されます。これが欲しい答えになります。
⚓ 1日目のまとめ
本日第1日目はここまでにしたいと思います。今後数日間ないし数週間は、Advent of Codeの各問を調べながら解答や手法を探っていきたいと思います。
私のコメント付き解答については以下でご覧いただけます。