【まとめ】僕の機械学習【2017年】

懺悔

このツイートは2017年1月1日に僕の勉強用のアカウントから僕の手によってツイートされたものです。

しかしながら僕は今日の今まで、毎日のように「機械学習をしよう、しなければ」と言いながら何もしてきませんでした。

目的とモチベーションについて

僕は「機械学習をしよう」といい続けていきましたが、これが達成されなかったのには以下の3点が考えられます。

  • モチベーションがなかった
  • 機械学習は目的ではない
  • 機械学習のハードルを高く感じている

1. モチベーションがなかった

これはとても重大な原因です。

正直、上記のツイートのせいでやらなければいけない気になっていただけで、モチベーションそのものが追いついていなかったと考えます。

2. 機械学習は目的ではない

機械学習は、目的ではなく手段です。

これはプログラミングも同様で、何かを達成するための手段は目的がなければ意味がないのです。

なにかの目的があるからこそ我々はそれを学び、利用し、そして達成するということをこの1年間でよく学べたと思います。

3. 機械学習のハードルを高く感じている

私は今年のうちに読みもしない機械学習に関しての本を3冊も購入しました。

そしてその本を開いて10ページのうちにハテナが浮かぶものが2冊ありました。

これは私の脳みそがスッカラカンであることが主な原因ではありますが、理解ができないなりにも努力というものを怠っていたなと今になって感じます。

要は気持ちの問題なのです。

最後に

私は、今回の機械学習のように「絶対に取り組むぞ!」といったようなことには取り組めないような傾向にあるなぁと今年を振り返ります。

更には、「絶対に手をつけない」と言っていたJavaScriptには半強制的に触らなければいけない状況に陥りました。

やりたいことはすぐに手を付ける をモットーに2018年は精進していきます!

グループワークに向けて(4)

今回の目的

前回の内容を自分が以前書いたコードを使用して実践します。また、「リーダブルコード」の残されている部分に触れていきます。

実践しよう!

変更前

今回使用するコードは、僕が以前書いたlife gameのコードです。当時、life gemeの仕組みがよくわかっていなかったので、セルが誕生か死亡するときにエフェクトをつけています。確か3,4ヶ月ほど前のコードなのでどの様に書いたかは完全に忘れています。言語はprocessingの2系で書いています。

int num = 400;
int side = 2;
int [][] pc = new int [400][400];
int [][] cell = new int [400][400];
int [][] nc = new int [400][400];
int c = 0;
void setup() {
  frameRate(500);
  fill(100, 255, 100);
  noStroke();
  size(800, 800);
  for (int i = 0; i < 400; i++) {
    for (int j = 0; j < 400; j++) {
      cell[i][j] = (int)random(6);
      if (cell[i][j] > 1) {
        cell[i][j] = 0;
      }
    }
  }
}
void draw() {
  background(0);
  for (int i = 0; i < num; i++) {
    for (int j = 0; j < num; j++) {
      if (cell[i][j] == 1) {
        if (frameCount%50 != 0) {
          if (pc[i][j] == 0) {
            fill(0, 5*(frameCount%50)+5, 0);
            rect(i*side, j*side, side, side);
          } else {
            fill(0, 255, 0);
            rect(i*side, j*side, side, side);
          }
        }
      } else if (cell[i][j] == 0) {
        if (frameCount%50 != 0) {
          if (pc[i][j] == 1) {
            fill(0, 255-5*(frameCount%50), 0);
            rect(i*side, j*side, side, side);
          }
        }
      }
    }
  }
  if (frameCount%50 == 0) {
    for (int i = 0; i < num; i++) {
      for (int j = 0; j < num; j++) {
        if (cell[i][j] != 0) {
          fill(0,255,0);
          rect(i*side, j*side, side, side);
        }
        count(i, j);
        judge(i, j);
      }
    }
  }
  if (frameCount%50 == 0) {
    for (int i = 0; i < num; i++) {
      for (int j = 0; j < num; j++) {
        pc[i][j] = cell[i][j];
        cell[i][j] = nc[i][j];
      }
    }
  }
}

int count(int x, int y) {
  c = 0;
  if (cell[(x+399)%400][(y+399)%400] >= 1)
    c++;
  if (cell[x%400][(y+399)%400] >= 1)
    c++;
  if (cell[(x+1)%400][(y+399)%400] >= 1)
    c++;
  if (cell[(x+399)%400][y%400] >= 1)
    c++;
  if (cell[(x+1)%400][y%400] >= 1)
    c++;
  if (cell[(x+399)%400][(y+1)%400] >= 1)
    c++;
  if (cell[x%400][(y+1)%400] >= 1)
    c++;
  if (cell[(x+1)%400][(y+1)%400] >= 1)
    c++;
  return c;
}

void judge(int x, int y) {
  if (cell[x][y] == 0) {
    nc[x][y] = (c==3 ? 1 : 0);
  } else if (cell[x][y] == 2) {
    nc[x][y] = 1;
  } else if (cell[x][y] == 1) {
    if(c==2||c==3){
      nc[x][y] = 1;
    }else{
      nc[x][y] = 0;
    }
  }
}

ため息が出るほど汚いコードですね...。ではここからまずは表面的に変えていきましょう。

第一段階

まずはグループワークに向けて(2)で取り扱った『表面上の改善』を行っていきましょう。

