⛳️

コードゴルフを楽しもう

2020/10/25に公開1

コードゴルフって聞いたことありますか?

とあるお題をできる限り短い文字数で実装することを競うコンテストの一種なんですけど、たまにやると結構面白いんですよね

何回か会社で「やろうぜやろうぜ」って声かけてやってみたんですけど、やっぱりいきなりだとどうしたら良いかわからないよねってことで、ちょっと僕なりの楽しみ方をまとめてみることにしました

この記事でのゴルフは「遊び・息抜き」であり、「楽しく競う息抜きのゲームである」というスタンスで紹介します

ちょっとでも楽しいこと広がれば嬉しいです

問題選び 〜 やってみる

問題はあんまり難しくなくて良いんです

僕は 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で良いです
solvefでもなんでも良いですし、resultbとでもしてしまいましょう、だって動きは変わらないもの

このコードだとforif{不要ですね

あと忘れがちですが、スペースとインデントなんてもっての外ですよ
挙動が変わらない範囲で切り詰めましょう

あと見落としがちですが、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();
}

sumIntStreamにしか生えてないんですね

圧縮前で 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)がないのは当然として、地味にListimportしなくても使えるのと、そもそも引数の型の明記がいらない点も良さそうです

クリア そして...

圧縮して 40 文字、今回はここまでとしましょう

お疲れ様でした!

2 言語くらい書いてみれば、無意識に書いた初版 ( 249 ) の 1/2 ~ 1/3 くらいにはきっとなります

そして「まだ物足りねぇぜ」という人は、groovy でここまでのアプローチ 1, 2, 3 をまた試し、その過程で課題が見えればそれを解決できる次の言語に行くのです

そこには果てしないコードゴルフ沼が広がっています

(この後 Haskell で最終的に 18 文字になりました 僕の引き出しだとこれが FA です)

レギュレーション

実際に企画してみるときに、結構困るのがこの「決まりごと」

この辺は事前に話しておいた方が良いです

  • サードパーティライブラリの利用
    • java の javaslang や lombok, python の numpy の様な、インストールが必要なもの
  • 結果は return するのか print するのか
    • さっきの例は print は呼び出し側にさせてました
      +return要否は言語によるし、print のしやすさも言語によるので、決めておきましょう
  • 引数の型
    • java のint[]orList<Integer>の様に、表現が何通りかできるのは避けた方が良い
    • 曖昧にしておくと、引数を最初からIntStream<Integer>にしておく、みたいな抜け道ができてしまう
    • 基本的に、数値や文字列をバラ渡しする問題を選ぶのが無難
  • 呼び出しを含むか
    • 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

AtridottAtridott

とても面白く勉強になりました!よければHaskellのコードって見せてもらうこと可能ですか?