複雑なプロジェクトに貢献する
オープンソースのメンテナーやコントリビューターとして活動することが多い私は、よく「どこから始めればいいのですか?」と聞かれます。意味のある変更を加えることを目標に、新しいプロジェクトにはどう取り組めばいいのでしょうか。複雑なプロジェクトの内部を、どうすれば理解できるのでしょうか。
こうした疑問は、オープンソースかプロプライエタリか、趣味か仕事かを問わず、どのソフトウェアプロジェクトにも当てはまります。私が取るアプローチは、どの場合でも同じです。ただし、プロフェッショナルな仕事と大きく違うのは、他のエンジニアに直接アクセスできることです。彼らは、少なくとも助けることを期待されている、場合によっては義務づけられている人たちです。一方、オープンソースプロジェクトでは、たいてい自分一人で取り組むことになります。
私は、複雑なプロジェクトに取り組むための一定のパターンを確立しており、この投稿でその方法を説明します。このパターンが誰にでも通用するとは思っていません。ただ、複雑なプロジェクトを学び、貢献してみようという自信を、他の人が持つ助けになればと思っています。
ここでいう複雑なプロジェクトとは、実装をすぐに理解するのが難しいソフトウェアプロジェクトのことです。この定義は主観的なものです。ある人には複雑に見えるプロジェクトが、別の人にはそう見えないこともありますし、その逆もあります。
ステップ1:ユーザーになる
どんなプロジェクトでも内部を理解するための第一歩は、そのプロジェクトのユーザーになることです。熟練ユーザーになる必要はありません。ただ、私自身がこのステップを終えたと判断する基準は、たとえ小さく単純なものでも、そのプロジェクトを使って実際に何かを作ってみることです。たとえば、Zigプログラミング言語に貢献する前に、私はいくつかの実用的なライブラリを作りました。
ユーザーとして使うことで、プロジェクトが持つ機能を幅広く理解できます。リファレンスドキュメントを読むことと、実際にプロジェクトを使うことの間には、大きな違いがあります。おもちゃのような小さなプロジェクトを作ることは、理論的な理解から実践的な理解へ移るための重要な橋渡しになります。
さらに、そのプロジェクトの慣用表現も学び始めます。そこにはプロジェクトの文化的な背景が表れていて、なぜそのプロジェクトがそのように動くのか、なぜその機能があるのか、といったことを理解する手がかりになります。これは重要です。プロジェクトに取り組む他の人たちへの共感を育てる助けになるだけでなく、どのような変更ならそのプロジェクトにとって自然で、どのような変更はそうでないかを判断する指針にもなるからです。
この段階でコミュニティに参加することも強くおすすめします。IRCやDiscordに参加し、地域のミートアップに出席し、講演を見たりしましょう。一定期間は、話すよりも聞くことに時間を使ってください。ここでの目標は、共感を持ち、プロジェクトがどのように動いているかを学ぶことです。他の人が学ぶ様子を見ているだけで、これほど多くを学べるのかと、私はいつも驚かされます。
ステップ2:プロジェクトをビルドする
プロジェクトのビルド方法を学び、動作するバイナリ(またはそれに相当するもの)を手に入れます。ビルドシステムや依存関係などを理解することに手間をかける必要はありません。ソースコードから自分の環境で実行可能なバイナリに至るまでを、確実に、何度でも再現できるように、ガイドやウェブサイトなどの手順をそのままなぞってください。
プロジェクトのビルド方法を学ぶ前に、コードを読まないでください。プロジェクトのビルド方法を学ぶ前にソースコードを理解しようとして、身動きが取れなくなっている人をあまりにも頻繁に見かけます。私にとって、その学習プロセスには実験して壊すことも含まれます。ビルドできなければ、ソフトウェアプロジェクトを実験したり壊したりするのは難しいものです。
機能が完全にそろったビルドにこだわらないでください。複雑なプロジェクトでは、適切な依存関係やシステム、設定などがそろっている場合にしか使えない機能があることも珍しくありません。その場合も、気にする必要はありません。目標は、自分の環境で十分に動作するバイナリを手に入れることです。後のステップを進めるうちに、より多くの機能を備えたビルドに取り組むための経験と自信が身につきます。
このステップでは、テストスイートの実行方法を学び、テストをパスさせることもおすすめします。そうすれば、後のステップで実験したり、ものを壊したりするのが簡単になります。複雑なプロジェクトには複雑なテストスイートがあることも多いため、テストスイートの一部だけを実行できるようにする場合もあります。実験に十分な範囲で構いません。
ステップ3:主要経路の内部を学ぶ
内部を学ぶとき、私は「下へたどり、上へ学ぶ」と呼んでいるアプローチを使うのが好きです。
下へたどる
まず機能やユースケースを一つ選び、その機能がたどるコードパスを外側から内側へ追跡します。この過程では、通過するファイル、行、関数についてメモを取ります。ただし、この時点では、何がどう動いているのかを理解しようとはしません。これが「下へたどる」段階です。
たとえばZigコンパイラーを調べたとき、私はZigのソースコードから実行ファイルをビルドするzig build-exeコマンドを追跡するところから始めました。この追跡によって、zig CLIのソース、build-exeサブコマンド、さらにそこから「Compilation」サブシステムへとたどり着きました。そのサブシステムは、レキサーやパーサーなどを呼び出します。私は経路を追うために必要な範囲を超えて、実装の詳細を読むことはしませんでした。
追跡メモからは、通常、何かがどのように動くのかという「全体像」を得られます。ファイル名や関数などを手がかりに、プロジェクトの主要なサブシステムも見えてくるでしょう。これは後で、学習のプロセスを無理のない大きさの単位に分けるのに役立ちます。
すべてを学ぼうとしないでください。よく見かける間違いは、プロジェクト全体を1行ずつ読もうとして、何週間、何カ月も迷い続け、最終的に意欲を失ってしまうことです。対象を絞り、機能ごとに学んでください。
ヒント:機能を選ぶときは、ユーザーとして使い慣れているものを選びましょう。また、可能なら、表面上は単純そうな機能を選んでください。たとえば、コンパイラーを学ぶために私が最初に追跡したZigプログラムは、2つの数を足すだけで、出力がまったくないものでした。
上へ学ぶ
機能を追跡したら、次は対応する各サブシステムがどのように動くのかを実際に学びます。追跡の段階ではCLIやAPI呼び出しなど、最も外側の地点から始めました。一方、学習の段階では、内側の地点から始める傾向があります。
最も内側の地点から始めるのは、そこがたいてい最も基礎的で、抽象化が少ない部分だからです。層を上へ進むほど抽象化の度合いが高くなる傾向があり、構成要素を理解していなければ学ぶのが難しくなります。
特定のサブシステムを学ぶときも、再帰的に「下へたどり、上へ学ぶ」ことを繰り返します。まず公開され、エクスポートされているAPIの範囲を調べ、次に各API呼び出しがどのように動くのかを学びます。上位の層はこのサブシステムをこのように使うため、学び方の指針になるだけでなく、スタックを上へ進むときにも理解しやすくなります。
実験して、ものを壊す
「下へたどり、上へ学ぶ」過程では、何かがどう動くのかを学ぶために、実験してものを壊すのがとても役に立つと感じています。プロジェクトの内部を読む前に、ビルド方法を学ぶことが重要なのはこのためです。
新しいログ出力を追加したり、小さな機能を実装したり、既存の機能を変更したりしてから、プロジェクトを再ビルドし、何が起きるかを確認します。何かがどう動くのかについて、自分の理解を本当に試すよい方法でもあります。
たとえば、Zigのトークナイザーを学んでいたとき、私は新しいトークンを追加しました。トークン化はできましたが、その後パーサーが失敗することが分かりました。次のシステムであるパーサーに進んだときは、新しいトークンに何かをさせました。その次も同じように進めました。
メディアで補う
この段階を通して、コードの細部を深く掘り下げる作業を、本や動画、ブログ記事など、利用できるメディアで補ってください。内部構造を扱った既存の資料があるなら、読みましょう。
ただし、そうした資料だけで専門家のレベルに到達できるとは考えないでください。ステップ1の「ユーザーになる」と同じように、「メンテナーになる」ために実際のソースコードを触り、動かしてみることの代わりになるものはありません。これも理論と実践の違いの一例です。
ヒント:内部を学ぶための資料が存在しないなら、自分で書いてみましょう。Zigについては、似たような最新の資料を見つけられなかったので、Zigコンパイラーの内部構造について自分で書きました。何かについて書くことは、学んだことを定着させるよい方法ですし、将来のコントリビューターの助けにもなります。
ステップ4:最近のコミットを読み、再実装する
内部を学ぶ最後のステップとして、調べてきたサブシステムに関係する最近のコミットを読み、なぜその変更が行われたのかを完全に理解しているか確認します。これは、教科書の最後にある問題集に取り組むような、学習の仕上げにあたります。
プロジェクト全体のコミット履歴か、調べてきたサブシステムに関係する特定のファイルやフォルダーのコミット履歴を確認します。そして、まず解決策、つまりコミットによる変更を調べるか、あるいはその変更が修正したバグを見つけて自分で修正を試み、同じような解決策にたどり着けるかを確認します。
「問題に取り組む」ときは、調べるコミットの直前のコミット時点までリポジトリをチェックアウトします。そのコミットがバグ修正なら、修正対象のバグを再現し、自分で解決策を実装してみます。最後に、自分の作業とメンテナーやコントリビューターによるコミットを比較します。
自分に与えるヒントは、必要な変更量だけです(VCSの差分に表示される+/-の行数)。最初は、変更行数が50行から100行を超えるものは避けることをおすすめします。
ステップ5:小さな変更を加える
私は小さく始め、少しずつ、より大きなタスクに取り組むのが好きです。この段階では、プロジェクトの技術的な構成要素を理解できています。ここからは、人間的な構成要素を学ぶ番です。目標は小さな変更を加え、貢献とレビューのプロセスを学ぶことです。
たいてい最も難しいのは、加えるべき小さな変更を見つけることです。ここに特効薬はありません。私はイシューを眺め、コントリビューター向けによさそうなものを探します。最初の数回はうまくいかなかったり、イシューを完全にあきらめて別のものを試したりすることもあります。それでも、最後には見つかります。イシューを見つけるまで、あるいはイシューを修正するまでにかかる時間は、いら立つほど長いかもしれません。しかし、それが参加料です。多くのプロジェクトには、新しいコントリビューターの道しるべとなる「contributor friendly」ラベルがあります。
最近のプロジェクトの多くは、貢献のプロセスをきちんと文書化しています。変更を実装したら、その手順に正確に従ってください。前のステップでコミュニティに参加していれば、誰かに連絡して助けを求めたり、手順を再確認してもらったりするよい機会です。
たとえば、私がZigに初めて貢献したのは3行の変更でした。前のステップにかけた時間は含めず、2晩で4、5時間ほどかかりました。今日このバグが現れたなら、私は数分で修正できるでしょう。しかし、そこまでの習熟度に達するには時間がかかります。
成功
ここまで来れば、複雑なプロジェクトを学び、実際に貢献できたことになります。
複雑さを恐れないでください。プログラミング言語やブラウザー、データベースなど、典型的に複雑だと思われているプロジェクトを、魔法のようなもの、あるいは高次の存在にしか作れないものとして見ているエンジニアは多すぎると思います。私は、すべてのプロジェクトは他の人間によって始められたものだと考えるようにしています。彼らにできたのなら、私にもできます。あなたにもできます。
私の進め方を共有することで、他の人にとって複雑なプロジェクトが、もっと取り組みやすいものになればと思っています。
記事をランダムに読む