#ライセンスとクレジット
クレジット
https://missing.csail.mit.edu/2020/metaprogramming/
ライセンス
https://creativecommons.org/licenses/by-nc-sa/4.0/
講義動画
https://www.youtube.com/watch?v=e8BO_dYxk5c
Todo
#メタプログラミング
コードを書いたり,より効率的な仕事をしたりすることよりも,プロセスに重きを置いた一連のことにもっとも適した言葉として「メタプログラミング」という言葉を使った.
この講義では,コードを構築してテストするためのシステムや依存関係を管理するためのシステムを見ていく.
プログラム上で使用されるメタプログラムとは異なるものである.
#システム構築
LaTeXで論文を書く場合、その論文を作成するためにはどのようなコマンドが必要でしょうか。
ベンチマークを実行して、それをプロットし、そのプロットを論文に挿入するためのコマンドはどうでしょうか?
あるいは、受講しているクラスで提供されているコードをコンパイルして、テストを実行するためのコマンドは?
コードが含まれているかどうかに関わらず、ほとんどのプロジェクトには「ビルドプロセス」が存在します。
インプットからアウトプットに至るまでに必要な一連の操作です。
多くの場合、そのプロセスには多くのステップと多くの分岐があります。
このプロットを生成するためにこれを実行し、これらの結果を生成するためにあれを実行し、最終的な論文を作成するために他のものを実行する。
このクラスで見てきた多くのことと同様に、このような問題に遭遇するのはあなたが初めてではありませんし、幸いにもあなたを助ける多くのツールが存在します。
これらは通常「ビルドシステム」と呼ばれ、たくさんの種類があります。
どのシステムを使うかは、作業内容や使用する言語、プロジェクトの規模などによって異なります。
しかし、基本的にはどれもよく似ています。いくつかの依存関係、いくつかのターゲット、そしてそれらを行き来するためのルールを定義します。
ビルドシステムは、特定のターゲットを要求すると、そのターゲットのすべての推移的な依存関係を見つけ、最終ターゲットが生成されるまでの間、中間ターゲットを生成するためのルールを適用します。
理想的には、ビルドシステムは、依存関係が変化しておらず、以前のビルドで結果が得られているターゲットに対して、不必要にルールを実行することなく、これを実行します。
makeは最も一般的なビルドシステムの1つであり、UNIXベースのコンピュータのほとんどにインストールされています。
欠点もありますが、シンプルで中程度のプロジェクトには非常に適しています。
makeを実行すると、カレントディレクトリにあるMakefileというファイルを参照します。
すべてのターゲット、その依存関係、ルールはこのファイルで定義されます。
その一つを見てみましょう。
paper.pdf: paper.tex plot-data.png
pdflatex paper.tex
plot-%.png: %.dat plot.py
./plot.py -i $*.dat -o $@
このファイルの各ディレクティブは、右手側を使って左手側を生成する方法のルールです。
言い方を変えれば、右辺に名前が付いているものが依存関係で、左辺がターゲットです。
インデントされたブロックは、それらの依存関係からターゲットを生成するための一連のプログラムです。
make
では、最初の行がデフォルトのゴールも定義します。
引数を取らずにmake
を実行すると、これがビルドされるターゲットになります。
あるいは、make plot-data.png
のように実行すると、代わりにそのターゲットが構築されます。
ルール内の%
は「パターン」であり、左と右の同じ文字列にマッチします。
例えば、plot-foo.png
というターゲットが要求された場合、make
は依存関係にあるfoo.dat
とplot.py
を探します。
では、ソースディレクトリが空の状態でmake
を実行するとどうなるか見てみましょう。
$ make
make: *** No rule to make target 'paper.tex', needed by 'paper.pdf'. Stop.
make
は、paper.pdf
を作るためにはpaper.tex
が必要だと親切に教えてくれていますが、そのファイルを作る方法を教えてくれるルールはありません。
では、作ってみましょう。
$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'. Stop.
興味深いことに、plot-data.png
を作成するルールがありますが、これはパターンルールです。
ソースファイルが存在しない(data.dat
)ので、make
は単にそのファイルを作れないと言っているだけです。
それでは、すべてのファイルを作ってみましょう。
$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()
data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8
さて,makeを実行するとどうなるでしょか?
$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
... lots of output ...
見てください、私たちのためにPDFを作ってくれました。
また作ったらどうなるでしょうか?
$ make
make: 'paper.pdf' is up to date.
何もしていないのに!?
なぜだ?それは、その必要がなかったからです。
以前にビルドされたすべてのターゲットが、リストされた依存関係に関して最新の状態であるかどうかをチェックしました。これをテストするには、paper.texを修正してからmakeを再実行します。
$ vim paper.tex
$ make
pdflatex paper.tex
...
makeはplot.pyを再実行しなかったことに注意してください。
なぜなら、それは必要ではなかったからです。
#依存関係の管理
よりマクロなレベルでは、ソフトウェアプロジェクトには、プロジェクト自体が依存関係にあることが多いでしょう。
インストールされたプログラム(python
など)、システムパッケージ(openssl
など)、プログラミング言語のライブラリ(matplotlib
など)に依存しているかもしれません。
最近では、ほとんどの依存関係は、多数の依存関係を一箇所に集めて、インストールするための便利な仕組みを提供するリポジトリを通じて入手できるようになっています。
例えば、Ubuntuのシステムパッケージを集めたUbuntu package repositoriesはapt
ツールでアクセスできますし、Ruby
のライブラリを集めたRubyGems
、Python
のライブラリを集めたPyPi
、Arch Linux
のユーザー提供のパッケージを集めたArch User Repository
などがあります。
これらのリポジトリと対話するための正確なメカニズムは、リポジトリやツールによって大きく異なるため、この講義では特定のリポジトリの詳細にはあまり触れません。
ただ、共通して使われている用語はいくつか紹介します。
まず、バージョン管理です。
ほとんどのプロジェクトは、他のプロジェクトが依存している場合、リリースごとにバージョン番号を発行します。
通常は8.1.3や64.1.20192004のようなものです。
バージョン番号は数字であることが多いですが、必ずしもそうではありません。
バージョン番号には様々な役割がありますが、その中でも最も重要なものの一つは、ソフトウェアが継続して動作することを保証することです。
例えば、私が自分のライブラリの新バージョンをリリースした際に、特定の関数の名前を変更したとします。
私がそのアップデートをリリースした後、誰かが私のライブラリに依存するソフトウェアをビルドしようとした場合、そのビルドはもはや存在しない関数を呼び出しているために失敗するかもしれません。
バージョン管理は、あるプロジェクトが他のプロジェクトの特定のバージョンやバージョン範囲に依存していることを示すことで、この問題を解決しようとするものです。
これにより、基盤となるライブラリが変更されても、依存するソフトウェアは私のライブラリの古いバージョンを使用してビルドを継続することができます。
しかし、これも理想的ではありません。私のライブラリのパブリックインターフェース(「API」)を変更しないセキュリティアップデートを発行し、古いバージョンに依存していたプロジェクトがすぐに使い始められるようにしたらどうでしょう?ここで、バージョンに含まれるさまざまな数字のグループが登場します。それぞれの数字の正確な意味はプロジェクトによって異なりますが、比較的一般的な基準としてセマンティック・バージョニングがあります。セマンティック・バージョニングでは、すべてのバージョン番号は、major.minor.patchという形式になっています。ルールはこうです。
しかし、これも理想的ではありません。
私のライブラリのパブリックインターフェース(「API」)を変更しないセキュリティアップデートを発行し、古いバージョンに依存していたプロジェクトがすぐに使い始められるようにしたらどうでしょう?
ここで、バージョンに含まれるさまざまな数字のグループが登場します。
それぞれの数字の正確な意味はプロジェクトによって異なりますが、比較的一般的な基準としてセマンティック・バージョニングがあります。
セマンティック・バージョニング( https://semver.org/ )では、すべてのバージョン番号は、major.minor.patch
という形式になっています。
ルールはこうです。
- 新しいリリースでAPIを変更しない場合は、パッチバージョンを増やす。
- 下位互換性のある方法でAPIを追加した場合は、マイナーバージョンを増やします。
- 下位互換性のない方法でAPIを変更した場合は、メジャーバージョンを増やす。
これはすでにいくつかの大きな利点を提供しています。
さて、私のプロジェクトがあなたのプロジェクトに依存している場合、マイナーバージョンが少なくとも当時のものである限り、私が開発したときに構築したものと同じメジャーバージョンの最新リリースを使用しても安全であるはずです。
言い換えれば、バージョン1.3.7
のライブラリに依存している場合、1.3.8
や1.6.1
、あるいは1.3.0
でビルドしても問題ないということです。
バージョン2.2.4
は、メジャーバージョンが上がったので、おそらく大丈夫ではないでしょう。
セマンティックバージョニングの例をPythonのバージョン番号で見ることができます。
多くの人は、Python 2とPython 3のコードがあまりうまく混ざらないことを知っていると思いますが、それがメジャーバージョンのアップになった理由です。
同様に、Python 3.5用に書かれたコードは、Python 3.7ではうまく動くかもしれませんが、3.4では無理かもしれません。
依存関係管理システムを使っていると、ロックファイルという概念に出会うこともあります。
ロックファイルとは、簡単に言えば、現在依存している各依存関係の正確なバージョンをリストアップしたファイルのことです。
通常、依存関係の新しいバージョンにアップグレードするには、明示的にアップデートプログラムを実行する必要があります。
不必要な再コンパイルを避けるため、再現性のあるビルドを行うため、あるいは(壊れているかもしれない)最新バージョンへの自動アップデートを行わないためなど、さまざまな理由があります。
この種の依存関係のロックの極端なバージョンは、自分のプロジェクトに依存関係のすべてのコードをコピーするvendoringです。
これは、依存関係にあるすべてのコードを自分のプロジェクトにコピーするというものです。
これにより、依存関係にあるすべてのコードに対する変更を完全にコントロールすることができ、独自の変更を導入することができますが、同時に、上流のメンテナからの更新を明示的に取り込まなければなりません。
#継続的インテグレーションシステム
より大きなプロジェクトに取り組んでいると、変更を加えるたびに追加の作業が必要になることがよくあります。
ドキュメントの新しいバージョンをアップロードしたり、コンパイル済みのバージョンをどこかにアップロードしたり、コードを pypi にリリースしたり、テストスイートを実行したり、その他いろいろなことをしなければならないでしょう。
誰かがGitHubでプルリクエストを送ってくるたびに、そのコードのスタイルチェックをしてほしいとか、ベンチマークを実行してほしいとか。
このようなニーズが出てきたときは、継続的インテグレーションを検討してみましょう。
継続的インテグレーション(CI)とは、「コードが変更されたときに実行されるもの」の総称で、世の中にはさまざまなタイプのCIを提供する企業があり、オープンソース・プロジェクトではしばしば無料で提供されています。
大きなものでは、Travis CI、Azure Pipelines
、GitHub Actions
などがあります。
どれも仕組みはほぼ同じで、リポジトリに様々なことが起こったときに何をすべきかを記述したファイルをリポジトリに追加します。
もっとも一般的なのは、「誰かがコードをプッシュしたら、テストスイートを実行する」といったルールです。
イベントが発生すると、CIプロバイダは仮想マシン(またはそれ以上)を起動し、「レシピ」のコマンドを実行し、通常は結果をどこかにメモします。
テストスイートが通らなくなったら通知されるように設定したり、テストが通っている間はリポジトリに小さなバッジが表示されるように設定したりするかもしれません。
CIシステムの一例として、クラスのウェブサイトはGitHub Pagesを使って設定しています。
Pagesは、masterにプッシュするたびにJekyllブログソフトウェアを実行し、構築されたサイトを特定のGitHubドメインで利用できるようにするCIアクションです。
これにより、ウェブサイトの更新が簡単にできるようになりました。
私たちは、ローカルで変更を加え、それをgitでコミットしてからプッシュするだけです。
あとはCIが面倒を見てくれます。
#テストについての余談
大規模なソフトウェアプロジェクトには、たいてい「テストスイート」が付属しています。テストの一般的な概念についてはすでにご存知かもしれませんが、一般的に遭遇する可能性のあるテストのアプローチやテスト用語について簡単に触れておこうと思います。
- テストスイート:すべてのテストを総称したもの
- ユニットテスト: 特定の機能を単独でテストする「マイクロテスト」。
- 統合(インテグレーション)テスト: システムの大部分を実行し、異なる機能やコンポーネントが一緒に動作することを確認する「マクロテスト」。
- 回帰テスト:以前にバグを引き起こした特定のパターンを実装し、バグが再発しないことを確認するテスト。
- モック:無関係な機能のテストを避けるために、関数やモジュール、型を偽の実装で置き換えること。例えば、「ネットワークのモック」や「ディスクのモック」などがある。