クフでダローバルな日記

タフでもグローバルもない

挫折してプログラミングに思うこと

突然ですが、最近プログラミングや今後の人生について考える機会が多かったので、今後の自分のためにまとめておこうと思います。 酒を飲みがてら。ジンジャーエールおいちい。

いつもとは違って人に読ませるためのものではないので、kuhudaro.mdに投稿しようか迷ったのですが、ちゃんと文章にしようと思いこちらに投稿することにしました。

契機となったはてなインターン

この話を何度もするのは鬱陶しいかもしれないが、今年の夏はてなインターンに行ったことは、やはり自分の人生でもかなりの契機になったと思う。
あのインターンで僕は、他大でプログラミングを生業としようとしている同年代の人達と、遅ればせながら初めて交流した。
もちろん、今までにも少しは交流する機会はあったけれど、あそこまでプログラミングのことだけを考えた1ヶ月はなかったし、本当に貴重な体験をさせて戴けたと思っている。

同年代の彼らとの交流はカルチャーショックから始まったように記憶している。

僕にとっての「プログラミング」は「自分が作りたいものを作る/自己承認欲求を満たす手段」であったし、それ故まず「作りたいもの」があって、その実現のために技術を習得するというトップダウンなものばかりだった。
しかしこのトップダウン的な勉強には、当時から気づいていた大きな欠点がある。 習得する知識の幅が狭いのだ。
例えば、twitterBOTを作るためにはruby,paas,cron,APIなどの知識が必要であり、webアプリを作るのであればフレームワークやhtml・css・jsなどの知識が当然必要になってくる。
しかし、逆に言うとそれさえマスターしてしまえば、すこし発展させたものはごく簡単に作れるようになってしまう。
この簡便さは一見良いことのように思えるが、実のところ全く勉強にならない。全くのコピペプログラミングでしかないのに成果物が生み出せるため、知識の幅が広がるペースが極めて遅いのである。

一方、はてなインターンで出会った彼らは、プログラミングの基礎となる知識がとても豊富だった。
それは競技プログラミングについてもそうだし、扱える言語の数についても僕は全く比べ物にならないレベルだった。
彼らと話していると常に僕は知識の乏しさに恥じ、少しバズッた小さな小さなwebアプリやtwitterBOTをさも代表作のように誇示していた視野の狭さを悔やんだ。

ただ、はてなインターンのマンガチームでそれなりの成果を挙げられたことは、少しばかりの自信につながった。 というより、そういった誉がなかったら僕は完全に挫折していただろうと思う。

夏以降の僕

はてなインターン以降は、自分の課題であるボトムアップ的で幅の広い勉強をしたいと思っていた。
また、視野を拡げるために機会があれば自分から行動するようになった。

そこでまず、「モノ」を作るハッカソンのHackUに出ることに決めた。
このHackUでも、二つのショックを受けた。
一つは多摩美大の学生たちの「プロフェッショナルさ」であり、一つは同じ大学の優秀な同期の存在である。

この東大×多摩美のHackUでは、僕と多摩美の学生三人がチームとなってものを作った。
僕は電気系にいながら電子工作をした経験が殆ど無いので、結局完成度の高いものを作ることはできなかった。 より厳密に言うならば、僕の失敗のせいで全く機能しないモノとなってしまった。
これに関しては僕が自分を責めれば良いだけなのだが、ガワを作った多摩美の学生は、僕に対して適切な指示と交渉をしてくる能力、ものを作るスピードと精密さ、そしてもちろんデザイン性・機能性と、凡そ同い年とは思えないフルスタックな人物だった。
さらに、このHackUで優勝した同じ東大工学部の学生も、ものを作るスピードやデザイン性やアイディアの豊かさにおいて僕を圧倒していた。

要するに、夏にかろうじて自分を支えていたトップダウン的な能力も音を立てて崩れ去ったのである。

それ以降、僕は自分の得意なものを見つけるため、色々なものに手を出した。
競技プログラミングやctf、人工知能の勉強やIT起業との就活イベントなど、目につく物は手当たり次第力を注ごうとして、その殆どで挫折した。
唯一情報可視化技術だけはとても興味を持っているが、これも時間の問題なのではないかと思う。