static final int NUM  = 600;
static final int SIDE = 2;
static final int UNIT_PERIOD = 50;
int [][] previousCell = new int [NUM][NUM];
int [][] currentCell  = new int [NUM][NUM];
int [][] nextCell     = new int [NUM][NUM];
int c = 0;

void setup() {
  size(NUM*SIDE, NUM*SIDE);
  frameRate(500);

  fill(100, 255, 100);
  noStroke();

  // セルを1/6の確率で生きている状態
  // その他は死んでいる状態にする
  for (int i = 0; i < NUM; i++) {
    for (int j = 0; j < NUM; j++) {
      currentCell[i][j] = (int)random(6);
      if (currentCell[i][j] > 1) {
        currentCell[i][j] = 0;
      }
    }
  }
}

void draw() {
  background(0);
  
  for (int i = 0; i < NUM; i++) {
    for (int j = 0; j < NUM; j++) {
      
      // セルの生死の移り変わりを表現
      if (currentCell[i][j] == 1) {
        if (frameCount%UNIT_PERIOD != 0) {
          if (previousCell[i][j] == 0) {
            fill(0, 5*(frameCount%UNIT_PERIOD)+5, 0);
            rect(i*SIDE, j*SIDE, SIDE, SIDE);
          } else {
            fill(0, 255, 0);
            rect(i*SIDE, j*SIDE, SIDE, SIDE);
          }
        }
      } else if (currentCell[i][j] == 0) {
        if (frameCount%UNIT_PERIOD != 0) {
          if (previousCell[i][j] == 1) {
            fill(0, 255-5*(frameCount%UNIT_PERIOD), 0);
            rect(i*SIDE, j*SIDE, SIDE, SIDE);
          }
        }
      }
      
    }
  }
  
  // 次世代を計算し、代入し直す
  if (frameCount%UNIT_PERIOD == 0) {
    for (int i = 0; i < NUM; i++) {
      for (int j = 0; j < NUM; j++) {
        if (currentCell[i][j] != 0) {
          fill(0, 255, 0);
          rect(i*SIDE, j*SIDE, SIDE, SIDE);
        }
        countAroundLiveCell(i, j);
        decideNextGeneration(i, j);
      }
    }
    for (int i = 0; i < NUM; i++) {
      for (int j = 0; j < NUM; j++) {
        previousCell[i][j] = currentCell[i][j];
        currentCell[i][j] = nextCell[i][j];
      }
    }
  }
}

// 2次元トーラスを再現
int countAroundLiveCell(int x, int y) {
  c = 0;
  if (currentCell[(x+NUM-1)%NUM][(y+NUM-1)%NUM] >= 1) c++;
  if (currentCell[x%NUM][(y+NUM+1)%NUM] >= 1)         c++;
  if (currentCell[(x+1)%NUM][(y+NUM-1)%NUM] >= 1)     c++;
  if (currentCell[(x+NUM-1)%NUM][y%NUM] >= 1)         c++;
  if (currentCell[(x+1)%NUM][y%NUM] >= 1)             c++;
  if (currentCell[(x+NUM-1)%NUM][(y+1)%NUM] >= 1)     c++;
  if (currentCell[x%NUM][(y+1)%NUM] >= 1)             c++;
  if (currentCell[(x+1)%NUM][(y+1)%NUM] >= 1)         c++;
  return c;
}

// (x, y)にあるセルの次の世代での生死をnextCellに代入
// 死なら0、生なら1、生存なら2(?)
void decideNextGeneration(int x, int y) {
  if (currentCell[x][y] == 0) {
    nextCell[x][y] = (c==3 ? 1 : 0);
  } else if (currentCell[x][y] == 2) {
    nextCell[x][y] = 1;
  } else if (currentCell[x][y] == 1) {
    if (c==2||c==3) {
      nextCell[x][y] = 1;
    } else {
      nextCell[x][y] = 0;
    }
  }
}

変数名や関数名をわかりやすくしてコメント入れてレイアウトを気にして...という感じに改善してみました。コードはほとんど読まずに変更したので本当に表面上の改善でしかありませんが、前のコードよりは読みやすくなっているのかな?という感じですね。

第二段階

次にグループワークに向けて(3)の『ループとロジックの単純化』を元に問題点を探しましょう。

  1. 不要なグローバル変数cが存在する
  2. draw関数内のネストが深い
  3. countAroundLiveCell(x, y)関数がわかりにくい(汚い)
  4. 三項演算子が使われているがわかりやすさはどうか
  5. 描画する際の色がわかりずらい
  6. セルの状態2は必要なのか?
  7. 定数に付いてる staticも不要ではないか

これらの問題点を元に、第一段階のコードを変更していきましょう。

final int   NUM         = 200;
final int   SIDE        = 5;
final int   UNIT_PERIOD = 50;
final color GREEN       = color(0, 255, 0);

int [][] previousCell = new int [NUM][NUM];
int [][] currentCell  = new int [NUM][NUM];
int [][] nextCell     = new int [NUM][NUM];

void setup() {
  size(NUM*SIDE, NUM*SIDE);
// 透明度の範囲を0〜(UNIT_PERIOD-1)に変更
  colorMode(RGB, 256, 256, 256, UNIT_PERIOD);
  frameRate(500);
  noStroke();

  // セルを1/6の確率で生きている状態
  // その他は死んでいる状態にする
  for (int i = 0; i < NUM; i++) {
    for (int j = 0; j < NUM; j++) {
      currentCell[i][j] = (int)random(6);
      if (currentCell[i][j] > 1) {
        currentCell[i][j] = 0;
      }
    }
  }
}

