Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby 3.0でアドベント問題集を解く(1日目)修正のレポート(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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, :*)

原注:
上でreduce1を渡している理由がおわかりでしょうか?もし仮に空の配列を渡したときにもまともな値を返して欲しいですよね。加算ならば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でラップすることも一応可能ですが、それでは左から右にスムーズに読めなくなってしまいます。ここでは出力結果には興味がないので、必要に応じてtapthenのどちらでも使えます。

さて、上のコードに_1というものがあることにもお気づきかと思います。これはRubyの「番号指定パラメータ(numbered parameter)」と呼ばれるもので、ブロックの最初の引数を暗に指しています。_1のほかにも_2_3などをもっと増やすことも一応できますが、そこまですることはめったにないでしょう。

ここでは、ファイル入力をパイプで関数に渡すためだけに使っています。

then { puts product_duals(_1) }

そして出力がめでたくSTDOUTからコマンドラインスクリプトに渡されます。これが欲しい答えになります。

1日目のまとめ

本日第1日目はここまでにしたいと思います。今後数日間ないし数週間は、Advent of Codeの各問を調べながら解答や手法を探っていきたいと思います。

私のコメント付き解答については以下でご覧いただけます。

baweaver/advent_of_code_2020 - GitHub

関連記事

Rubyでわかる「時計もモノイドの一種」(翻訳)

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。