はじめに
このページは、一見正しそうなコードを書いているものの、 実は code golf 的にはあまり上手くいっていない事例をまとめたものです。
私が失敗するたびに、順次増えていく予定(増えないことを祈りつつ)。
つぶやき Processing にチャレンジする皆様方にとって 他山の石となれば幸いです。
range の機能を素直に使った方が良い事例
以下の作品の x 座標に関する処理は、一見、工夫をしているように見えます。 しかし、Python の range の機能を使って、素直に書いた方が、 結果として全体のコード量が減るーという説明をします。
#つぶやきProcessing
— Koji Saito (@KojiSaito) July 13, 2020
size(500,500)
background(-1)
stroke(0)
R=range
N=noise
L=line
for y in R(50):
for x in R(-50,50):
a=N(x*.1,y*.1)*100
p=N(x*.1+.1,y*.1)*100
q=N(x*.1,y*.1+.1)*100
L(x*10+(y-1)*5,y*10+a,x*10+10+(y-1)*5,y*10+p)
L(x*10+(y-1)*5,y*10+a,x*10+y*5,y*10+10+q) pic.twitter.com/loXHbbg4t7
これは noise 関数の値を可視化する作品ですが、 プログラムの構造としては、x 軸 y 軸方向にそれぞれループを構成し、 対象の地点の値(=a)および右隣の値(=p)、ひとつ下の値(=q)を求め、 それらを用いて局面のグリッドを描いています。
なお、ここで着目すべき点は、あくまでも x 軸方向の range の使い方なので、 2 重ループになっているところについては、言及しません。
もし、2 重ループの単ループ化に興味のある場合は ループをまとめる http://koji.jpn.org/tweet_processing_tech/loop-compaction/ を御覧ください。
ループ変数 x,y の役割
ループ変数 x,y は、それぞれ range(-50,50), range(50) として、
- x は -50,-49,-48, … ,49
- y は 0,1, … ,49
という値をとります。
これらの値を用いて、a=N(x*.1,y*.1)*100=noise(x*0.1,y*0.1)*100 として 現在着目している場所=ノイズ関数における (x*0.1,y*0.1)の値を求めています。
一方、実際の描画に使用する line 関数では、
# L=line なので
line(x*10+(y-1)*5,y*10+a,x*10+10+(y-1)*5,y*10+p)
として、(x*10,y*10) という座標値が基準となっています。
つまり、ループ変数 x,y の役割としては、
- noise 関数の値を得るための 0.0, 0.1, 0.2, … ,4.9 という値と
- line 関数で使用する座標値としての 10 刻みの整数値
を生成するための役割を担っていることが分かります。
そして range(50) x range(-50,50) という範囲の 2 重ループを形成しているので、 プログラマに対し、格子サイズ 50x100 における値を計算していることも 表明しています。
私の場合、このグリッドサイズを示す点に重きをおいているため、 上記のようなツィートになっている状況です。
range の機能を使った場合
実際のツィートでも、プログラム全体を 1 ツィートに収めているので、 ある意味間違いではありません。
しかし、line 関数の引数に着目すると、 x*10 というコードが 4 箇所に出てきます:
L=line
L(x*10+(y-1)*5,y*10+a,x*10+10+(y-1)*5,y*10+p)
# ^^^^ ^^^^
L(x*10+(y-1)*5,y*10+a,x*10+y*5,y*10+10+q)
# ^^^^ ^^^^
ここで、例えば x の for ループに対し、
for x in R(-50,50):
ではなく、
for x in R(-500,500,10):
という範囲を用いた場合はどうでしょうか。 この場合、それぞれのコードを比較すると、
for x in R(-50,50):
for x in R(-500,500,10):
となり、5 文字増えています。
しかし、line 関数の処理では x*10 が単に x という表記で良くなるため、 3 文字削減が 4 箇所分で 12 文字の削減となっています。
しかし、noise 関数の引数では、
a=N(x*.1,y*.1)*100
p=N(x*.1+.1,y*.1)*100
q=N(x*.1,y*.1+.1)*100
を
a=N(x*.01,y*.1)*100
p=N(x*.01+.1,y*.1)*100
q=N(x*.01,y*.1+.1)*100
としなければならず (x の変動量が 10 倍になっているので、*0.1 を *0.01 にする必要があります)、 結果として 3 文字の増加となっています。
これらをまとめると、ループ変数 x に対し、 range(-50,50) ではなく range(-500,500,10) とした場合の文字数の変化量は、 +5-12+3 = -5 となり 5 文字の削減となります。
range(-50,50) の方がループの変化量は単純ですし(なので、増分の +1 は省略されている)、 また、line 関数における x*10 という記述も、何か処理をしている雰囲気があります (実際に処理をしているので、「雰囲気」という表現も語弊があるかもしれませんが)。
でも言い換えれば、処理をさせるという記述が必要であり、 ここに文字数を要してしまっていた ー ということにも他なりません。
この事例のまとめ
つぶやき Processing においては、コードの文字数を圧縮する必要があります。 これはつまり、 素直に記述するだけでは駄目で「圧縮する」という特別な行為が必要であることを 暗に意味しています。
今回のループ変数 x の処理は、普通に記述すればおそらく range(-500,500,10) という記述になると思います。
一方、range(-50,50) として、使用する時に x*10 という変換処理をすると、 なんだか特殊な記述をしている気分になります。
このように、一見正しそうな処理をしているところこそが問題である、という指摘です。 事実、今回検証するため、私は自分のコード ー つまり range(-50,50) として毎回 x*10 するもの ー の方がコード量が短いと思っていました。
しかし、実際には少々トリッキーに見えるコードの方が文字数が多く、 素直に記述した方が文字数が少なくなる、という状況でした。
素直に書いた方が code golf という視点では効果があった、ということです。 まさに蛇足。策士策に溺れる、といったところでしょうか。
code golfing に関わらず、同様の事例は最適化という視点でも発生しています。 人力による最適化を施したコードを書くと、かえって遅くなるという減少です。 一方、素直に書くとコンパイラの最適化処理が適用され、 結果として実行速度が上がるといった例です。
※ 安物のコンパイラしか与えられず、 「これでなんとかしろ」という状況では、 コンパイラの最適化処理だけでは要求水準を満たさないため人力で工夫した方が早くなる、 といった例もあるのでやっかいです。 もちろん、コンパイラの最適化処理も近年ではなかなか優秀なので、 本気で取り組まないと勝つことは難しいですが。
今から 30 年ほど前、仲間内で話題になっていた話でもあり、 私自身もよく理解していたつもりでした。
まさか code golf においても体験するとは…。 この事例が皆さんの他山の石となれば幸いです。