というのも、そもそも「プログラミング」自体が出来ないのだということに気づいたためである。

作りたいものを作っていた時、本来プログラミングに必要な可読性、保持性、拡張性などは殆ど無視していた。
そんなものはなくても、それなりのものは出来る。

逆に、自分が思いつくもの・作りたいものはすべて実現できるという自信もある。
随分楽天家のように聞こえるかも知れないが、実際は悲観的な自信である。 すなわち、自分が実現できる範囲のものしか、自分では思いつけないのだ。 (実際、僕の場合webサービスのアイディアは思いつくこともあるが、電子工作やネイティブアプリのアイディアは殆ど浮かばない。)

そうなってくると、逆に作りたいものをすべて作っていたら時間が無くなってしまうし、上記に上げたような必須能力は身につかないままだろう。
だからこそ、上記に言ったような「プログラミング」の方法論を身に付けたいと感じている。

はてなドワンゴ合同ハッカソンとこれから

昨日(2015/11/28)、はてなドワンゴの合同ハッカソンに出場した。 作成したものについては今度また別に記すつもりだ。
僕はほとんど優勝する気はなく、久しぶりにただ自分の作りたいものを、馬の合う友人と作ることが出来て純粋に楽しかった。

ただ、やはりここでも自分が到底かなわないような技術力を持つ人達を目の当たりにしたし、それなりに落ち込んだりもした。

しかし、最近はむしろ、そういった技術力を磨くのではなく、純粋に研究をする方がやはり僕には向いているのではないかと思い始めている。 僕はやはり勉強は好きだし、世の中に数多いる自分より優秀なエンジニアと同じ土俵で戦う必要はないのではないかと感じたためである。
もちろん、研究の世界でも自分より優秀な人はいくらでもいるだろうし、彼らに打ちのめされる自分は既に想像できる。
それでも、(この比喩は正しくないかもしれないが)パーツを組み合わせて完成品を作るよりも、パーツとなるもの、さらにはそのパーツを創りだすものというより基礎的な部分の研究に、一工学徒として魅力を感じている。

実際に研究室に配属されるのはまだ先で、研究室を選ぶのにもかなり苦労しているが、この情熱が冷めないように、「ボトムアップ」を意識したプログラマ生活を送って行きたい。

「シンデレラ」への反省としての「イントゥ・ザ・ウッズ」

久々にプログラミング以外のことを書いてみようと思います。

先日、ディズニーの映画「イントゥ・ザ・ウッズ」を観ました。 特に期待せずに見たのですが、結構面白かった上にあまりweb上に感想エントリもあんまり上がっていないようだったので、感想を書いてみようと思います。

普段ディズニーアニメをそんなに見ているわけではないので、もし間違ってる所があればコメントとかで指摘して戴けると幸いです。

紹介エントリというよりは感想文(考察?)的な感じなので当然ネタバレを多数含みますが、その点はご容赦ください。

ちなみに本当は一回で書ききる予定だったのですが、なんか長くなっちゃったので分割してます。

続きを読む

gitに新機能を追加してgit masterを目指す(応用編)

前回の記事の続きとなります。
今回はcommitに新機能を追加していきます。

前回まで

前回、git addを手探って、git add .とするように変更しました。

前回記事のコメントでも指摘されたように、このような変更は簡単にプラグインで対応できます。
従って、今回の変更点はプラグインやシェルの設定では実現しがたいものとなるようにします。

変更内容

git commit --amend をしようとした際に、直前のcommitがpush済みであったら中止する

目的

この変更の必要性がわかりづらいと思われるので、丁寧に説明してみます。

git commit --amendは、直前のcommitを修正できるコマンドです。
commit messageなどを修正したい時には便利なのですが、もし既にpushしたcommitを修正しようとすると面倒なことになります。

例えば直前のcommit A がpush済みであるとします。
この時、remoteにも A が存在します。

この状態でAをamendし、A'とするとしましょう。

そうすると、pushした際にremoteでAとA'がconflictするため、修正がかなり面倒なことになってしまうわけです。

実際、gitのマニュアルでも、以下の様な強い口調で、push済みのcommitを修正することが非難されています。

