プログラミング文体練習
元ネタはレーモン・クノー『文体練習』
スタイル(文体)=制約を決めて同じプログラムを書いてみよう
確かにプログラミングスタイルに関する説明ってあまりないかも、ここではこういうものだからただ従え、みたいな
計算課題:term frequency (TF: テキスト内の単語の登場頻度)の計算(まさかここでこの用語を聞くとは)
別に複数のテキストがあるわけじゃないので、IDF は計算しなくていい
文芸表現と違って、スタイルには優劣がある
序章にプログラムの仕様がある
- 所感
- 第一部 歴史的スタイル
- 第二部 基本スタイル
- 第三部 関数合成
- 第四部:オブジェクトの相互作用
- 第五部:リフレクションとメタプログラミング
- 第六部:異常事態
- 第七部:データ中心
- 第八部:並行性
- 第九部:対話性
- 第十部:ニューラルネットワーク
所感
これ歴史本だ
アセンブリからニューラルネットワークまで、それぞれのプログラミング技術がどういった背景で生まれているのかを解説してる
第一部 歴史的スタイル
1 章:アセンブリ言語
識別子(変数・タグ付きアドレス)使用不可、データはリストを作ってインデックスでアクセスする
メモリ=一度に読み込めるデータ量に制限がある(ここでは 1024 セル)
2 章:スタックマシン
Forth というプログラミング言語が元ネタらしい
スタックとヒープを定義して、その後に手続き(ワード)を定義する
処理対象データはすべてスタックからポップして処理してからスタックにプッシュしなおす
3 章:ベクトル演算
すべてのデータは固定長の配列に格納される
配列に対して反復処理をしてはいけない
NumPy を使う
演算対象の中にこういう値があったらそれをこういう風に整形して、みたいなのを繰り返す感じか
第二部 基本スタイル
4 章:モノリス
最初から最後まで一ブロックで記述する
名前付き抽象化(関数に分けるとか?)をしない
ライブラリも使わない(サンプルコードでは sys
と string
のみ)
今時はコードを細かく分割するのが良いとされてるけど、それをあえてしないスタイル
処理フローを有向グラフで書いたとき、エッジ(矢印)の数を E
, ノードの数を N
, 出口ノード(エッジが出てないノード、このノード数は N
にも含まれる)の数を P とすると、
循環的複雑度 (Cyclomatic complexity) CC
は CC = E - N + 2P
の式で求められるらしい
Flesch Reading Ease テスト、Flesch-Kincaid grade level という、ともに英文の読みやすさの指標があるらしい
日本語にもあるのかな?主語と述語の距離とかで測ってそう
5 章:クックブック
手続き型で問題を分割する
状態をグローバル変数に保存する
自分が Ruby とか Python とか書いてるときはターミナルに打ち込んで実行したのをそのままファイルにすることがあるので、最終的にこれに近いスタイルになることが多い
状態が保存されていて、処理にその状態を使うので手続きが冪等(べきとう)でない(呼び出したときに返ってくる値が一定でない)ことがある
6 章:パイプライン
関数型だ!
クックブックと違い関数間で状態が共有されない=冪等
コードの構造もほぼ同じかな?
一つの関数につき一つの引数で、複数の引数を持たせるときはカリー化を使う……Haskell で聞いた言葉だ
同じ入力値に対する出力が常に同じなので単体テストがやりやすい
7 章:コードゴルフ
少ない行数で実装する
キーストローク数=文字数を少なくするという意味もある
SLOC (Source Lines Of Code)が最小限になる
あまり短く書くことにこだわると読みにくくなる(変数名を 1 文字に統一するとか)
第三部 関数合成
数学的な考え方かも
8 章:再帰
数学的帰納法を用いてモデル化する
リストの先頭に対して処理し、リストの残りに対して自分自身を呼び出す
9 章:継続渡し
すべての関数が引数として他の関数を受け取り、最後にその関数を呼び出す
JavaScript の Ajax 呼び出しが成功時、失敗時にそれぞれ実行される関数を引数として求めてくるけど、そういうやつ?
このスタイルの起源は goto
らしい、つまり取扱注意か
10 章:モナド
関数を呼び出すメソッドを持ったクラスを定義する
Haskell 由来
第四部:オブジェクトの相互作用
11 章:オブジェクト
問題をモノ (things) に分解する
データには直接アクセスしない
概念を拡張したりできる(乗り物→自動車)
12 章:レターボックス
オブジェクトはメッセージを受け取るメソッド一つを公開する
14 章:抽象データ型
Java でいう interface
走る、とか、クラクションを鳴らす、とかができるものをまず定義して、その後具体的に車を作っていく、みたいな
16 章:pub/sub
画像処理のパフォーマンスを上げるために、処理済みフレームをバックグラウンドでためておいて、ためたものをフロントエンドで表示するようなアプローチを試みたことがある
(確かうまくいかなかった)
第五部:リフレクションとメタプログラミング
プログラムが自分自身にアクセスすること
メタフィクションのメタか
18 章:リフレクション
文字列として関数を定義して、実行時にそれを読み込む
下手するとインジェクションできそうで怖いな
20 章:プラグイン
モジュール作って import
, よくやるやつだな
第六部:異常事態
いわゆる例外処理
ここは実用的な話かも
21 章:防御的プログラミング
引数が妥当でないときには戻り値として妥当なものを返すか引数を上書きする
デフォルト的なものを用意して、無効な値が渡されたらデフォルトに切り替えて最後まで実行する
値を明示的に与えてもいいし、何も渡さなくてもいいって感じ?
当然ユーザーが渡したものとは違うデータで実行されるので、実行結果はユーザーの意図しないものになる
22 章:癇癪持ち
引数が妥当でないときはその場でエラーを吐いてプログラムを停止する
エラー処理が関数ごとに存在することになる(「例外を撒き散らす」と表現されている)
何かしらのデフォルト値を持ってるときは前章のアプローチでいいだろうけど、
アプリケーション全体の挙動は経験上こっちのほうがいい気がしている
C 言語には Exception がないので、(ほかの言語でいう throw Exception
的な感覚で?)goto
を使うことがあるらしい
こんな感じ?(参考):
int main(void){
if (except) {
goto PROCESS_EXCEPTION;
} else if(anotherExcept) {
goto PROCESS_ANOTHER_EXCEPTION;
} else {
return 0;
}
PROCESS_EXCEPTION:
return 1;
PROCESS_ANOTHER_EXCEPTION:
return 2;
}
23 章:例外
エラー処理を一か所にまとめる
メインブロックで try ... except
して、他の関数ではエラーがあったときに例外を throw する
癇癪持ちスタイルより可読性がよさそう
24 章:型注釈
手続き・関数は期待する引数の型を宣言する
本では @AcceptTypes
というデコレーターを使ってるけど、以下のようにも書ける(型ヒント):
# @AcceptTypes と違って強制力がない、エディタで警告が出るくらい
def calc_double(num: int) -> int:
return num * 2
FORTRAN の設計者が整数と浮動小数点数の区別を変数名の最初の文字で行うようにした(いわゆるシステムハンガリアン)のが始まりらしい
(参考)
25 章:純粋関数と不純関数
関数は副作用を持たない=IO(ファイルの入出力など)が行えない
第七部:データ中心
「したいこと」ではなく「今あるもの」から考えるアプローチ
26 章:データベース
データをモデル化して保存する=構造化が目的
データはいろいろなプログラムから呼び出される
データベースがあって、そこに問い合わせることで処理を行う
27 章:スプレッドシート
データの列と数式でモデル化する
28 章:ジェネレータ
関数でデータを変換する
第八部:並行性
29 章:アクター
すべてのモノが独自のスレッドで実行される
30 章:データ空間
データ集約的=作業をどこから分割しても同じ方法でできるような並列処理に適している
31 章:マップリデュース
複数のワーカー関数の出力を一か所にまとめて出力する
マップリデュース自体は並列化できず、直列に実行する必要がある
32 章:二重マップリデュース
マップリデュースを並列化するもの
Hadoop で用いられる仕組みらしい
データセンターでブロック単位でサーバーにデータを送るために使われることがある
第九部:対話性
33 章:MVCTrinity
- Model: データ
- View: データの表現
- 画面表示など
- Controller: 入力制御、モデルの更新、ビューの呼び出し
GUI, Web フレームワークなどで使われる
34 章:RESTful
REpresentational State Transfer
サーバーは状態を持たない
第十部:ニューラルネットワーク
概念は昔からあった(計算量が多すぎて当時現実的じゃなかったんだっけ)
- あらゆる種類のデータを数値に変換して処理する
- 数値を入力として受け取って数値を出力する純粋関数のみ
- 関数はニューラルネットワークで、入力と重みをもつ
- 関数は微分可能
ここからは TensorFlow と Keras を使う
35 章:ニューラルネットワーク
- 重みをハードコードした 1 層の Dense ネットワーク
36 章:学習する Dense 層
- 重みを学習できるようにする
37 章:多層ネットワーク
- 一つ以上の隠れ層があって、入力も出力もそこを経由する
- この隠れ層が多いニューラルネットワークの学習処理をディープラーニングという
39 章:畳み込み
IME の予測変換で出る程度には重要な概念、畳み込みニューラルネットワーク
機械学習を触ってた時に聞いたことがある単語だな