Rubyでコードゴルフ ~235bytes → 40bytesの過程を追う~
TL;DR
コードゴルフとは? その魅力は? の解説と、Rubyで書かれた普通(?)のコードがワンステップずつ縮んでいき、コードゴルフらしいコードになるまでの解説。
目次
まえがき
コードゴルフとは、与えられたお題でどれだけコード長を縮められるか? を競う遊びです。
所属するサークルTSGの学祭(五月祭)企画でミニ・コードゴルフ大会というものを開催することになり、その実況担当になりました。→参加者のuraさんがWriteupをUPしてくれました:TSG LIVE! 3 ライブコードゴルフ大会 writeup - 何か書く
私はコードゴルフという遊びについて全く触れたことがなかったのですが、今回の企画のためにRubyでコードを(主に部員のみんなの助けにより)縮め、コードゴルフの一端を体験しました。せっかく説明用のスライドも作ったので、共有したいと思います。
なお、筆者はそもそもRubyを書くのすら初めてでした。Rubyistから見ると不思議な点が多々あると思います。間違いなどあれば指摘していただけると幸いです。
コードゴルフとは
先ほども書いたのですが、「与えられたお題でどれだけコード長を縮められるか(バイト数を減らせるか)」を競う遊びです。 基本的に、お題はかなり簡単なものが与えられます。Atcoder Beginner ContestのA問題をちょっと難しくしたくらいだと感じました。なので簡単に書けるのですが、そのコードをいかに縮められるか、というところで様々な知識が要求されます。
当然のことながら、「それの何が面白いの?」という疑問が出てくると思います。今回私が感じたのは以下の3点です。
- 一見すると意味不明なコードがちゃんと動く
ゴルフコードは改行・空白等が基本的にない上、コード長を短くするために様々な工夫が施されています。ですから、一見しても(コードゴルフに慣れていなければ)何が何だかわかりません。でもちゃんと動くのです。そこにちょっと感動し、ああそういうことか! と仕組みが分かるとちょっとうれしくなれます。
- コードを縮めるために細かい言語仕様が役に立つ
普段のコーディングではある処理に対しては私個人のやり方をひとつ覚えてさえいればどうにかなるため、複数の書き方を覚える必要に迫られません(もっとも、これは私が仕事として書いていないせいかもしれませんが)。
ですが、コードゴルフでは複数の書き方、すなわちより短くなる書き換え方を知っていることが強みになります。それは結局言語の仕様についての知識に直結し、深い知識によるコード短縮テクに「こんな書き換え方があったのか! 賢い!」と唸らされます。
- 難解プログラミング言語(esolang)の存在
世の中には「出来上がったコードが星空のように見える言語」や「絵文字でプログラミングする言語」、「人間にとって読みにくいコードを生成する言語」などのように、様々な非実用的な言語が存在します。
それらはある種のジョークや実験的言語として作られたもので、実用的でないので当然使われないわけですが、コードゴルフという遊びにおいてはこの難解プログラミング言語(esoteric programming language, esolang)によるプログラミングが普通のプログラミングにはない面白味を与えてくれます。しかし、この記事はRubyでのコードゴルフが主題なのでesolangには触れません。
今回のお題
- 上の画像のように、アルファベットと数字が(ほぼ)交互に並んだ文字列が与えられます。
- 数字の数だけ直前のアルファベットを連続させた文字列を返してください。
というものです。
これは「連長圧縮」という技術をテーマにしており、実際にファクシミリで使われていた(使われている?)圧縮方法らしいです。
これだけだとあまりにも問題が簡単なので、制約が加わっています。
- 連続回数が1回のときは、数字は省略される。
つまり、sssgww
に対応するのはs3g1w2
ではなく、s3gw2
になる、ということです。この制約により、前から2文字ずつ読んでいくやり方は不可能になります。
この制約をクリアする方針の一つが以下になります。
今回はこの方針に基づいて実装したコードを縮めていきます。
普通のRubyコード
cipherText = gets number = "23456789" for i in 0..(cipherText.length - 1) do if number.index(cipherText[i]) == nil then print cipherText[i] else for j in 0..(cipherText[i].to_i - 2) do print cipherText[i - 1]; end end end
筆者はこのコードが初Rubyなので、まったくRubyらしいコードになっていないと思います。Rubyはふつうforを使わない、と聞いてびっくりしました。聞きかじっただけなので嘘を言っているかもしれませんが。
補足すると、変数number
に入力文字が数字かどうか判定するための文字列を格納し、index
で検索することで処理を分けています。
現在のコード長が235bytesなのですが、最終的に40bytesまで縮みました。そのコードがこちら。
$<.chars{|d|n=d.to_i-1;puts n<0?B=d:B*n}
ちなみに、解説のkurgmさんとプレイヤーのuraさんがちょっとずつ違う方法で36bytesまで縮めていました。Twitterでも「36が最短」というツイートを見つけたので多分これが一番短いと思います。
さて、ではコードを縮めていきます。
ステップごとの解説
最初の方は当たり前のことを書いているので飛ばしてもよさそうです。ちなみに書いている本人はどこまでがRubyistにとっての常識なのか全くわかっていません。
1. 変数名を1文字にする
コードゴルフではコードの短さが命なので、変数名は1文字にしてしまいます。もちろん悪いプラクティスなわけですが。
171B(-62)
2. 空白・改行をなくす
コーディング規約にもよりますが、-
や==
の両隣にはスペースを挿入することが好ましく、また適宜改行を挿入することがよいプラクティス、のはず。これも取り除きます。
158B(-13)
c=gets n="23456789" for i in 0..(c.length-1) do if n.index(c[i])==nil then print c[i] else for j in 0..(c[i].to_i-2) do print c[i-1]; end end end
3. 後半のforはいらない
テクでも何でもないです。
後半のforは「今読み込んだ数字-1回、インデックスが一つ前の文字を出力する」という処理ですが、そもそも'c' * times
でc
をtimes
回連続させた文字列を作れます。これをprint
した方が短いです。
133B(-25)
c=gets n="23456789" for i in 0..(c.length-1) do if n.index(c[i])==nil then print c[i] else print c[i-1]*(c[i].to_i-1) end
準備:for
をeach
に置き換え
Rubyにおけるfor
は特殊なので、do...end
→{}
などができません。ということでeach
に書き換えます。
c=gets n="23456789" (0..c.length-1).each do |i| if n.index(c[i])==nil then print c[i] else print c[i-1]*(c[i].to_i-1) end end
4. (0..c.length-1).each
を書き換え
入力として受け取った文字列の長さだけループを回していますが、これはc.length.times
というメソッドと同じです。
さらにlength
メソッドと同じ機能を果たすsize
メソッドがあり、こちらの方が文字数が短いです。
125B(-8)
c=gets n="23456789" c.size.times do |i| if n.index(c[i])==nil then print c[i] else print c[i-1]*(c[i].to_i-1) end end
5. do...end
size.times
メソッドのdo...end
は{}
で置き換えられます。
変わっている部分がわずかなのでコードは次にまとめます。
120B(-5)
6. ifを条件演算子に
条件演算子とはa ? b : c
という書き方のことで、aがtrue
ならbが、false
ならcが実行されます。ということで、ifを置き換えます。
この辺りから、表示されるコードには見やすさのために改行を入れています。
103B(-17)
c=gets n="23456789" c.size.times{|i| (n.index(c[i])==nil)?(print c[i]):(print c[i-1]*(c[i].to_i-1)) }
7. printはまとめられる
a ? print b : print c
は print (a ? b : c)
と書き換えられます。
97B(-6)
c=gets n="23456789" c.size.times{|i| print (n.index(c[i])==nil)?(c[i]):(c[i-1]*(c[i].to_i-1)) }
8. n.index(c[i]==nil)
(x == nil) ? b : c
は x ? b : c
と書き換えられます。実はx = false
となると結果が違ってしまいますが、今回のケースではindex
メソッドが返すのは数値かnil
なので、大丈夫です。
さらに、n.index(c[i])
はn[c[i]]
と書き換えられます。xに(文字列型としての)数字が入っていればn[x]
はn.index(x)
と同じ働きをします。たとえば、n['2'] = '2'
となり、n['t']
のように存在しない文字の場合はnil
を返します。
78B(-19)
c=gets n="23456789" c.size.times{|i| print(n[c[i]]?c[i-1]*(c[i].to_i-1):c[i])}
準備:i-1
をくくりだす
現状size.times{|i|}
の中で配列のインデックスとしてi
が使われている箇所の中に、1か所だけi-1
が使われています。これを別の変数としてくくりだすことで別のメソッドに置き換えができ、さらなる短縮が可能になります。
86B(+8)
c=gets n="23456789" b=nil c.size.times{|i| print(n[c[i]]?b*(c[i].to_i-1):c[i]) b=c[i]}
9. size.times
とc[i]
の置き換え
先ほどの準備により、size.times{|i|
はchars{|d|
に、c[i]
はd
に置き換えられます。
69B(-17)
c=gets n="23456789" b=nil c.chars{|d| print(n[d]?b*(d.to_i-1):d) b=d}
10. b=nil
rubyの変数は初期化なしで宣言すると初期値がnil
になります。この際大文字で宣言してスコープをグローバルにしないといけません。小文字のローカル変数だとループの中で2回目以降参照できなくなってしまいます。
ちなみに大文字の宣言はグローバル定数なので再代入はwarningが出ますが、warningだけなので無視します。
63B(-6)
c=gets n="23456789" c.chars{|d| print(n[d]?B*(d.to_i-1):d) B=d}
11. print
→ puts
に
改行が入るかどうかが違います。今回はルール上改行が無視されるので置き換えられ、1バイト短くなります。コードは次にまとめます。
62B(-1)
12. nを消す
数字かどうか判定するためにn
を用意していましたが、実はn=d.to_i - 1
とすると、d=nil
→d.to_i = 0
→d.to_i - 1 = -1
となるので、n<0
がtrue
ならアルファベット、false
なら数字と判定できます。
47B(-15)
c=gets c.chars{|d| n=d.to_i-1 puts n<0?B=d:B*n}
13. cを消す
入力のために変数を用意する必要はなく、$<
でOKです。
$<.chars{|d| n=d.to_i-1 puts n<0?B=d:B*n}
最後に、見やすさのために挿入していた改行を取り除きます。
$<.chars{|d|n=d.to_i-1;puts n<0?B=d:B*n}
完成です。40バイトになりました。
ちなみに最短である36バイトのコードでは、gsub
メソッドを使います。
おわりに
当然私はRubyを何もわかっていないので、上の短縮はTSG部員のみんなにSlackで訊ねた結果を載せています。強い人がすぐそばにいるって素晴らしい。教えてくれた@kcz, @hakatashi, @kurgmさんに感謝です。
コードゴルフ場(?)というか、コードゴルフが楽しめるサイトとして anarchy golf があります。
コードゴルフ大会はライブ企画でしたが、YouTubeでストリーミング視聴できます。実況できていませせんが……(画面を追えていない)。2日目にもコードゴルフ大会があり、そちらはちゃんとプレイヤーの画面を追ってくれていました。
スライドも作ったのでリンクを載せておきます。
面白いesolangが多かったので、その紹介記事も余裕があれば書きたいです。
Szkieletor( @gh_end_)