公開リポジトリにプッシュしたコミットをリベースしてはいけない

この指針に従っている限り、すべてはうまく進みます。もしこれを守らなければ、あなたは嫌われ者となり、友人や家族からも軽蔑されることになるでしょう。
Git - リベース

また、gitのメーリングリストでもこの問題については既に話し合われていたようで、SmallProjectsIdeas - Git SCM Wiki に以下の様な記述がありました。

Commands like "git rebase", "git rebase -i", "git reset", "git filter-branch", "git commit --amend" can be very powerful to rewrite local history before publishing it. On the other hand, they can be very dangerous if used on an already published history. Git could relatively easily detect that one is rewriting a commit that is an ancestor of a remote-tracking branch, and warn the user (perhaps giving a way to "git reset --hard" back to the original state).

以上のことから、commitをamendする際に、それがpush済みであるか否かを判別するのが必要であるということがわかると思います。

手探りの手順

手探りの手順、といってもだいたいのことは前回のadd編と同じです。

しかし、今回の最大の問題点は 「あるcommitがpushされているのかどうかをいかにして判別するか?」 というものだったので、以下の様な手順を踏みました。

  1. localのcommitとremoteのcommitの関連性を探す
  2. 全探索
  3. git statusを手探る

というものです。以下順に解説していきます。

localのcommitとremoteのcommitの関連性を探す

git pushした際に、「pushした」という情報がどこかに保存されていれば、そのフラグを見ることで実現できるだろうと予想して、まずはこの関連性を探すところから始めました。

具体的には、最初にgit pushの動きを追いました。 もし「pushした」という情報をどこかに保存するのであれば、その処理はpushをした際になされるだろうという予測をした為です。
しかし、pushの際には処理が多くていまいち全容が把握できず、多分そういった情報は保存していないだろうという予測はできたのですが、確信を持てずにいました。
この原因はcommitの仕組みについても殆ど理解していなかったことにあると考え、一度git addgit commitgit pushが何をやっているのかをボトムアップ式に学んでみることにしました。

主に参考にしたのはGit - Gitの内側 と、Git の仕組み (1) - こせきの技術日記です。

前者は公式の情報でありかなり詳細なのですが、gitは用語が難しく、概念としてイメージしづらいという難点があります。
そこで、後者の図が豊富なブログで一度大雑把に理解し、もう一度公式のチュートリアルを読むことにしました。
gitの内部構造について知りたいのであればこれら2つを読めばかなり理解が捗るのではないかと思います。

これらを読んで理解した、私達の目的に直接携わる部分は以下の様にまとめられます。

  1. commit同士の関係はparentによって辿る片方向リスト
  2. branchについての情報は先頭のreferenceと上記の片方向リストでしかわからない
  3. 従って、localとremoteのcommitのどちらが進んでいるかを保存しているものはない

1つ目については、gitのソースのcommit.hを読めばcommitが構造体として定義されているため一応知ることが出来ます。しかし、全体像を把握するためには大量のソースを読む必要があるので、こういった「まとめ」を読むことが有効なのではないかと思います。
考え方が先、実装は後ですね。

全探索

上記のことが分かったので、commit同士の関係性を知る方法が定まりました。 すなわち、 remoteとlocalのbranchの先頭のreferenceからparentをたどっていき、一致した時の状態によって判断する というものです。

しかし、この手順を実装するのは若干面倒であることがわかりました。というのも、二つのcommitの関係はただの直線関係だけではないからです。

その関係のパターンについて、図を用いて説明します。
図中、左にあるノードのcommitがparentであるとします。

パターン1 直線上の場合

関係を探るためには、localとremoteそれぞれのheadからparentをたどって行き、どちらかのheadに到達すれば分かるので容易です。

以下の3パターンの内、localがremoteより進んでいるもののみがamend可能であるべきです。

f:id:SWIMATH2:20151103213443p:plain:w300 f:id:SWIMATH2:20151103213447p:plain:w300 f:id:SWIMATH2:20151103213452p:plain:w300

パターン2 分岐している場合

これは手順が面倒で、localとremoteそれぞれのheadからparentをたどっていき、お互いのheadに到達せずに一致する所があったら分岐している、というアルゴリズムが考えられます。

