メタプログラミング
「メタプログラミング」とは何のことでしょうか? これはコードを書くことやより効率よく仕事をするといったことよりも、 むしろそれらの 手順 のことを意味するために我々が思いついた総称です。 この講義ではコードをビルド、テストし、また依存関係を管理するためのシステムについて見ていきます。 日々の学生生活にこれらはあまり重要ではないように思えるかもしれませんが、 インターンシップで大きなコードベースを扱うようになったりひとたび「実世界」に足を踏み入れると、これらを毎日目にすることになります。 なお「メタプログラミング」とは「プログラムを操作するプログラム」 を意味することもありますが、これは本講義で使う定義ではありません。
ビルドシステム
LaTeX で論文を書くとき、論文を生成するために実行するコマンドはなんでしょうか? ベンチマークを実行し、プロットし、プロット結果を論文に挿入するためのコマンドは? あるいは受講している講義で与えられたコードをコンパイルしテストを実行するコマンドは?
多くのプロジェクトにおいて、コードがあろうがなかろうが、「ビルド手順」があります。 それは入力から出力へ至るために必要な一連の操作のことです。 この手順にはしばしば多くのステップや分岐があります。 例えばこのプロットを生成するためにこれし、あの結果を生成するためにあれをし、 最終的な論文を生成するためにまた別のことをします。 この講義でみた他の事柄と同じように、この面倒くささに出会うのはあなたが最初ではなく、 ラッキーなことに既存の多くのツールが助けになります!
これらは普通「ビルドシステム」と呼ばれ 多くの ものがあります。 どれを使うかは行うタスク、使いたい言語、またプロジェクトの大きさに依存します。 しかし根本的な思想は全て似ています。 あるものから次のものを生成するために 依存関係 、ターゲット 、 ルール を定義します。 あなたはビルドシステムにあるターゲットを生成したいことを伝え、 ビルドシステムはそのターゲットに必要な依存関係を辿り、 最終的なターゲットが得られるまでルールを適用して中間ターゲットを生成します。 素晴らしいことに、ビルドシステムはこれを依存関係が変化しておらず前回の実行結果を使い回せる ターゲットについてのルールは実行せずに実現します。
make
は最もよく使われるビルドシステムの一つで、通常はほぼ全ての 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
が必要だがその作り方を指示するルールがない、と親切に教えてくれています。
それでは 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
は単に plot-data.png
を作れないと言っています。
それでは必要なファイルを全て作ってみましょう。
$ 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
make: 'paper.pdf' is up to date.
何も起こりません!なぜでしょう?なぜなら必要ないからです。
make
は前回ビルドしたターゲットがそれぞれの依存関係に対し最新であることをチェックしました。
これを確かめるため、paper.tex
を編集して make
を再実行してみます。
$ vim paper.tex
$ make
pdflatex paper.tex
...
make
が不要な plot.py
の再実行をして いない ことに注意してください。
plot-data.png
の依存関係は何も変更されていません!
依存関係管理
大雑把にとらえると、あなたのソフトウェアプロジェクトは何かに依存しており、依存先はそれ自体がまたプロジェクトでしょう。
インストールされたプログラム(例えば python
)、システムパッケージ(例えば openssl
)、
または使っている言語のライブラリ(例えば matplotlib
)に依存しているかもしれません。
今日では依存関係は リポジトリ によって提供されており、
このような多くの依存関係が一箇所にまとめられ簡単にインストールする方法が提供されています。
例えば Ubuntu のパッケージリポジトリは Ubuntu のシステムパッケージを提供し apt
ツールでアクセスでき、
RubyGems は Ruby のライブラリを提供し、PyPi は Python のライブラリを提供し、
また Arch User Repository は Arch Linux のユーザが作成したパッケージを提供します。
個々のリポジトリを扱う方法についてはリポジトリごとやツールごとに大きく異なるため、 この講義では特定のリポジトリやツールの詳細にはあまり立ち入りません。 むしろこの講義では扱うのは、それらが共通で使う用語です。 最初の用語は バージョニング です。 他のプロジェクトから依存されるほとんどのプロジェクトは、リリースごとに バージョン番号 を付与します。 例えば 8.1.3 や 64.1.20192004 のようなものです。 これらにはだいたい(必ずとは限りませんが)数字が使われます。 バージョン番号の目的は様々ですが、最も重要なものの一つはソフトウェアがきちんと動き続けることを保証することです。 例えば、私が自分のライブラリの新しいバージョンをリリースし、特定の関数の名前を変更したとしましょう。 そのリリース後にもし誰かが私のライブラリに依存するソフトウェアをビルドしようとすると、 そのソフトウェアはもはや存在しない関数を呼ぼうとしてビルドに失敗するかもしれません! バージョニングはプロジェクトに他のプロジェクトの特定のバージョンや 特定の範囲のバージョンに依存すると明示させることでこの問題を解決しようとします。 これにより、もしあるライブラリに変更があっても、それに依存したソフトウェアはライブラリの古いバージョンを使うことでビルドし続けることができます。
しかしこれでもまだ完璧ではありません! 例えば私がライブラリの公開されたインターフェース(ライブラリの “API”)を変更せずにセキュリティに関する更新を行い、 古いバージョンに依存している全てのプロジェクトは即座に新しいバージョンを使うべきであるとき、どうなるでしょうか? ここでバージョンに含まれる番号のグループ分けが役に立ちます。 それぞれのグループの詳細な意味はプロジェクトごとに異なりますが、 比較的よく使われるのが セマンティックバージョンニング です。 セマンティックバージョンニングでは、全てのバージョン番号は major.minor.patch の形式をとり、以下のルールに従います。
- もし新しいリリースが API を変更しないなら、patch バージョンを 1 上げる。
- もし API に後方互換性を保ったまま何かを 追加する なら、minor バージョンを 1 あげる。
- もし API に後方互換性を壊すような変更をするなら、major バージョンを 1 あげる。
これは大変有用です。いま私のプロジェクトがあなたのプロジェクトに依存しているとすると、
私が開発時にビルドに使ったのと同じ major バージョンであれば、
minor バージョンが開発時のもの以上である限り最新のライブラリをつかっても問題ない はず です。
言い換えると、もし私があなたのライブラリのバージョン 1.3.7
に依存しているとすると、
1.3.8
や 1.6.1
や 1.3.0
すらも問題なくビルドに使える はず です。
バージョン 2.2.4
はおそらくダメでしょう、なぜなら major バージョンが上がっているからです。
セマンティックバージョニングの例を Python のバージョン番号に見ることができます。
知っての通り Python 2 と Python 3 にはあまり互換性がありませんが、
これが major バージョンが上がっている理由です。
同様に、Python 3.5 用に書かれたコードはたぶん Python 3.7 では動きますが、
おそらく Python 3.4 ではダメでしょう。
依存関係管理ツールを使っていると、ロックファイル と呼ばれるものに出会うでしょう。 ロックファイルとは単にそれぞれの依存関係に関しあなたが 現在 依存しているバージョン番号を並べたファイルです。 通常、依存関係を新しいバージョンに更新するには更新プログラムを明示的に実行する必要があります。 これには不要な再コンパイルを避ける、再現可能なビルドを行う、あるいは勝手に(壊れているかもしれない)最新バージョンに アップデートされるのを避ける、といった多くの理由があります。 このような依存関係の固定の究極のものは vendoring と呼ばれ、依存関係のコードを全て あなたのプロジェクトにコピーすることを言います。 これによりあなたは依存関係のコードを完全にコントロールすることができ、 また独自の変更を加えることもできますが、 アップストリームのメンテナが加えた変更を明示的に持ってくる必要が生じます。
継続的インテグレーションシステム
より大きなプロジェクトを扱うようになるにつれ、プロジェクトに変更を加えるごとに 追加で行わなければならないタスクがあることに気づくでしょう。 例えばドキュメントの新しいバージョンをアップロードしたり、 コンパイルされたバージョンをどこかにアップロードしたり、pypi にコードをリリースしたり、 テストを実行したり、といった様々なことが必要になります。 あるいは誰かが GitHub でプルリクエストを送るたびにコードのスタイルチェックをして ベンチマークを実行したいでしょうか? このような必要性が生じたときが、継続的インテグレーションについて見てみるべきときです。
継続的インテグレーション(CI とも呼ばれます)は「コードが変更されたときに実行されるもの」 を表す総称で、多くの企業がオープンソースプロジェクト用には無料で様々な種類の CI を提供しています。 有名なものには Travis CI、Azure Pipelines、そして GitHub Actions があります。 これらは大雑把に言うと同じような動きをし、あなたは「リポジトリに何かが起こったときに 何をすべきか」を書いたファイルをリポジトリに追加します。 圧倒的によく使われるルールは、「誰かがコードをプッシュしたら、テストを実行せよ」です。 このイベントが発生すると、CI プロバイダは仮想マシンを 1 つ(あるいは複数)立ち上げ、 あなたの「レシピ」に書かれたコマンドを実行し、そしてたいていは結果をどこかに書き込みます。 テストが途中で失敗したら通知を受けるように設定したり、 テストが通っているときに限り小さなバッジがリポジトリに表示されるよう設定したりできます。
この講義の web ページは GitHub Pages という CI システムを使って構築されています。
Pages は CI のアクションの一つで、master
にプッシュがあるたびに Jekyll ブログエンジンを
起動し生成されたサイトを特定の GitHub ドメインで公開します。
これによって web サイトの更新が簡単になります!
手元で更新を行い、git でコミットしプッシュします。
あとの作業は CI がやってくれます。
テストについてちょっと余談
大きなソフトウェアプロジェクトのほとんどには「テストスイート」がついています。 テストの一般的な概念については知っていると思いますが、ここでは実世界で目にすることになる テスト手法とテストに関する用語について簡単に述べておきます。
- テストスイート:全てのテストを表す総称。
- ユニットテスト:特定の機能を他から分離しテストする「小さなテスト」。
- 結合テスト:システムの大きな部分を実行し異なる機能やコンポーネントが 一緒になって きちんと動くかをテストする「大きなテスト」。
- 回帰テスト:以前の バージョンでバグを発生させていたパターンを実行し、そのバグが再発していないことを確認するテスト。
- モック:関係のない機能をテストすることを避けるために、ある関数、モジュール、型を置き換えるための偽の実装。 例えば「ネットワークのモック」や「ディスクのモック」など。
練習問題
- たいていの makefile は
clean
と呼ばれるターゲットを持っています。これはclean
と呼ばれるファイルを生成することを 意図したものではなく、make で再生成可能な全てのファイルを削除することを目的としています。ビルドの手順を「元に戻す」 ようなものです。上記のpaper.pdf
のためのMakefile
にclean
ターゲットを実装してください。 phony と呼ばれるターゲットを作る必要があります。 サブコマンドgit ls-files
が役に立つでしょう。 その他のよく使われるターゲットはここに載っています。 - Rust のビルドシステムで依存関係の バージョンを指定するための様々な方法を確認してください。たいていのパッケージリポジトリは似たような文法をサポートしています。 それぞれの文法(caret、tilde、wildcard、comparison、multiple)についてそれが役に立つようなユースケースを考えてみましょう。
- Git はそれ自体が簡単な CI システムとして利用できます。
.git/hooks
には特定の操作をしたときに実行される ファイル(の実際には実行されないサンプル)があります。make paper.pdf
を実行し失敗したらコミットを拒否するようなpre-commit
ファイルを書いてください。 これはあるコミットで論文がビルドできなくなることを防ぎます。 - GitHub Pages を使って自動更新される簡単なページを作ってください。
リポジトリ内の全てのシェルスクリプトに対して
shellcheck
を実行する GitHub Action を追加してください (やり方はここにあります)。 ちゃんと動いていることを確認しましょう! - 独自の GitHub action を作り
全ての
.md
ファイルに対しproselint
やwrite-good
を実行してください。あなたのリポジトリでこれを有効にし、typo のあるプルリクエストを出すことで ちゃんと動いていることを確認してください。
Licensed under CC BY-NC-SA.