void draw() {
  background(0);
  // 次世代を計算し、代入し直す
  if (frameCount%UNIT_PERIOD == 0) {
    for (int i = 0; i < NUM; i++) {
      for (int j = 0; j < NUM; j++) {
        countAroundLiveCell(i, j);
        decideNextGeneration(i, j);
        if (currentCell[i][j] == 0) continue;
        fill(GREEN, UNIT_PERIOD);
        rect(i*SIDE, j*SIDE, SIDE, SIDE);
      }
    }
    for (int i = 0; i < NUM; i++) {
      for (int j = 0; j < NUM; j++) {
        previousCell[i][j] = currentCell[i][j];
        currentCell[i][j] = nextCell[i][j];
      }
    }
  } else {
    for (int i = 0; i < NUM; i++) {
      for (int j = 0; j < NUM; j++) {
        // セルの生死の移り変わりを表現
        if (currentCell[i][j] == 0) {
          if (previousCell[i][j] == 0) continue;
          fill(GREEN, UNIT_PERIOD-(frameCount%UNIT_PERIOD));
        } else {
          if (previousCell[i][j] == 0) fill(GREEN, frameCount%UNIT_PERIOD);
          else fill(GREEN, UNIT_PERIOD);
        }
        rect(i*SIDE, j*SIDE, SIDE, SIDE);
      }
    }
  }
}

// 2次元トーラスを再現
int countAroundLiveCell(int x, int y) {
  int c = 0;

  int left   = (x+NUM-1)%NUM;
  int top    = (y+NUM-1)%NUM;

  int center = x%NUM;
  int middle = y%NUM;

  int right  = (x+1)%NUM;
  int bottom = (y+1)%NUM;

  if (currentCell[left]   [top]    == 1) c++;
  if (currentCell[center] [top]    == 1) c++;  
  if (currentCell[right]  [top]    == 1) c++;
  if (currentCell[left]   [middle] == 1) c++;
  if (currentCell[right]  [middle] == 1) c++;
  if (currentCell[left]   [bottom] == 1) c++;
  if (currentCell[center] [bottom] == 1) c++;
  if (currentCell[right]  [bottom] == 1) c++;
  return c;
}

// (x, y)にあるセルの次の世代での生死をnextCellに代入
// 死なら0、生なら1
void decideNextGeneration(int x, int y) {
  int count = countAroundLiveCell(x, y);
  
  if (currentCell[x][y] == 0) nextCell[x][y] = (count==3           ? 1 : 0);
  else                        nextCell[x][y] = (count==2||count==3 ? 1 : 0);
}
  1. ccountAroundLiveCell関数内のローカル変数にし、独立させた
  2. 同じ内容を括りだしたり、順序を入れ替えてわかりやすく変更
  3. 中身を整理して変数に置き換えることでわかりやすくした
  4. 三項演算子のほうがわかりやすい、逆にif文のが分かりづらかったので変更
  5. GREENという定数に置き換え、透明度で濃淡を変えることに
  6. 不要だったので排除した
  7. 上に同じく、排除した

特に3番の内容変更を見てください。実は第1段階のコードではcountAroundLiveCell内の符号を間違えていたため、プログラムが思った通りに動いていませんでした。(関数内2行目[(y+NUM+1)%NUM] ⇒ [(y+NUM-1)%NUM])それを変数に置き換えて分割することで、わかりやすくなった上にバグもなくせました。素晴らしい変更ですね!

最終段階

最後は第Ⅲ部の『コードの再構成』に準じて、コードを考え直してみましょう。考え直すことをリストアップしてみました。

  • 無関係の下位問題がたくさんある
  • セルをインスタンスにしたほうがわかりやすい
  • 本当に読みやすいコードになっているのか?

以上を踏まえて、コードを再構成してみました。まずはCellクラスから!

class Cell {
  // row    : 横の行
  // column : 縦の列
  // 0からスタート
  final int row;
  final int column;

  boolean  wasArive = false;
  boolean   isArive;

  Cell(int _row, int _column) {
    this.row    = _row;
    this.column = _column;
    // セルを1/6の確率で生にする
    this.isArive = int(random(10))%7==0;
    this.wasArive = this.isArive;
  }

  int cellAlpha() {
    if (frameCount%UNIT_PERIOD==0)     return UNIT_PERIOD-1;
    if (this.isArive == this.wasArive) return UNIT_PERIOD-1;
    else if (this.isArive == true)     return frameCount%UNIT_PERIOD;               // 誕生
    else                               return UNIT_PERIOD-(frameCount%UNIT_PERIOD); // 死亡
  }

  void draw() {
    if (!this.isArive && !this.wasArive)        return;
    if (frameCount%UNIT_PERIOD==0 && !wasArive) return;
    fill(GREEN, this.cellAlpha());
    rect(this.row*SIDE, this.column*SIDE, SIDE, SIDE);
  }


