なぜぼくが書く競技プログラミング用のソースコードは"汚い"のか
はじめに
こんにちは。かえると言います。ぼくはAtCoderを最近そこそこやっています。33歳です。業務経験は9年だか10年だかあります。
競技プログラミングのコードが"読みにくく"なることについて、競技プログラミングをやっていないと「なにこれ」「こんなコードを業務で書かれると困るんだけど……」となることもあると思います。また、「競技プログラミングのコードちょっと見てみたけど読みにくすぎるな〜」と思った人もいると思います。なので、それに関する話をしていきたいと思います。
ぼくは昔、「なんで競技プログラミングで書かれてるソースコードはこんな感じになるのだろう」と思っていましたが、気がつくとぼくもそうなっていました。
この記事では、ぼくがなぜそういう「読みにくいコード」を競技プログラミングで書いているのかを説明したいと思います。
ただ、「読みにくい」のはそれはそうだと思うのですが、そうではなく、競技プログラミングの性質を知らずに、それをぶった切って「汚い」とか「クソコード」と強く表現されているのを見かけると、わりと悲しいです。あまり見かけはしませんが…
ちなみに競技プログラミングと言っていますが、ぼくはほぼAtCoderしかやっていないので、他の競技プログラミングについては実はエアプです。あくまで1つの参考意見だと考えて読んでもらえると幸いです。タイトルは少し目をひくようなものにしています。
この記事では、
- 実際ぼくはどのようなコードを書くのか
- 競技プログラミングの性質について(※ 個人の感想です)
- なぜ読みにくさが発生するのか
- 競技プログラミングしている人が書くコードは読みにくいのか?
の順に説明したいと思います。
なにかの参考になれば幸いです。
(ちなみにぼく自身は、別に競技プログラミングで提出されているコードを特別「汚い」「読みにくい」と感じることはありません)
実際ぼくは競技プログラミングではどのようなコードを書くのか
これは先日ぼくが生成したコードですが、だいたいこんなコードになります。見ると「ウワーッ」となると思います。ぼくもハッキリと、これはつらいコードであると思います。
これは、二分探索とダイクストラ法を用いて、迷路の問題を解いた様子です。これは30分ぐらいで書きました。
→ https://atcoder.jp/contests/mujin-pc-2018/tasks/mujin_pc_2018_e
これに関して、想定される批判は次のようになると思います。
- 処理のあとに関数を入れて、その関数の下にまた処理を書くのはよくないのでは
- フォーマットかけたほうがいいのでは
- 変数名はちゃんとつけたほうがいいのでは
- コメントは入れたほうがいいのでは
- マジックナンバーェ……
などなどです。こうした懸念がわいてくるのはもっともなことだと思います。
なので、それがなぜ起こるのかについて、説明したいと思います。
この時点で「こんなコードを書くような人の話は読むに値しない」と思ってしまわれると困るのですが、ひきつづき読んでいただけると幸いです。
競技プログラミングの性質について
競技プログラミングでは、有名なアルゴリズムを利用して解ける問題が出題されます。内容としては、大学受験の数学の問題を解くことに近いと思います。共通テスト(センター試験)の数学は、大問1、大問2、大問3、大問4のように出題されるので、ぼくの感覚ではこれが一番近いと思っています。やることが違うので難しさは単純には比較はできませんが、AtCoderに関しては、ぼくが受けた当時のセンター試験数学よりは難しいと思います(※個人の感想です)
競技プログラミングには、制限時間がとても長いものもありますが、短いものが多いです。だいたい1時間から2時間ぐらいで終了すると思います。
また、競技プログラミングには「競技」という性質があり、他の人と争います。他の参加者より1秒でも早く提出することにインセンティブがあります。もう提出してもいいような状態のときに綺麗に修正しても順位が落ちてしまうので、綺麗にするというモチベーションは発生しにくいです。
コードは1つのファイルで完結させます。ファイルを分割することはしません。ソースコードの行数は、たいてい3行から200行におさまると思います(たぶん)
つまり
- 数学に結構近いアルゴリズムの問題を数問解く
- 時間がかなりタイト
- 早く提出したほうが圧倒的に得
- 1つのファイルに書く
という4点が、業務で書くときとかなり違った環境になると思います。
業務で例えるなら「要件定義はできたから今から作り始めて!2時間後にリリースするよ!」というような形の時間圧力を受けることになると思います。
つまり競技プログラミングは、業務とは違ったルールのもとで行うプログラミングなので、業務では常識的だとされる論理が通用しないことがあります。もちろん、競技プログラミングでは常識的な論理でも業務では通用しないことも、当然あります。
「サッカーではハンドが反則だからといって、書道も足でやらなければならないわけではない」ということと同じだと思っています。
なぜ読みにくさが発生するのか
可読性は絶対的なものではないから
所変われば品変わるといいます。ぼくは「可読性」というのは絶対的なものではないと思います。
たとえば、クリーンアーキテクチャを受け入れていない人から見ると、クリーンアーキテクチャは可読性が高くありません。一方で、クリーンアーキテクチャがチーム内に浸透していれば、クリーンアーキテクチャに沿ったコードは「可読性が高い」と判定されると思います。
「可読性が悪い」という発言は、「あなたのコードは、他のみんなも読みにくいって言うと思うよ」を暗示している発言にも見えます。コードそれ自体が可読性を持つのではなくて、評価者である人間がいて、評価者が読みやすさを決めるので、「可読性」というのは評価者の主観による尺度にならざるを得ないと思います。まあそんなこと言ったら、だいたい全部主観なんですけど。
可読性は、協調するとき・チーム開発のときにおいては非常に重要なことだと思います。だから「可読性が悪いからなんだというのだ!」「読みにくいと感じるのはあなたの能力が低いからだ!」というような意見は論外だと思います。
それでも、可読性は常に絶対に盲信すべきものでもないと思います。
アルゴリズム関連のコードは本質的に読みにくいから
次のpython3のコードは、その次に示した図形を描画します。このコードでは、プログラミングの考え方はfor文しか使っておらず、ライブラリも使っていないので、プログラミング歴が短くても読もうと思えば読めるし、一見簡単に見えますが、その裏には数学的な難しさが潜んでいると思います。
N = 20
token = "‹‹\\(*'ω'* )/››"
palette = token + ' ' * (N - len(token))
for i in range(N):
s = ''
for j in range(i, i + N):
s += palette[j % N]
print(s)
‹‹\(*'ω'* )/››
‹\(*'ω'* )/›› ‹
\(*'ω'* )/›› ‹‹
(*'ω'* )/›› ‹‹\
*'ω'* )/›› ‹‹\(
'ω'* )/›› ‹‹\(*
ω'* )/›› ‹‹\(*'
'* )/›› ‹‹\(*'ω
* )/›› ‹‹\(*'ω'
)/›› ‹‹\(*'ω'*
)/›› ‹‹\(*'ω'*
/›› ‹‹\(*'ω'* )
›› ‹‹\(*'ω'* )/
› ‹‹\(*'ω'* )/›
‹‹\(*'ω'* )/››
‹‹\(*'ω'* )/››
‹‹\(*'ω'* )/››
‹‹\(*'ω'* )/››
‹‹\(*'ω'* )/››
‹‹\(*'ω'* )/››
↓のような感じで遊ぶときにも、少し難しくなると思います。これは、🚗を真ん中に固定しつつ、背景をどんどん回転させている様子です。
import random
import time
tree = '🌴'
house = '🏡'
car = '🚗'
N = 25
tree_count = random.randint(2, 5)
house_count = random.randint(2, 5)
scene = [tree] * tree_count + [house] * house_count + [' '] * (N - tree_count - house_count)
random.shuffle(scene)
for i in range(N):
s = ''
for j in range(i + N - 1, i - 1, -1):
if j - i == N // 2:
s += car
else:
s += scene[j % N]
print(s, end='\r')
time.sleep(0.1)
描画された結果は誰の目にも明らかで、for文ぐらいしか使っていないので簡単にみえますが、何も見ずに実装しようとすると、意外と「おや……」となるのがアルゴリズム関連の実装だと思います。これは実際に実装しようとすると難しさがよくわかります。「あれ?思ってたより難しくない??」みたいになります。
時間がタイトで、早く提出することに大きなインセンティブがあるから
競技プログラミングは競技なので、タイピングする時間をなるべく短くしたいという考え方があります。自動補完の時間すらもったいない場合もあります。
また、競技中は、誰かにソースコードを読んでもらう必要がありません。だから、自分さえわかればいいので、x
でもy
でもp
でもq
でもc
でもvv
でも、なんでもいい感じになります。
同じ問題を解くのでも、5秒早く解ければ順位が1つ上がることがあります。
業務では、命名を考えるときにあとで困らないように少し時間をとって考えたり、これは結局どういう概念なんだっけというふうに考えることができたりしますが、そういうふうにしているとコンテストで勝てないわけです。ここで命名をゆったり考える行為は「格ゲーの対戦の最中に、画面から目を離して『う〜ん、どうしようかな』と考え始める」みたいな行為に相当すると思います。
また、問題を見た瞬間に解答が浮かんでくるような場合は、もうあとは、自分の脳の中で完成しているコードをなるべく速く実装するだけの、手と指の運動に近い感じになると思います。すごく強い人は、なんだか難しそうなアルゴリズムを、よくわからないスピードで実装します。
ただ、変数が1文字だったとしても、d
と書いたら距離
、r
と書いたら半径
みたいなものは、なんとなく共通言語としてあると思います。だから、distance_list
などとは書かずにds
とかdl
などと書いたりすると思います。べつに何か決まっているわけではないと思います。
1つのファイルに全部書くし、コードの行数が少ないから
これはぼくだけかもしれませんが、メソッドを作ったときに、メソッドをいい感じの場所に移動する時間がもったいないです。だから、考えた順だったり、呼び出したい順にコードが並んでいることが多いと思います。
だから、書いた直後の本人にとっては自明なコードでも、他の人から見ると「ウワーッ」となるようなコードになっていたりするのかもしれません。
競技プログラミングしている人が書くコードは読みにくいのか?
こういうことを言っている人とは出会ったことはありませんが、ググったらそういう主張をしている人をちらほら見かけました。
ただ、これはぶっちゃけ偏見だと思っています。
競技プログラミングで書くようなコードの書き方を完全に業務で導入されたら怒ってもいいと思います。ただそれは、その人が競技プログラミングをやっているからとかではなくて、「書道では手を使っていたからサッカーでも手を使うぜ!」みたいな人だったから、ということではないかと思います。(ぼくはそういう人は見かけたことがありませんが……)
たとえば、↓のコードは比較的綺麗だと思います。これを「なんて汚いコードなんだ……っ」と言われてしまうと困ってしまいますが、吐き気を催すようなレベルのものではないと思います。
from collections import deque
INFINITY = 1e18
def calculate_distances(graph, start_vertex):
"""グラフと始点を与えると、始点から他の各点までの距離を幅優先探索を用いて計算して返す"""
max_out = max([max(out) for out in graph.values()])
max_vertex = max(max(graph.keys()), max_out)
distances = [INFINITY] * (max_vertex + 1)
queue = deque()
queue.append(start_vertex)
distances[start_vertex] = 0
while len(queue) > 0:
vertex = queue.popleft()
for next_vertex in graph[vertex]:
if distances[next_vertex] == INFINITY:
distances[next_vertex] = distances[vertex] + 1
queue.append(next_vertex)
return distances
if __name__ == '__main__':
graph = {
0: [4],
1: [2, 3],
2: [3, 4],
3: [1],
4: [2]
}
distances = calculate_distances(graph, 0)
for i, distance in enumerate(distances):
print(f'点0から点{i}までの距離: {distance}')
点0から点0までの距離: 0
点0から点1までの距離: 4
点0から点2までの距離: 2
点0から点3までの距離: 3
点0から点4までの距離: 1
これが競技プログラミングでは↓のようになります。余計なものがすべて削ぎ落とされる感じです。書くのが早いです。
from collections import deque
G = {
0: [4],
1: [2, 3],
2: [3, 4],
3: [1],
4: [2]
}
m = max(max(G.keys()), max(max(out) for out in G.values()))
d = [1e18] * (m + 1)
q = deque()
q.append(0)
d[0] = 0
while len(q) > 0:
v = q.popleft()
for nex in G[v]:
if d[nex] == 1e18:
d[nex] = d[v] + 1
q.append(nex)
print(*d)
0 4 2 3 1
そういう感じなのだと思います。
「競技プログラミングのコードってなんで読みにくいんだろう?」と思っていた人に「そういうこともあるのか〜〜」と思ってもらえたら、とても幸いです。
何か捕捉や「これ違うよ」という点などあれば、コメントをもらえるとうれしいです。
Discussion