f:id:SWIMATH2:20151103213606p:plain:w300

この場合はremoteのheadのcommitがlocalのheadのcommitに依存していないので、amend出来るようにします。

これらに加えて、mergeしている場合などにparentが複数あるパターンがあるので、全探索の手順はかなり複雑なものになりそうです。
また、gitを使っていて生じうるパターンを数多く考えるのはかなり困難を極めていました。

git statusを手探る

全探索の実装を始める前に冷静になって考えなおしてみたところ、git statusをした際に表示される画面では、localのheadがaheadなのかbehindなのか表示されるため、この情報を取得することができれば良いのではないかという予測を立てることが出来ました。

ということで、git statusした際の動きを前回同様手探ってみたところ、remote.cの中にあるstat_tracking_info()という関数によってbranch同士の情報を得ていることがわかりました。
すなわち、commit --amendした際にこの関数を呼べば、push済みか否かのチェックが出来るということです。

結局、施した変更は以下のとおりです。

diff --git a/builtin/commit.c b/builtin/commit.c
index 63772d0..8a9a5b3 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -32,6 +32,7 @@
 #include "sequencer.h"
 #include "notes-utils.h"
 #include "mailmap.h"
+#include "remote.h"

 static const char * const builtin_commit_usage[] = {
        N_("git commit [<options>] [--] <pathspec>..."),
@@ -1125,6 +1126,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
                                      struct wt_status *s)
 {
        int f = 0;
+       int ours, theirs;
+       const char *full_base;
+       struct branch *branch = branch_get(NULL);

        argc = parse_options(argc, argv, prefix, options, usage, 0);
        finalize_deferred_config(s);
@@ -1149,6 +1153,12 @@ static int parse_and_validate_options(int argc, const char *argv[],
                else if (whence == FROM_CHERRY_PICK)
                        die(_("You are in the middle of a cherry-pick -- cannot amend."));
        }
+
+       stat_tracking_info(branch, &ours, &theirs, &full_base);
+       if (amend && ours == 0) {
+               die(_("You've already pushed this branch."));
+       }
+
        if (fixup_message && squash_message)
                die(_("Options --squash and --fixup cannot be used together"));
        if (use_message)

この関数を発見する事ができたことで、変更がかなり容易なものとなりました。 面倒な処理を実装するときには、「その実装が既に為されているのではないか?」と考え、似たような処理をしてそうな部分を探してみるのが有用な手段の一つなのではないかと思います。

git mailing listへの投稿と今後の課題

目的を達成できた私たちは、git本体にマージされることは出来ないかと考えgitのメーリングリストに投稿してみることにしました。

チームメンバーのtkk君が英語で投稿してくれたのですが、2時間後には他の開発者からコメントの返信が来たのでそのスピードに驚きました。
とはいってもコメントはかなり要求水準が高く、以下の様な修正点を挙げられました。

  1. remoteにブランチがあることを確認できていない
  2. 後方互換性が弱いので、オプションで指定できるようにするべき
  3. テストを書くべき

1については簡単に修正ができるのですが、オプションで指定するとなるとまた別の部分を手探る必要が生じ、さらにテストを書くとなると全く別のシェルスクリプトを書かねばならないため、実験のタイムリミットを考慮して修正せずに終えることにしました。

テストや後方互換性の重要性については理解しているつもりでしたが、実際に多くの人に使われるソフトウェアに携わると考えると、このように厳しくチェックされることでより再認識できたように感じます。

今回は時間の制約上諦めることになりましたが、今後の課題としてこれらの改善点を記しておくことにします。

謝辞

ご指導頂いた先生とTAの方々やチームメンバーの二人のおかげでどうにか開発が進められました。ありがとうございました。

gitに新機能を追加してgit masterを目指す(初級編)

学科の実験「大規模ソフトウェアを手探る」でgitを弄って改良しようとしたので、その過程と結果を何回かにわたって書いてみようと思います。

「レポートとしてブログ記事が認められる」という変則的な実験なので、レポートとして書きますが、文体は敬語かつ口語っぽく書かせていただきます。
また、単にハウツーを書くだけではなく、チームや個人としての感想や想いについても書かせていただきます。

というのも、ブログ記事が推奨されている理由が、

今後同じく大規模ソフトウェアやOSS、特に自分の暑かったソフトについて手探ろうとしている人たちの参考となるため

であるためです。
従って、この記事は単なるレポートではなく、他の方に参考にして戴ける記事にしたいと思っているので、質問や意見などがあればコメント欄やはてブコメントなどで仰ってください。
twitter@showmeearなので、リプを飛ばしていただいても構いません。

ちなみにチームメンバーはtkkとvinterと僕です。
記事内でもこの名前で呼びますが、レポートの中でどう扱われるんでしょうか……?

なお、手探るために用いたgitのバージョンは2.6.0です。

実験「大規模ソフトウェアを手探る」とは

レポートとしては不要かもしれませんが、まずこの実験について簡単に紹介しておきます。

僕の所属する東京大学電子情報工学科(通称eeic)で3年後期に行われる実験の一つで、テーマは

全容を把握できるわけがない程大きなソフトウェアをいかに扱い,必要な動作を理解し,変更するか

だそうです。
サイトは一般に公開されているようなので、ご覧になってみてください。
配布されている資料のpdfがとても参考になるかと思います。

手探る対象ソフトの決定

手探る対象として、私達のチームはgitを選びました。

その主な理由を以下に述べます。

ブラックボックス化の解消

一つ目の理由が、「 普段良く使っているけど、中身がほとんどわからないまま使っているので、手探ることで実態を知りたい 」というものです。

僕自身もかねてから思っていたこととして、「便利なものをブラックボックス化して使っているだけだと、トップダウン的な学習しか出来ずに技術の進歩についていけなくなる」というものがあります。
gitは何も理解しなくても使えて便利なのですが、そのために完全にブラックボックス化して使っていたことの反省として弄ることにしました。

「もともとバージョン管理システムsvnなどがあったのに、どうしてgitがデファクトスタンダードとなったのか?」とか、「gitで何が出来て何が出来ないのか?」を真に理解しようと思ったら、エンジニアとしては「コードを読む」のが何よりも良いのではないかと思います。

コードの綺麗さ

ふたつめの理由は「gnuplotなどに比べると割と新しめのソフトなので、コードが綺麗そう」というものです。

有名な逸話ですが、創造主Linusは、1週間でgitの元となるものを創りだしたそうです。

安直な考えではありますが、元となるものが1週間で出来たのだったらコードもさほど煩雑ではないだろうし、歴史も長くないので破綻しているところも少ないだろうということで、コードが綺麗で弄りやすいだろうと期待したということです。

実際に見てみると、コードが上手く分割されていたり、変数名や関数名が分かりやすかったりと、かなり綺麗なものでした。
後述しますが、mailing listでmergeしてもらおうとする際には結構厳し目のレビューをされるようなので、そういった開発者コミュニティの努力によって維持された美しさなのかなと思います。

開発とは直接関係はありませんが、こういった綺麗なコードを読む機会は普段一人で開発している時には得難いものなので、そのためにもOSSのコードを読んでみることは良いのではないかと思います。

カッコよさ

gitにコミットしたんだぜ」って言うとカッコ良さそうですよね。それだけです。

最初の手探り git add

最初に私達のチームでは

  1. commitメッセージを勝手に変える
  2. git addとするとgit add .される

機能を実装することにしました。
あまり重くなさそうなタスクを探した結果、これらになりました。

1つ目はvinter君、2つ目は僕とtkk君が担当したので、僕は後者について解説します。

目的

現状git addとするとgit add .の間違いではないかと言われるが、当然その間違いであるので、そのような提案をするのではなく、実際にgit add .と入力したものとした動きをする。

gitの処理の流れ(怪しい関数の探し方)

まず、git add . した時の流れをgdbで追いました。

ただ、gdbで追っているだけだとどれが大事な関数なのかわかりづらいので、実際はソースをエディタで見て怪しい関数にめどを付け、その関数にbreakpointを貼るようにしました。
最初のうちは「怪しい関数」と言われてもよくわからなかったのですが、慣れてからの探し方は以下のような流れでした。

  1. gdbでどのファイルのどの関数に入ったのか見る
  2. そのファイルをエディタで見て、呼ばれる関数を探す
  3. その関数の中で、複数の処理をし、複数の関数が呼ばれていくはずなので、その中に怪しい物があるはず
    • 引数のない関数は多分準備とか設定に関わっているものなので怪しくない
    • 逆に、引数が多くてrunとかhundleとかcmdとかのいかにも実行しそうな名前のついてる関数は怪しい
      (gitはこのように命名がわかりやすいので非常に弄りやすい)
  4. 見つけた関数にbreakpointを設定し、1に戻る
  5. もし何らかの文章が表示された時は実効が完了してしまった可能性が高いので、飛ばした部分にある関数が怪しいとめどを付けてやり直す

こういった手探りを繰り返して分かったgitの大雑把な処理の流れは以下のとおりです。

  • 引数として与えられたコマンドやオプションをparseする
  • commandをbuiltinかexternalか判別
  • commandに対応した関数を呼ぶ

です。

コードの中での関数の流れ(先述の怪しい関数)は主に以下の通りとなっていました。

main(git.c)
→handle_options(git.c)
→ run_argv(git.c)
→ handle_builtin(git.c)
→ cmd_add(builtin/add.c)

parseについては当然のことだと思われるので省略します。

builtin,externalについて、僕は知らなかったのですが、gitはプラグインを用いて自前のコマンドを用意することが出来ます。
もちろんそれはソースとは別の所にあるので、それらを区別する必要があるわけです。
それがrun_argv()の中で行われて、builtinコマンドであればhandle_builtin()が実行されます。

このhandle_builtin()ではgit.c内で定義されているstruct cmd_struct commands[]の中から対応する関数を探してきます。
このcommands[]の定義はgit.cの中でもひときわ目を引くので、エディタでみてればすぐに怪しいなと分かります。

実際、"add"に対応しているのは cmd_add() という関数であることがわかり、テキストエディタでプロジェクト内でこの関数で検索をかけてみると builtin/add.c にこの関数を見つけることが出来ました。

更に、commands[] のテーブルとbuiltinフォルダを見比べることで、「builtinコマンドの多くはこの builtin/hoge.c のなかに cmd_hoge() として定義されてるっぽい」ということまでわかりました。

add.cの流れ

次に、add をした時の流れについて詳細に追ってみます。

先ほどの調査でbuiltin/add.ccmd_add() が怪しいということが明らかになりました。

今回の目的はgit addgit add .とすることだったので、まずはgit add とした時にどこで例外処理されて終了するのかを探してみることにします。

gdbb cmd_add とした上で r addとして、nを連打して飛ばし続けていると、add.c:357

if (require_pathspec && argc == 0) {
  fprintf(stderr, _("Nothing specified, nothing added.\n"));
  fprintf(stderr, _("Maybe you wanted to say 'git add .'?\n"));
  return 0;
}

の部分で終わっていることが明らかにわかりました。
エラーで吐く文章も一致していることからも明らかですね。

無理矢理addするための二つの方法

今回は Maybe you wanted to say 'git add .'? との忠告を無視して、無理矢理git add .であるように振る舞いたいので、そのための方法を探します。

といっても、僕が実装した方法はとても愚直で、
argc==0だったらargv[0]に"."を入れておく
ということで解決しました。
つまり、エラーで提案するのではなく、あたかも最初からgit add .されていたかのように引数を無理矢理修正したということです。

実際にこれでコンパイルしてみて実行すると、期待通りの挙動をしたので、目標は達成しました。

ただ、この方法は汎用性が低く、汚いという欠点が明らかだったので、どうにか関数を使って上手いことやりたいとも感じました。
それを実際にやってくれたのがtkk君で、彼はargcが0の時にはaddremoveを1にすることで解決していました。

このaddremoveという変数は、git add -Aの時に1になるフラグなので、git add -Aの挙動を示すことになりました。

gitはこのようにフラグでオプションを管理しているところが多いので、困ったら怪しそうなフラグを探すのも手段の一つとして有りうるのではないかと思います。

次回予告

以上、gitを手探った際のファーストステップ、簡単なコマンドを弄ってみた時の手順を解説しました。

次回は、もう少し実用的な修正をした時の記録を書いていこうと思います。