  // 2次元トーラスを再現
  int countAroundAriveCell() {
    int count = 0;

    int left   = (this.column+NUM-1)%NUM;
    int top    = (this.row   +NUM-1)%NUM;

    int center = this.column%NUM;
    int middle = this.row   %NUM;

    int right  = (this.column+1)%NUM;
    int bottom = (this.row   +1)%NUM;

    if (cells [top]    [left]   .wasArive) count++;
    if (cells [top]    [center] .wasArive) count++;
    if (cells [top]    [right]  .wasArive) count++;
    if (cells [middle] [left]   .wasArive) count++;
    if (cells [middle] [right]  .wasArive) count++;
    if (cells [bottom] [left]   .wasArive) count++;
    if (cells [bottom] [center] .wasArive) count++;
    if (cells [bottom] [right]  .wasArive) count++;
    return count;
  }

  boolean suggestNextGeneration() {
    int count = this.countAroundAriveCell();
    if (this.wasArive) return count==2||count==3;
    else              return count==3;
  }

  void shiftNextGeneration() {
    boolean willArive = this.suggestNextGeneration();
    this.wasArive = this.isArive;
    this.isArive  = willArive;
  }

}

次にメインのコードです。

final int   NUM         = 400;
final int   SIDE        = 2;
final int   UNIT_PERIOD = 30;
final color GREEN       = color(0, 255, 0);

Cell cells [/* row */] [/* column */] = new Cell [NUM][NUM];

void setup() {
  size(NUM*SIDE, NUM*SIDE);
  frameRate(10);
  noStroke();

  // 透明度の範囲を変更
  // 透明 0〜(UNIT_PERIOD-1) 不透明
  colorMode(RGB, 256, 256, 256, UNIT_PERIOD);

  for (int _row = 0; _row < NUM; _row++) {
    for (int _column = 0; _column < NUM; _column++) {
      cells [_row] [_column] = new Cell(_row, _column);
    }
  }
}

void draw() {
  background(0);

  if (frameCount%UNIT_PERIOD == 0) {
    for (int _row = 0; _row < NUM; _row++) {
      for (int _column = 0; _column < NUM; _column++) {
        cells [_row] [_column].shiftNextGeneration();
      }
    }
  }

  for (int _row = 0; _row < NUM; _row++) {
    for (int _column = 0; _column < NUM; _column++) {
      cells [_row] [_column].draw();
    }
  }

}

無関係の下位問題を抽出し、クラスに置き換えました。コードのコメントも工夫を凝らしてわかりやすくなるようにしてみました。

が!まだまだ問題が山積みです...。

  1. 無関係の下位問題を抽出しきれていない
  2. 一度に一つのことだけを行えていない
  3. 各関数などが完全に独立しきれていない
  4. (これは完全に僕の実力不足ですが、life gameが正常に動作しているかわかりません...)

と、このように経験が浅いためか上手にわかりやすいコードを書けませんでした。 やはり何度も意識的にコードを書いていくことが重要になるのでしょう。 日々精進して行きましょう!

『リーダブルコード』の第Ⅳ部

今回参考にしたのは『リーダブルコード』の第Ⅰ〜Ⅲ部でした。 しかし、この本には第Ⅳ部があり、そこではより具体的な例を挙げて実践的な内容に触れています。 また、付録にはオススメの本が載っていたりと、まだまだ紹介しきれていない部分がたくさんあります。 なにより僕にはこの本の内容を全て伝えきれるだけの説明力がありませんでした。 なので皆さんには実際にこの本を手にとって読んでいただきたいです。 内容としてはあまり難しいものではなく、特にプログラミング初心者の方や共同作業を目前に控えた方に読んでいただきたい一冊です。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

まとめ

実際に読みやすいコードを書こうとするのはとても大変で、頭をよく使う作業でした。 「読み手はここがわかりづらいんじゃないか」、「ここはこう書いたほうがいいんじゃないか」と試行錯誤しながらコードを改善していました。 まるで国語の文章をを書いているかのようでした笑。 しかしコードが読みやすくなったかと言われると、素直に頷けるものではありません。 今後も『より良いコード』を目指して読み手を意識しながらコードを書いていきます! 次回は総まとめとして、今までの内容をすべて振り返っていきます。

グループワークに向けて(3)

今回の目的

今回はクループワークに向けて(2)で扱った「リーダブルコード」の続きを勉強していきます。 今回はコードの内部、ロジカルなところを読みやすくするなどのことがメインです。第Ⅲ部まで進んでしまいましょう。

第Ⅱ部 『ループとロジックの単純化』

複雑なループ、巨大な式、膨大な変数を見ると、真剣に考えて記憶しなければいけないので、頭のなかに「精神的な荷物」が増えてしまう。 これは「理解しやすい」とは正反対のことだ。 コードに「精神的な荷物」がたくさんあると、バグは見つからなくなるし、コードは変更しにくくなるし、何よりコードに触れるのが楽しくなくなる。

(「リーダブルコード」第Ⅱ部より)

ここではループやロジックを簡単にし、読み手に理解してもらいやすいコードを書くにはどうすれば良いのかについて言及しています。

