コードゴルフを楽しもう
コードゴルフって聞いたことありますか?
とあるお題をできる限り短い文字数で実装することを競うコンテストの一種なんですけど、たまにやると結構面白いんですよね
何回か会社で「やろうぜやろうぜ」って声かけてやってみたんですけど、やっぱりいきなりだとどうしたら良いかわからないよねってことで、ちょっと僕なりの楽しみ方をまとめてみることにしました
この記事でのゴルフは「遊び・息抜き」であり、「楽しく競う息抜きのゲームである」というスタンスで紹介します
ちょっとでも楽しいこと広がれば嬉しいです
問題選び 〜 やってみる
問題はあんまり難しくなくて良いんです
僕は AtCoder とか codewars とかから選んで持ってきています
難易度のイメージは「解くこと自体に必死にならない」くらいが目安かなと思います
例えばこんな問題でゴルフしてみましょう(例としてとても簡単な問題にしています)
数値のリストが与えられます。偶数の要素の総和を求めなさい。
ex) [1, 2, 3, 4, 5] -> 6
例えば java でこんな実装ができそうです
import java.util.List;
static Integer solve(List<Integer> ns) {
Integer result = 0;
for (Integer i = 0; i < ns.size(); i++) {
if (ns.get(i) % 2 == 0) {
result = result + ns.get(i);
}
}
return result;
}
お行儀が良いですね
解くまでは問題なくできます
数え方
数え方はみんなが同じ方法になっていればなんでも良いですが、例えばwc
コマンドで数えるってのが楽です
$ wc -c a.java
249 a.java
あくまでバイナリサイズとかではなくて文字数を競います
249 文字だったみたいです
こっから短くするんですが、「ほれ、短くせい」と言われても「(´・ω・`)??」になっちゃうので、少しガイドしてみます
個人的に分類してみただけですが、アプローチは4つあります
1. 圧縮する
挙動の変わらない文字はガンガン削ってしまいましょう
ns
がちゃんと複数形になってて偉いですね、けどa
で良いです
solve
もf
でもなんでも良いですし、result
もb
とでもしてしまいましょう、だって動きは変わらないもの
このコードだとfor
とif
の{
も不要ですね
あと忘れがちですが、スペースとインデントなんてもっての外ですよ
挙動が変わらない範囲で切り詰めましょう
あと見落としがちですが、import
も減らせる場合があります
List
は一回しか使ってないので、import
せず fqcn で使っちゃいましょう
最後に改行をつめます
static Integer f(java.util.List<Integer>a){Integer b=0;for(Integer i=0;i<a.size();i++)if(a.get(i)%2==0)b=b+a.get(i);return b;}
みる影もなくなってしまいましたが、127 文字になりました
(お仕事でこれをやってはいけませんよ!)
2. 糖衣構文を使う
同じ挙動の違う書き方のことです
単純な変数名削減とかとはちょっと分けて、いくつか紹介しましょう
(圧縮されてると読みづらいので、一旦コードは最初のものに戻します)
まずは目立つfor
文を拡張for
文にしてしまいましょう
- for (Integer i = 0; i < ns.size(); i++) {
- if (ns.get(i) % 2 == 0) {
- result = result + ns.get(i);
}
}
+ for (Integer n : ns) {
+ if (n % 2 == 0) {
+ result = result + n;
}
}
それから、result
の加算は+=
で良いですね
- result = result + n;
+ result += n;
糖衣構文とはちょっと違いますが、Integer
も可能な範囲でint
にしてしまいましょう
- static Integer solve(List<Integer> ns) {
- Integer result = 0;
- for (Integer n : ns) {
+ static int solve(List<Integer> ns) {
+ int result = 0;
+ for (int n : ns) {
あとは三項演算子です
もともとのif
文にはelse
ブロックはありませんでしたが、何もしないを+ 0
をすると捉えると、書き直せます
- if (n % 2 == 0) {
- result += n;
- }
+ result += n % 2 == 0 ? n : 0;
最終的に、こうなりました
import java.util.List;
static int solve(List<Integer> ns) {
int result = 0;
for (int n : ns) {
result += n % 2 == 0 ? n : 0;
}
return result;
}
圧縮はしてないですが、171 文字になったみたいです
3. ロジックを変える
ここから先は「ほぼ書き直し」になります
ただ、闇雲に書き直すのも当てがなさすぎるので、今回はresult
に着目してみます
result
の初期化、加算、返却と、頻出していますね
こいつを経由せずに直接リスト操作したら済まないかな、ということでStream
を使って書き直してみましょう
static int solve(List<Integer> ns) {
return ns.stream().filter(n -> n % 2 == 0).mapToInt(n -> n).sum();
}
sum
はIntStream
にしか生えてないんですね
圧縮前で 134 文字になったみたいです、が...
2 の糖衣構文を使った結果 ( 171 ) と 3 のロジック変更 ( 134 ) の結果を圧縮してみると、84 vs 98 で、逆転負けしてしまいます
result
という変数自体を排除するために払った.stream()
や.mapToInt(n -> n)
のコストが、result
という変数名をb
で済ませる削減コストを上回ってしまったんですね
このロジックではだめだった、ということです
別のロジックが思いつけばそれも同様に試してみましょう
4. 言語を変える
これはもう完全に「書き直し」もしくは「考え直し」ですね
とりあえず java だとリスト扱うのしんどいので groovy にしてみっか、というアプローチです
( kotlin とかでも意味は同じです ただ説明の都合で慣れてる groovy にしました )
そう、言語は自由です
いやむしろ言語選びから競技は始まっています
ロジックは 3 のresult
変数排除案です
static solve(ns) {
ns.findAll { n -> n % 2 == 0 }.sum()
}
今までの頑張りを嘲笑うように、圧縮前で 62 文字でした
.stream()
と.mapToInt(n -> n)
がないのは当然として、地味にList
をimport
しなくても使えるのと、そもそも引数の型の明記がいらない点も良さそうです
クリア そして...
圧縮して 40 文字、今回はここまでとしましょう
お疲れ様でした!
2 言語くらい書いてみれば、無意識に書いた初版 ( 249 ) の 1/2 ~ 1/3 くらいにはきっとなります
そして「まだ物足りねぇぜ」という人は、groovy でここまでのアプローチ 1, 2, 3 をまた試し、その過程で課題が見えればそれを解決できる次の言語に行くのです
そこには果てしないコードゴルフ沼が広がっています
(この後 Haskell で最終的に 18 文字になりました 僕の引き出しだとこれが FA です)
レギュレーション
実際に企画してみるときに、結構困るのがこの「決まりごと」
この辺は事前に話しておいた方が良いです
- サードパーティライブラリの利用
- java の javaslang や lombok, python の numpy の様な、インストールが必要なもの
- 結果は return するのか print するのか
- さっきの例は print は呼び出し側にさせてました
+return
要否は言語によるし、print のしやすさも言語によるので、決めておきましょう
- さっきの例は print は呼び出し側にさせてました
- 引数の型
- java の
int[]
orList<Integer>
の様に、表現が何通りかできるのは避けた方が良い - 曖昧にしておくと、引数を最初から
IntStream<Integer>
にしておく、みたいな抜け道ができてしまう - 基本的に、数値や文字列をバラ渡しする問題を選ぶのが無難
- java の
- 呼び出しを含むか
-
public static void main(String args[]) {
とかを計上文字数に入れるかどうか(さっきの例では入れなかった) - 含まない方が言語選びの自由度は上がるけど、どうやって実行できればクリアかははっきりさせておこう
-
実際にやってみる
ここは、きっと自由です
楽しければ良いんです
個人的には、こんな感じだと数日ちまちまと楽しめて好きです
- いくつか問題の候補を出し、そこからみんなでいくつか決める
- 2, 3 問あった方が言語相性のせいで競えないみたいな不平等感が減るし、たくさん言語ができる人は戦略が練れて面白い、と思う
- 一週間とか区切って、みんなで好きなときにやる
- 自己ベスト更新のたびに「150 文字切った!」とか「py -> rb にしたら減った!」とかアピールする
- ブラフじゃあないよ!
- 締め日にコード公開、勝者発表
逆に「60 分制限!」なんてのも、楽しそうですね
興味が出たら、ぜひ試してみたり、声を掛け合ってやってみたりしてください!
特別な道具も大掛かりな準備もいらないし、お手軽で楽しいですよ!
それでは、僕は会社でプチ主催しているコードゴルフに戻ります〜
120 文字弱から進展がないので息抜きにこのポエムを書いたという噂ですw
おまけ 言語選びの目安
問題を見て、ざっと使うことになりそうな要素をいくつか想像します
ex) String を List[Char] としてループしたい、flat map、文字列整形、出力
それら要素の得意率が高くなる様な言語から試しましょう
僕だとこんなイメージをして、ruby から試します
(点数は短くなりやすさ、値は例)
lang | String -> [Char] | flat map | 文字列整形 | 出力 | 合計 |
---|---|---|---|---|---|
haskell | 6 | 3 | 1 | 2 | 12 |
ruby | 3 | 2 | 4 | 4 | 13 |
python | 4 | 1 | 4 | 3 | 12 |
あと、コードがすっきりかけてもこの辺りでぐぬぬってなりがちなので、想像してみると面白いです
- 型推論があるか
- 引数 / 戻り値の型明記が必要か
- インデントが挙動に影響するか
- if ブロック等を作るのに end みたいな記述が必要か
- やりたいことが import 不要でできるか
- 標準ライブラリが潤沢か
- 数値 -> 文字列 とかがやりやすいか
- return を省略できるか
- flat_map があるか
- lambda 式をメソッド参照で置き換えられるか
言語選びが一番楽しいと言っても過言ではないので、そんなに得意じゃあない言語でも試してみるのがおすすめです
Discussion
とても面白く勉強になりました!よければHaskellのコードって見せてもらうこと可能ですか?