制御フローを読みやすくする

  • 条件式の左に変化する調査対象、右にはあまり変化しない比較対象を置くようにする
  • if/else文の書き方
    • 条件は否定型よりも肯定型を使う
    • 単純な条件を先に書く
    • 関心を引く条件や目立つ条件を先に書く
    • この優劣が衝突した場合は、自分で優先度をつける
  • コードを短くするよりも、読み手の理解する時間を短くする
    • 三項演算子はif文で書くよりもわかりやすい時のみ使う
      • ex) time_str += (hour >= 12) ? "pm" : "am"
    • 関数内では早めにreturnする( = ガード節)
    • do/while文やgoto文は使わない(コードが飛び飛びになってしまう)
  • ネスト(二重ループや二重if文など)を浅くする
    • コードを書き直すときはコードを新鮮な目で見る、一歩下がって全体を見る
    • returncontinueを使って早めに返す
実行の流れを追いづらくする構成要素 高レベルの流れが不明瞭になる理由
スレッド どのコードがいつ実行されるのかわからない
シグナル/割り込みハンドラ 他のコードが実行される可能性がある
例外 色々な関数呼び出しが終了する可能性がある
関数ポインタと無名関数 コンパイル時に判別できないので、
どのコードが実行されるかわからない
仮想メソッド object.virtualMethod()は未知の
サブクラスのコードを呼び出す可能性がある

巨大な式を分割する

  • 説明変数:式を表す変数を作り、何を指し示すのかを明確にする
  • 要約変数:条件式などを代入し、四季の意味を要約する
  • ド・モルガンの法則を使う
    • notを分配/括りだし、andorを反転させる
  • きれいに書けた(頭がいい)コードではなく、読みやすい(簡潔な)コードを書く
    • コードは長くなってもいいのでわかりやすく分割しよう
    • イディオムを知っておくと頭がいい簡潔なコードが書ける場合がある
  • コードを書くのが難しく、長くなってしまいそうな場合は逆転の発想をしてみる。
  • 巨大な文に含まれる同じ式を変数に置き換える
    • 見やすくなり、スペルミスがなくなる
    • 変更しやすくなる
  • よく似ているが、同じでないものはマクロや関数を組んで見やすくする

変数と読みやすさ

  • 不要な変数を削除する
    • 役に立たない変数、なくても理解に支障のない一時変数
    • 中間結果を保持するだけの変数
    • 制御フロー変数(ループを制御するフラグ)
      • breakを代わりに使おう
  • 変数のスコープ(有効範囲)を縮める
    • グローバル変数をローカル変数に「格下げ」する
    • メソッドをできるだけstaticにする
    • 大きなクラスを小さなクラスに分割する
    • 言語によっては条件式の中で変数を宣言できるのでそれを使う
      • ex) C++ : if(PaymentInfo* info = database.ReadPaymentInfo()){...}
      • ex) java : for(int i = 0; i < 10; i++){...}
    • JavaScriptpythonなどの変数宣言を必要としない言語では意図しないグローバル変数を生み出しやすい
      • タスクをできるだけ早く完了させる
      • 変数をグローバルにし、Noneなどを代入しておく
    • 変数の宣言を変数の使用直前に置く
  • 変数は一度だけ書き込む
    • constfinalなどを使おう

まとめ

プログラミングの技術的な部分に大きく関わる内容でした。 とても細かく、第Ⅰ部と違って強く意識しなければ身につかない内容なので、何度もこの本やこのページを振り返って確認しながらコードを書いていきましょう。

第Ⅲ部 『コードの再構成』

コードを理解しやすくするには、コードを書かないのがいちばんだ。

(「リーダブルコード」第Ⅲ部より)

第Ⅲ部にはコードを大きく変更する方法について書かれています。 コードを再構成したり完全に削除することで、さらに読みやすいコードにしていきましょう。

無関係の下位問題を抽出する

1. 関数やコードブロックを見て「このコードの高レベルの目標は何か?」と自問する
2. コードの各行に対して「高レベルの目標に直接効果があるか?あるいは無関係の下位問題を抽出しているか?」と自問する
3. 無関係の下位問題を解決しているコードが相当量あれば、それらを抽出して別の関数にする
  • 無関係の下位問題を積極的に見つけて抽出する(別の関数やクラスを作る)
    • コード量の多いループ
    • 将来的に再利用可能な部分
    • 完全に自己完結しているコード
    • 有用だが実装されていない関数
    • 無関係の下位問題を処理しているコード
  • コードを抽出しておくと、改善が楽になる
  • プロジェクトから切り離された汎用コードをたくさん作る
  • 完全に独立していなくても、取り除くだけで読みやすくなる
  • インターフェイスを直感的に扱いやすく変更する
    • プログラムの本質的なロジックを扱うコードを簡潔にする
    • 「理想とは程遠いインターフェイスに妥協することはない」
  • やりすぎてしまうと可読性が著しく下がってしまう

一度に1つのことを

  • 一度に1つのタスクをする
    • プロジェクトのタスクを列挙する(小さくても緩くてもいい)
    • タスクを出来るだけ異なる関数に分割する
    • 分割出来ない場合は少なくとも領域に分割する
  • 「タスクをどのように分割するか」より、「分割すること」自体が重要

詳しい例については本に記載されているのでそちらを読むと理解が深まります。

コードに思いを込める

おばあちゃんがわかるように説明できなければ、本当に理解したとは言えない。

(アルバート・アインシュタイン)

  1. コードの動作を簡単な言葉で同僚にもわかるように説明する
  2. その説明の中で使っているキーワードやフレーズに注目する
  3. その説明に合わせてコードを書く

プログラムのことを簡単な言葉で説明することで、コードをより自然なものに書き換えることが出来る。(ラバーダッキング) 問題や設計を上手く説明できないならば、何かを見落としているか、詳細が明確になっていないということだ。 プログラム(あるいは自分の考え)を言葉にすることで明確な形に成るのである。

(「リーダブルコード」第Ⅲ部 12章 まとめより)

短いコードを書く

  • 不要な機能は実装しない
    • プログラマはプロジェクトに欠かせない機能を過剰に見積もりすぎ
  • 機能に必要な精度は、利用先によって大きく変化する
    • すべてのプログラムが高速で、100%正確で、あらゆる入力を上手く処理する必要はない
  • コードを出来るだけ小さく軽量に維持する
    • 汎用的で有用なコードを作り、重複コードを削除する
    • 未使用のコードや無能なコードを削除する
    • プロジェクトをサブプロジェクトに分割する
    • コードの「重量」を意識する(軽微で機敏にしておく)
  • 身近なライブラリに親しむ
    • APIを知っていれば容易に解決してしまうことがある

まとめ

長く単調になってしまいましたが、これが第Ⅱ部と第Ⅲ部についての内容の大まかなものです。 とても重要なことが多く、本書を実際に読んで理解を深めていただきたいです。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

次回は今回の内容を実践し、第Ⅳ部の内容についてお話します。

グループワークに向けて(2)

今回の目的

今回はグループワークに向けて(0)で上げた問題点のうち、問題2の方についてです。 コードの可読性はグループワークの作業効率に大きく影響すると思います。(あくまで予想ですが)

なので「どのようにコードを書けばわかりやすいのか」を考えていきましょう。

リーダブルコード

リーダブルコードという本を読むことにしました。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

この本のサブタイトルは『より良いコードを書くためのシンプルで実践的なテクニック』となっています。 ここで言う『良いコード』とは、端的に言ってしまえば読みやすいコードということでしょう。 今回は第一部の『表面上の改善』について、実際にコードを見直して学習していきましょう。

第Ⅰ部 『表面上の改善』

このテーマはすごく大切だ。コードのすべての行に影響するからね。 個々の変更は小さいかもしれないけど、それらを合わせれば、コードに大きな改善をもたらせる。

(「リーダブルコード」第Ⅰ部より)

ここでは出来るだけ短い時間でコードを読めるようにするにはどのようにコードを書けば良いのかについて言及しています。

  • 変数や関数の名前に情報を埋め込む
  • 紛らわしい名前を避ける
    • ex) filter 選択するのか除去するのかわからない
  • 一貫性のある美しいコードを書く
    • 似たような部分を似せる。=,などの位置を揃える。
  • 簡潔で価値のあるコメントを書く
    • コメントを読むために消費する時間に見合うだけのコメントを書こう
    • 関数の引数などをわかりやすく示すのもいい。

大まかに上げればこのような感じです。読んでみると「えっ!?このコードってだめなの!?」と思うことも多々ありました... とても面白い本なので、是非読んでみてください!

実践してみよう

せっかく勉強したので実際にコードを改善してみましょう。

static final int vert = 7;
static final float r = 180;
static final int max_level = 2;
static final float k = 2 / (1 + sqrt(5));

void setup() {
  blendMode(ADD);
  stroke(0, 0, 255);
  smooth();
  noFill();
  size(displayWidth, displayHeight);
  background(0);
  float x = 0;
  float y = 0;
  translate(width/2, height/2);
  rotate(radians(270/vert));
  fractalloop(r, x, y, 0);
  save("fractal.png");
}

void drawShape(float _r, float _x, float _y) {
  beginShape();
  for (int i = 0; i < vert; i++) {
    float x = cos(radians(i * 360 / vert)) * _r + _x;
    float y = sin(radians(i * 360 / vert)) * _r + _y;
    vertex(x, y);
  }
  endShape(CLOSE);
}

void fractalloop(float _r, float _x, float _y, int level) {
  if (level > max_level) {
    return;
  }
  stroke(level * 10, 100 + level * 4, 100 + level * 50, 100 - level * 25);
  for (int i = 0; i < vert; i++) {
    float x = cos(radians(i * 360 / vert)) * _r + _x;
    float y = sin(radians(i * 360 / vert)) * _r + _y;
    drawShape(_r, x, y);

    float r = _r * k;
    fractalloop(r, x, y, level+1);
  }
}

これは僕が実際に書いたフラクタル図形を描くprocessingのプログラムコードです。 実行結果はこんな感じ。

f:id:fms_eraser:20160930000503p:plain

フラクタル図形って綺麗でいいですよね。では、コードを見直していきましょう。

定数

最初は定数の宣言です。vertとかrとかありますが、なんの為の定数なのかが明確ではありません。 また、変数との見分けもつきづらいですね。 rという変数はradiusから来ていると思いますが、直径を代入してしまう人も少なくないでしょう。 その点に注意して書き換えましょう。

// MAX_LEVELはループ回数の上限
// SCALEは辺の縮小率、黄金比を代入
static final int   VERTEX    = 7;
static final int   MAX_LEVEL = 2;
static final float RADIUS    = 180;
static final float SCALE     = 2 / (1 + sqrt(5));

定数はすべて大文字表記に変更しました。 rではなくRADIUSと全文表記することで、半径であることをわかりやすくしています。 SCALEに関しては、どうしてもわかりやすい名前が思いつかなかったので、コメントで補足をしています。

関数

次は関数です。関数は2つありますが、drawShapefractalloopで、なんの関数かわかりづらいですね。 コメントも一切ないので、何をしているのかが全くわかりません。

// 正(VERTEX)角形を書く
void drawPolygon(float _centerX, float _centerY, float _radius) {
  beginShape();

  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    vertex(topX, topY);
  }

  endShape(CLOSE);
}

// フラクタルの描画を制御
void drawFractal(float _centerX, float _centerY, float _radius, int _level) {
  // 再帰ループを脱出
  if (_level > MAX_LEVEL) {
    return;
  }

  stroke(_level*10, 100+_level*4, 100+_level*50, 100-_level*25);

  // 各頂点を求めなおしている -> 改善の余地あり
  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    drawPolygon(topX, topY, _radius);
    float nextRadius = _radius * SCALE;

    // 再帰ループ
    drawFractal(topX, topY, nextRadius, _level+1);
  }
}

書き直した結果がこちらです。関数の引数には規則として_をつけました。 関数の名前は簡潔で正確なものに変え、変数の情報量も増やし、ひと目で分かるように変更しました。 コメントで改善できそうなところを見つけて示しておくと、グループワークでコードを見直したときの変更すべき点がわかりやすいと思います。 for文のiは使用するスコープが小さく、補足するほど重要な変数ではないと考えてそのままにしておきました。

setup()関数

最後はメインのsetup()です。

void setup() {
  size(displayWidth, displayHeight);
  background(0);

  // 色加算合成モードに変更
  blendMode(ADD);
  smooth();
  noFill();
  
  translate(width/2, height/2);
  rotate(radians(270/VERTEX));
  drawFractal(0, 0, RADIUS, 0);

  save("fractal.png");
}

大きな変更点はないですが、それぞれの行をブロックに分けた事によって理解しやすくしています。 また、不要な変数を削除してより見やすくしました。 みんなが知らないような関数があればコメントで何をしているのか書いてあげると丁寧だと思います。

まとめ

すべてをまとめてみましょう。

// MAX_LEVELはループ回数の上限
// SCALEは辺の縮小率、黄金比を代入
static final int   VERTEX    = 7;
static final int   MAX_LEVEL = 2;
static final float RADIUS    = 180;
static final float SCALE     = 2 / (1 + sqrt(5));

void setup() {
  size(displayWidth, displayHeight);
  background(0);

  // 色加算合成モードに変更
  blendMode(ADD);
  smooth();
  noFill();
  
  translate(width/2, height/2);
  rotate(radians(270/VERTEX));
  drawFractal(0, 0, RADIUS, 0);

  save("fractal.png");
}

// 正(VERTEX)角形を書く
void drawPolygon(float _centerX, float _centerY, float _radius) {
  beginShape();

  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    vertex(topX, topY);
  }

  endShape(CLOSE);
}

// フラクタルの描画を制御
void drawFractal(float _centerX, float _centerY, float _radius, int _level) {
  // 再帰ループを脱出
  if (_level > MAX_LEVEL) {
    return;
  }

  stroke(_level*10, 100+_level*4, 100+_level*50, 100-_level*25);

  // 各頂点を求めなおしている -> 改善の余地あり
  for (int i = 0; i < VERTEX; i++) {
    float topX = cos(radians(i * 360 / VERTEX)) * _radius + _centerX;
    float topY = sin(radians(i * 360 / VERTEX)) * _radius + _centerY;
    drawPolygon(topX, topY, _radius);
    float nextRadius = _radius * SCALE;
    // 再帰ループ
    drawFractal(topX, topY, nextRadius, _level+1);
  }
}

結果的に行数は多くなってしまいましたが、読みやすさは上がっていると思います。 これからは『良いコード』を意識してプログラミングをしていきましょう。 次回もリーダブルコードの続きを勉強していきましょう。

グループワークに向けて(1)

今回の目的

今回はグループワークに向けて(0)で上げた問題点のうち、問題1の方について考えていこうと思います。

コード共有しよう

グループで作業するということは、1つのプロジェクトを複数人で同時に作り上げるということです。 しかし、みんなで集まって1つのパソコンに向かってコードを書きながら議論しているのでは効率が悪すぎます。 つまり基本的には個人の作業でプログラムを組み、それをタイミングを見て1つのプログラムにまとめます。 プログラムはネットワークを通じて共有すれば作業が楽になります。

Git Hubって何?

「そもそもGit Hubってなんだ?」という疑問があがると思います。 わかりやすく言うと、Gitがプログラムを段階ごとに保存してくれるシステムで、Git Hubがそれを使ってソフトウェアを開発できるサービスです。 つまり、Git Hubを使えばソフトウェアを開発しやすくなるってことです。 初めはその程度の理解で大丈夫です。 正直僕もあまり詳しいことがわかってないです笑。

Git Hubを使ってみよう

Git HubはGitを使ったことがなくても利用できます。

僕の先輩の@tkw_fmsが作ったスライドにわかりやすくまとまっています。

これを見ればGit Hubの基本的な使い方はわかるはずです。

グループ作業を始めよう

GitHub Desktopからチームを作る方法が見つからなかったので公式のwebから作ります。

  1. まずGitHubにloginする。

  2. 右上の+マークをクリックする。

  3. New organizationをクリックする。

  4. アカウントを持っていない人はここで作成する。

  5. チームの情報を入力する。

    • Organization name チームの名前

    • Billing email 登録するメール(このメールにチームの情報が届く)

    • Plan 無料がいいなら上を選びましょう。

  6. Create organizationをクリックする。

  7. チームメンバーのIDを入力して招待する。

これで完成です。

まとめ

僕もグループ作業での経験が浅いですが、GitHubを使えばよかったという経験はあります。 いちいちUSBでデータを渡したり、ファイルの名前を変更してバージョン管理をするのはスマートではありません。 今後はGitやGitHubを利用してグループワークを効率的に行って行きましょう!

明治大学で開かれるABProを見に行った話

 9/24(土)に明治大学ABProという発表会が開かれました。 アブノーマルなプログラミング発表会ということでABProとなっていますが、僕はこの発表会を初めて見に行くことができました。

みんなが何言ってるのかわからない

 僕の知識が浅いせいで、出てくる単語が全くわかりませんでした。 何してるのかわからない発表も多数...

もっと勉強して理解できるようになりたいと強く感じました。

発表から一つだけ紹介

記事を書くときに許可とか必要なのかな、ということを全く考えていなかったので、急遽一人にだけ許可を得て記事を書かせて貰うことに。

これは午前中に発表した先輩のりゅうふじわらさんのツイートです。 先輩は犬の世界の住人で、この前散歩中に向こうの世界の犬と仲良くなったそうです。

こちらがそのわんちゃんのtwitterアカウントです笑。 逆ポーランド記法で数式を渡すと犬語で答えを返してくれるということで、リーマンゼータ関数も答えてくれる優秀な犬(?)でした笑。

飛び入り参加

 この発表会は午前と午後に分かれていて、午後は更に前半と後半に分かれて、それぞれが終わった後、休憩などの前に飛び入りタイムがあります。 その場で実装して参加したり、過去のプログラムを発表したりとやりたい放題な時間でした。 もちろん見に行くだけで参加するつもりはなかったのですが、その場でコードを作って飛び入り参加させて頂きました! 僕は最近terminalをいじって遊んでいるのですが、catコマンドはあるのにdogコマンドがないことに違和感を覚えていました。(注意:catはcatenateの略なので猫とはなんの関係もないです)

なら実装してしまおう!ということで、超スピードで作業に取り掛かりました。 dogコマンドと言ったら、犬語翻訳しか思いつかなかったのでそれに決定しました。 早速解析です。わんちゃんのアカウントにリプを送りまくってデータ収集をした結果、すぐにわかりました。

数字 犬語
0 クゥゥ
1 ワン!
2 クーン
3 バウ!!
4 キャン!
5 ワオーン
6 グゥゥ
7 ガウ!
8 ハッハッ
9 クー
- ガルル
. ...
/ ワォーーー!
^ キュン
π バウワウ

こんな感じ。数字を犬語に置き換えてる簡単なものでした。 それらを置き換えるプログラムをjavaで組んでコンパイルしてterminalから実行できる状態にしたら、

alias dog='java -classpath ~/path dog_emotion'

をして設定完了。

発表では皆さんに笑っていただいて、とても満足しました。

僕の戦いはまだ終わっていなかった!

こんなツイートを発見してしまいました。まだまだ解析しきれていないようです。 試行錯誤の結果...無限大ってどうなってるんだ?という結論に。 調べてみたところ...

かわいいわんちゃんが現れました。∞はUo・ェ・oUのようです。

まだある模様...(僕は必死で考えましたが思いつきませんでした)

まとめ

面白い人達の集まりでした。どうやっているのか、なぜその発想に至ったのかがわからないものが多かったです笑。 皆さん技術力が高いので来年に向けてしっかりお勉強していきます! 来年は一般で出たいです。

追記

あの後、更に試行錯誤を重ねて2つほど、犬語を発見しました!

bow-wow = E
▽・エ・▽ = -∞

といった感じでした。 Eのほうはりゅうふじわらさんのデバッグからのおこぼれでした笑

-∞は ガルルUo・ェ・oU だと思っていたので、発見までに時間がかかってしまいました...

そしてついに...

犬語解析完了!!!

やったぜ!めちゃくちゃ大変だった!でも楽しめたのでりゅうふじわらさんに感謝m( )m やっと僕のABPro2016が幕を閉じたのだ...

グループワークに向けて(0)

 僕の通っている大学で、秋学期からグループでのプログラミングが始まります。 僕は夏休み中の合宿で友達と3人でグループワークをしたのですが、そのときに気づいた2点ありました。

問題1 コードの共有

「Gitを使えばいいじゃん」

そんな簡単に使えたら困らないですね。 3人ともグループワーク初心者だったのもあり、コードのバージョン管理がグチャグチャでした。 fileの名前にmergedとか2とかつけるのはcoolじゃないですね!

問題2 可読性の低さ

コメントを入れてなかったり、変数や関数の名前がわかりにくいことが多かったです。 「ここって何したいの?」「これなんの関数?」みたいな会話がありました。

今後に向けて

お勉強しましょう。GitHubとかお勉強すればわかるのかな? あとは読みやすいコードとはどんなものなのかを見ていこうと思います。