ラップ・アップ・ニーマルニーマル
1月
ベルリンからブダペストまで鉄道旅行をした。大陸はどこまでもつながっている、という素朴な感動をえた。滞在したのは次の7都市。
滞在記は私的な日記にのみ書いた。この頃はまだブログを持っていなかったため。
2月
アテネ・カイロ・ローマへ旅行した。まだコロナの存在感は薄かった頃のこと。いま思えば平和で幸福な旅行だった。
旅行の前後では塩野七生の古代アテネ衰亡史を読んでいた。文明も民主主義もはかないものかという悲観と、いまある文明はきっともっとうまくやれるだろう、という楽観。そのどちらであろうと、僕ひとりが思い悩んだところで何も変わらないだろうという諸行無常の感覚。
3月
外務省からドイツに滞在注意報が発令され、渦中のドイツから帰国した。帰国直前のデイリー感染者は200-300人。これが帰国後とんとん拍子に1000人、1万人と拡大してしまう。
帰国後は秋田の実家に滞在していた。国内はいわずもがな、ヨーロッパ・アメリカの方からはいっそう陰惨なバッドニュースが伝えられ、気が滅入ってしまう。
このころ Qiita を退会してブログを開設した。
4月
松坂和夫『集合・位相入門』を読む。序盤の100ページほどのみ、わかるまで食らいつくような態度でじっくり読んだ。ある時までてんで読めなかった文章が、一瞬のひらめきによって単純明快そのものに変貌する。そんな体験が数度あった。どんな分野でも、専門書を読むのには教育と修行が必要であると知らされた。
『カラマーゾフの兄弟』、『燃え上がる緑の木』と大長編小説を立て続けに読んだ。
5月
前年から少し触っていたC++を使って、 AtCoder に初挑戦した。 Ruby は使わない縛りを自分に課して、プログラミングの感覚をイチから鍛え直そうと努めた。過去問を解くのにも熱中していた。
エージェント経由で業務委託の面談をうけた。すべての面談がオンライン化していて、楽ではあったが結果は出なかった。3社を受けていずれも落ちる。心の準備がまだできていないのだろうと諦めて、もうしばらく勉強期間を継続することにする。
6月
1年ぶりに東京に戻る。このころは勉強内容をまとめたブログ記事を毎日投稿していた。記事を公開することに対する心理的障壁は劇的に下がった。
以前より興味のあった Hanami フレームワークを試してみたり、『クリーン・アーキテクチャ』を再読してレイヤリングの考え方を再訪したりしていた。
7月
縁あって以前お世話になっていた会社から業務委託案件を受注した。月初から稼働開始した。
Project Euler に初挑戦し、50問までトントン拍子に進められた。ここでも C++ だけを使う縛りを自分に与えていた。
8月
nand2tetris に取り組み始めた。
フィリップ・K・ディック作品をいくつか読んだ。『アンドロイドは電気羊の夢を見るか?』『高い城の男』そして『マイノリティー・リポート』。
9月
森美術館にスタア展をみにいった。顧客体験としてよいとは思えなかった。作品というよりは、もっぱらオペレーションの問題である。広告費が大量投入された美術展は地雷と思うのが無難か。
勤労・学習のどちらにおいても、モチベーションが低下してしまった時期であった。なんのために働くのだろう? というようなことばかり考えていた。
ジム通いを再開した。人生初のパーソナルトレーニングも経験した。結局のところ、学習効率を高めるにはプロに教えを請うことである。生徒としてのアティチュードには注意している。自我は消し、トレーナーの言うことは神の啓示と思って取り組むのがよい。「コーチャビリティ」という概念はあとから知ったが、要はそういうことだろう。
『真田丸』を見るべくして、毎週ツタヤに通っていた。ストリーミング疲れした精神にとってツタヤはオアシスである。古典も駄作も、アイウエオ順にソートされてフラットに陳列されているのは荘厳である。推薦エンジンに馴致されるあまり、「自分の観たい映画は自分で決める」という最低限の主体性すら失ってはいまいか。ツタヤは救済である。
10月
転職活動を開始した。労働、運動、企業研究に明け暮れた月だった。この上なく合理的な生活リズムだったが、振り返ってみると無味乾燥にならざるを得ない。
11月
志望企業をふたつに絞り、月末には一社から内定を得た。
勤労感謝の日が三連休にあたるので、大阪旅行を計画した。大阪らしい何かを読もうと思って、谷崎潤一郎をまとめて読んだ。『細雪』『卍』『少将滋幹の母』そして『吉野葛』と読破。大長編ながら読みやすい名作である『細雪』と、技巧を技巧と思わせない熟練味をたたえた『少将滋幹の母』がお気に入り。
大阪では太陽の塔を見物した。異形の造形と突き抜けたスケール感で、人生ベスト級の芸術体験を味わった。並いる現代作家のなかで、どうして岡本太郎ただひとりがこれだけの仕事をできたのかはミステリーである。そしてそれがミステリーであるゆえに、これだけの成果を国内で再現することは永久に不可能だろうと思う。
芦屋の谷崎記念館も訪ねた。直筆原稿、書簡のたぐいをみた。飼い猫のシャムに「タイ」と名付けていたということを知って、いいセンスだと思った。
月末にはツイン・ピークスの旧シリーズを一気見した。
12月
転職の意志を業務委託元に伝えた。ありがたいことに引き留めてもらうことになり、改めて自分の行く末を考える機会となった。いろんな人と話し、異なる観点からキャリアについてのアドバイスをもらえたのは財産である。自分の直感を信頼して、転職の道をとることに結論づけた。
分散システムについての概論書である Designing Data-Intensive Applications の読書に取り掛かる。
図書館に通い、海外小説もいくつか読破した。話題作とはいえハードカバーは高価だからと敬遠していた中国産SFの『三体』も読んだ。結果、まんまとファンになってしまい、きっと第三部が出版されるときに、あらためて買い揃えることになりそう。
『ベン・ハー』『風と共に去りぬ』『サンセット大通り』と3本の古典映画もみる。名作と呼ばれるだけあって内容は大充実である。人海戦術と言っていいほどに潤沢なモブの使い方、圧倒的に配慮の行き渡った衣装や小物にセット、そしてそれらを「これでもか!」と惜しげなく披露してくれる俯瞰ショット。すべてが最高で大満足だった。平時に鑑賞するには重すぎるからこそ、年の瀬にまとめて味わえて幸福であった。
平方三角数
平方数でも三角数でもある最初の 2 つの数は 1 と 36 である。次に小さな例を見つけよう。できれば、その次の例も見つけよう。三角数でありかつ平方数でもある数を見つける有効な方法を見つけることはできるだろうか? こうした性質をもつ数は無数にあると考えられるか?
-- ジョセフ・H・シルヴァーマン 著, 鈴木治郎 訳, 『はじめての数論』練習問題 1.1 より
はじめての数論 原著第3版 発見と証明の大航海‐ピタゴラスの定理から楕円曲線まで
- 作者:Joseph H. Silverman
- 発売日: 2014/05/13
- メディア: 単行本(ソフトカバー)
数論の本を読んでいる。コンピュータの使用について、節度を守って使うよう前書きで注意されていた。プログラムで答えを知るだけでは理解したとはいえない、という読者への警句である。
考えたこと
で、冒頭の練習問題である。著者の指示を守って、コンピュータは使わずに計算するところから始めた。「次に小さな例」が 1225 であることは見つけられた。しかし結局「その次の例」には手計算では辿り着けず、プログラムを書いた。
1, 36, 1225, 41616, 1413721, 48024900, ...
こういう数列をえられた。しかし、次の数を探索する「有効な方法」については、結局プログラムに頼るのが有効、ということしかいえなかった。そして、この数列が無限に続くかどうかには答えを見つけられなかった...。
答え合わせ
wikipedia に 平方三角数 の項目があった。この呼称も初めて知った。
一般項が与えられていたから、いちおう自分でも計算してみた。当たり前だが、きちんと答えが求められた。
公式を導く手順については、手続きに組み込まれた「ペル方程式」をそもそも知らず、追いきれなかった。ただ、いまは少なくとも名前と形は認識したから、きっと近いうちに学ぶことになるだろう。いまはまだそこまでは踏み込んでいない。
とはいえ、 の形で式を立てるところまでは、初見でも不足なかったはず。そもそも愚直に計算するプログラムを作るところで思考が止まっていて、立式してみるという発想を持てていなかった。思考がコンピュータに依存している傾向の現れであって、これに対してこそ著者は警鐘を鳴らしていたのだろう。つまり、「単にプログラムで答えを知ることで満足してはならない」という警告は、他ならない僕に向けられていたのだった。
終わりに
こうした反省と自己批判の意識から、こうして記事に残しておく。これが記事公開にいたったひとつの動機付けである。
もうひとつは、第三の平方三角数 1225 がちょうど今日の日付とかちあっていることに不思議を覚えたため、なんとなくの記念の気持ちで書いている。
merry christmath!
合同式におけるモジュラ逆数 (mod_inv) の求め方
合同不定式 を について解く。
という不定方程式であれば、両辺に の逆数 をかけて としてあげればよい。
同じように合同式 においても両辺に の逆数をかければ が求められる。ただしこの文脈では、単に「 の逆数」と言うよりも「 を法とする の逆数」と呼ぶのが正確である。単に「逆数」や「逆元」とだけ名指すことも可能とはいえ、合同式における逆数は拡張された概念であるため、単に分数のような計算を想像すると誤る。 wikipedia では「モジュラ逆数」という項目にまとまっているように、特殊な手続きを必要とする逆数であると認識しなければならない。とはいえ、例えば atcoder/ac-library においても modinv.hpp
に inv()
が定義され、この計算がまとめられていることから、多くのユースケースを伴う汎用的な概念であるのだろうと思っている。
僕自身はそもそも合同式にすらさほど慣れ親しんでおらず、このあたりの概念についてはまだわからないことだらけである。だからこそ、まずはこうして学んだことを記録するところを第一歩とし、将来より高度な考察に至れるようにと願いを込めながら書く。
以下はいくつかのソースから得た情報を僕なりに解釈したものとして読んでもらえればと思う。基本的に知識を持たないところにバラバラな資料を与えて理解を構築しているから、誤った論理もあると思う。誤謬は修正できるように未来の自分自身に期待するが、読者におかれては以上のような前提をもった上で読んでほしいと思う。
問題
を について解く。
問題文の読み替え
問題文は次のように、 についての合同式に変換できる。
ここで現れる のモジュラ逆数を とおいて、さらに次のように式変換する。なお、 と は互いに素であるとする。
つまり一次不定式 を満たす任意の を求めれば、それがそのまま のモジュラ逆数 のひとつの元となることにある。「ひとつの」と書いたのは、法を とする合同式を満たす は無限に存在し、ここで求めることのできる任意の はその合同類のうちのひとつでしかないためである。とはいえ、解答する上では は任意の一元で構わず、これは問題にならない。
一次不定式の特殊解を求める
を満たす任意の を求めるには、適当な特殊解を見出せばよいのであって、一般解を導く必要はない。特殊解を求めるにあたって、例えば次の不定式を見てみよう。
これを見ると、 と直感的にひとつの解を見出すことができるだろう。そしてこのときの こそが、 における のモジュラ逆数なのである。実際、明らかに である。
一次不定式の解法に戻ると、上のように係数が小さなケースでは単純な論理や推測で x を求めることができた。しかし、大きな二つの係数の最大公約数を解とする、次のような不定式はどうであろうか。
これについて、直感ないし暗算で のペアを見出すのは困難であろう。ではどうするか?
実はここで、二つの係数 と についてユークリッドの互除法を適用すると、ひとつの の組み合わせが求められる。手続き的な計算手順としてはこうなる。
まずは係数とその剰余を、剰余が発生しなくなるまで切り下げていく。これは一般的なユークリッドの互除法である。
続いて今度は、上記のそれぞれの計算過程における剰余を根拠に、 の形を復元していく。つまり
以上より が求められる。つまり
このように解くことができる。
アルゴリズム
こうして確立することのできた手続きを、今度はアルゴリズムに実装していきたい。すでに材料は出そろっているから、あとはコードに置き換えていくだけ…と言いたいところだが、再帰を制御するやり方にまだしっくりきておらず、いまは写経したもので間に合わせておく。
なお、モジュラ逆数が存在する必要十分条件は「 と が互いに素であること」であるが、ここでは と をそれぞれ で除算してから演算を行うことで、 と は必ず互いに素 であることが保証される。
#include <bits/stdc++.h> using namespace std; using ll = long long; ll ext_gcd(ll a, ll b, ll &x, ll &y) { if (b == 0) { x = 1; y = 0; return a; } ll d = ext_gcd(b, a % b, y, x); y -= a / b * x; return d; } ll mod(ll a, ll m) { return (a % m + m) % m; } ll mod_inv(ll a, ll m) { ll x, y; ll g = ext_gcd(a, m, x, y); return mod(x, m / g); } int main() { // 3x ≡ 1 mod 7 cout << mod_inv(3, 7) << endl; // 5 // 3x ≡ gcd(3, 11) = 1 mod 11 cout << mod_inv(3, 11) << endl; // 4 // 1071x ≡ gcd(1071, 1029) = 21 mod 1029 cout << mod_inv(1071, 1029) << endl; // 25 // 2021x = gcd(2021, 1763) = 43 mod 1763 cout << mod_inv(2021, 1763) << endl; // 7 return 0; }
Designing Data-Intensive Applications を読むにさきだって
年末年始の課題図書を紹介する。といっても紹介するのは一冊だけである。一冊だけではあるが、骨太な一冊を選んでいる。積読を消化するよりも絶対これを読みたい! というホットな気持ちで手に入れた本であるため、初々しいアティチュードを記録する目的で書く。
それは Designing Data-Intensive Application というオライリー社の一冊で、『データ指向アプリケーションデザイン』のタイトルで邦訳もされている。
データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理
- 作者:Martin Kleppmann
- 発売日: 2019/07/18
- メディア: 単行本(ソフトカバー)
nand2tetris (これは今夏読んだ)と並んで teachyourselfcs.com で強くプッシュされていた一冊である。 nand2tetris は「コンピュータ・アーキテクチャ」のカテゴリの入門書であるが、他方のこちらは「分散システム」の項目に分類されている。
nand2tetris は読者が手を動かしながら学ぶことを要求していたのに対して、こちらは個別の技術の使い方に深入りすることはなく、一般性の高い理論書として読みやすい構成になっているとの評判である。なにより原著の出版は2017年であり、いわゆる正統派のテキストにしてはまだまだ若く、トピックとしても老いていない。古びていないどころか、たかだか出版後3年にして国内外で高く評価されているのは、なににも増してこのテキストの有効性をあかし立てている。
さて、内容について、目次をみるとおよその関心内容がわかる。おおよそ三部に大別されており、それぞれが注目するのは次のような項目群である。
「分散システム」をトピックとしつつ、「マイクロサービス」という語句がフィーチャーされていないのが秀逸であるとおもう。実際、上に並べたヘッドラインの文言は、古典的で骨太な概念を中心に肉付けされたものという印象を与え、賞味期限の早いバズワードには最初から目もくれないスタンスが気に入った。
序説から少しだけ抜粋する。
CPU clock speeds are barely increasing, but multi-core processors are standard, and networks are getting faster. This means parallelism is only going to increase. (xiii)
これに関連してもう一箇所。
We call an application data-intensive if data is its primary challenge--the quality of data, the complexity of data, or the speed at which it is changing--as opposed to compute-intensive, where CPU cycles are the bottleneck. (xiii-xiv)
システムの価値はデータに宿るということ、これは言われてみれば当たり前の話であるが、真理としてひろく一般に浸透しているかというと怪しいところがある。僕自身、データは大事であると心がけたり、チームに対して主張することはあっても、それはどちらかというと直感的で、理論的支援を持たないアイデア(要するに「データが壊れると後始末でひどい目をみる」という経験則)に過ぎなかった。同じ主張をするに及んでも、背景の文脈をきちんと整理できているかどうかが鍵になる。その意味でも、一冊の書籍の議論に沿いながら、知識を体系立てて入力できる機会に興奮が高まっている。
「分散システム」というと、なんだか Google の入社試験で設計させられるような、厳かで近寄りがたい構築物のような響きがある。しかし複数台構成で冗長化した単純なウェブシステムもまた、ひとつの分散システムにほかならない。つまるところ、分散システムとはいまや空気のように当たり前のアーキテクチャであり、ある意味では誰にでもできるが、正しく理解して運用するには正しい知識が必要であることに変わりはない。
サーバーレスであるとかマイクロサービスというようなバズワードはいったん傍において、そうした個々の技術や概念を要請することになった中心的な議論はなんであるのかを学ぶこと、そしていわゆるトレンド技術がそれらの問題をどういう形式において解決できるかということ。そのあたりをより具体性をもって眺められる目を獲得するつもりで、年末年始はこの本に向き合おうとおもう。
AtCoder で緑難度の問題を解けるように稽古している
競技プログラミングの練習、いわゆる「精進」を11月から再開している。
9月から10月までは、仕事探しの一環でカジュアル面談に多く参加してみたり、面接対策に時間を割いていて、しばらく間が空いていたのだった。コンテストへの参加もしばらくご無沙汰になっており、9月に茶色レーティングになってからはとくに、勉強時間も参加時間も捻出できていないのであった。
いま、仕事探しがひと段落を迎えたところで、あらためて稽古にいそしんでいる。
再開直後は感覚がやや離れている感覚があったが、これはすぐに回復させられた。もともと夏あたりの時点では「ABCで問題Cまでを30分前後で攻略できること」を目標にしていた。この方針である程度の手応えこそ得られ、結局問題Cまでしか考慮にいれない戦略であって、その先は運任せになってしまっていた。茶色難度までを効率的に攻略することに集中できても、その先の難度の問題を解く実力は必ずしも向上しなかった。つまるところ、問題Cまでをスピーディに回答できても、そのさきでさらにスコアを積めるとは限らないし、問題Cまでの時点で躓いてしまうと挽回するのがこの上なく困難になってしまうわけである。
端的にいって、次のステップに進むべきときなのだろう。そこで、最前よりはABCの問題Dにおよそ相当する、緑色難度の正答率をあげることを目標において取り組んでいる。問題集としては、 AtCoder Problems のトレーニングモード で難度 HARD の百題を先頭から順に取り組んでいる。
やってみると慣れるものなのか、わりにうまく解くことができることも多く、手応えは感じられるし、それが心地よい。6問に1問程度、うまく方針を立てられずに解説に助けを求めてしまうものの、およその精度としてはそう悪くないし、さらに練習を積むことで改善できるイメージも湧いている。一問あたりの回答時間もおよそ40-60分ほどに抑えられているから、本番のコンテストでもいくらかは使い物にできるのではないかとおもっている。
ちなみに同じトレーニングモードで中級をコンプリートしたのが8月末であった。そのときの記事がこちらになる。
これを終えた直後から2ヶ月以上中断していたわけだが、まだまだ成長できていることを感じられて非常に励みになる。さしあたり、この調子で稽古を積んでいけば、向こう2-3ヶ月中にはレーティングを緑に更新できるくらいの手応えはある。せっかく最初から C++ を選んで取り組んでいるのだから、そのアドバンテージを活かせるレベルまで早く到達したいともおもうが、これはまだまだ身の丈に合わない願いだろう。
成長を感じられることが何より嬉しいことである。そしてこの感触は、日々コツコツと稽古を積み重ねてこそ心地よく感じられる。ひとまずは目の前の問題群を順繰りに解いていくこと、そしてきちんと本番コンテストに継続的に取り組むこと、このあたりの緩いハードルを目標においてのんびりと続けていきたい。
bundle update --conservative で狙った gem だけをアップデートする
TL;DR
ミニマムなバージョンアップを行いたいときは bundle update --conservative
を使おう
gem の更新
ライブラリのバージョンアップをしようとしているとする。単に bundle update
と叩いてすべての依存を一括でバージョンアップするのは、差分を追うことがまず不可能になってしまうから、個別のライブラリをひとつずつアップグレードしていきたい。
功利的にも、例えば LRU 方式で一日ひとつずつのライブラリをアップグレードしていく、くらいのやり方であれば、それなりの頻度で更新を行っていけるし、チームに対してもよい影響を与えていけるだろう。
というわけで、個別のライブラリをターゲットに bundle update
を実施したいわけであるが、必ずしもそれで十分ではないことがある。変更範囲を最小限に bundle update
を実行するには、 --conservative
オプションを使おう、というのが今日の学びである。
サンプル
適当な Gemfile と Gemfile.lock を用意してみた。
Gemfile に記載された3つの gem のうち、 Gemfile.lock に定義されたバージョンと最新バージョンの乖離をテーブル化するとこうなる。
name | current_version | upstream_version |
---|---|---|
capistrano | 3.11.0 | 3.14.1 |
capistrano-bundler | 1.4.0 | 2.0.1 |
capistrano-rails | 1.4.0 | 1.6.1 |
# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } group :development do gem 'capistrano' gem 'capistrano-bundler' gem 'capistrano-rails' end
GEM remote: https://rubygems.org/ specs: airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) capistrano (3.11.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) capistrano-bundler (1.4.0) capistrano (~> 3.1) sshkit (~> 1.2) capistrano-rails (1.4.0) capistrano (~> 3.1) capistrano-bundler (~> 1.1) concurrent-ruby (1.1.7) i18n (1.8.5) concurrent-ruby (~> 1.0) net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) rake (13.0.1) sshkit (1.21.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) PLATFORMS ruby DEPENDENCIES capistrano capistrano-bundler capistrano-rails BUNDLED WITH 2.1.4
この設定のプロジェクトについて、 capistrano-rails
だけを最新バージョンに更新してみよう。
単に bundle update capistrano-rails
を実行する
単に bundle update capistrano-rails
と実行すると、差分としてはこうなる。
specs: airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) - capistrano (3.11.0) + capistrano (3.14.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.4.0) + capistrano-bundler (2.0.1) capistrano (~> 3.1) - sshkit (~> 1.2) - capistrano-rails (1.4.0) + capistrano-rails (1.6.1) capistrano (~> 3.1) - capistrano-bundler (~> 1.1) + capistrano-bundler (>= 1.1, < 3) concurrent-ruby (1.1.7) i18n (1.8.5) concurrent-ruby (~> 1.0)
capistrano-rails
だけを更新することを意図したつもりが、依存先の capistrano
と capistrano-bundler
も同時に更新されている。依存は解決されているため現実的に問題ないかもしれないとはいえ、例えば capistrano-bundler
のメジャーバージョンが意図せず上がってしまっているのは実に気持ちが悪い。あくまで capistrano-rails
を単体で更新するにはどうするのがよいだろう?
--conservative
オプションを与えて実行する
bundle update capistrano-rails --conservative
とオプションを追加してあげればよい。結果、次の通りにきちんと狙った gem だけが更新できており、依存先の更新を巻き込んでしまわずに済んでいる。
specs: airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) capistrano (3.11.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) capistrano-bundler (1.4.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.4.0) + capistrano-rails (1.6.1) capistrano (~> 3.1) - capistrano-bundler (~> 1.1) + capistrano-bundler (>= 1.1, < 3) concurrent-ruby (1.1.7) i18n (1.8.5) concurrent-ruby (~> 1.0)
conservative
というと必ずしもいい響きに聞こえないけれども、ことライブラリの更新というタスクについていうと、保守的に進めて悪いことなんかないと思っている。というより、デフォルトの振る舞いが --conservative
であることを期待していた身にとっては、オプションを明示しない限り変更範囲が最小とならないというのはちょっと意外であった。思わぬ形でえた学びとして、正しく Today I Learned であった。
「決断をしない」という決断をすること
アーキテクトの目的は、方針とは無関係に詳細を決めながら、方針をシステムの最も重要な要素と認識するシステムの形状を作ることである。こうすることで、詳細の決定を延期や留保することができる。1
メールマガジンの配信システムを実装している。
配信基盤には外部システムを利用している。外部システムの API を自社システムにつなぎこむだけだし、ひとまずは実験的に送信してみたいというクライアント要望から、急なスケジュールで実装を進めることになった。
配信 API の呼び出しを抽象化するクラスを設計し、リクエスト/レスポンスのデータ構造も外部システムの仕様書通りにモデリングできた。つまり、要望通りにひとまず「実験的な送信」を行える環境はそろったわけである。
実現したい振る舞いは十分実装できているから、あとはクラスを呼び出してレスポンスを検査する10行ばかりの簡単なスクリプトを作るだけ、というところで、追加要件があがってきた。いわく、クライアントが事前に用意した配信リストに沿って配信するのではなく、一定の条件でユーザーを抽出して動的に配信リストを作成し、あとでそのリストを提出してほしいのだという。
初回配信日はすでに決まってしまっており、あわてて配信履歴の記録を実装しなければ、と焦ったメンバーが、急ごしらえでテーブルを設計した。みると配列として定義されたカラムがあり、そこにメールアドレスの配列を愚直に格納する構造となっている。
Postgres の Array 型を使った設計で、確かに使い所によっては便利なデータ構造かもしれないが、少なくともこのユースケースでは不適格だという直感があった。たしかに現状求められる数百、数千の配列を適当に保存するだけなら耐えられるかもしれないが、まんがいち将来数十万、数百万と配信対象が拡大するとどうなってしまうかわからない。おそらく保存するだけなら耐えられるだろうが、「この数百万の配列対象の中に任意のメールアドレスが含まれているか調べてほしい」など検索系のユースケースが生じてしまうと、おそらく立ち行かなくなる。かと言って、実際に配信システムが本採用されて、やがて数百万単位の配信が実行されるようになるまで成長するかどうかはまだわからないし、検索要件まで含まれるようになるかどうかもわからない。まだわからない要件のために最適化を追求するのは、文字通りのオーバーエンジニアリングに陥ってしまうだろう。
言ってみればこういう二律背反が生じてしまったとみなすこともできるだろう。
- スケーラビリティをはなから捨てた設計を本番投入してしまうか
- 設計をクリーンにするために求められるスケジュールは満たせないと謝罪するか
とはいえ、この命題は誤っている。しかし(しばしば起こりうることだが)この誤った命題に基づいて議論してしまうと、きっと「技術の理解できないビジネスサイド」と「頭でっかちで顧客要望に対応できないエンジニア」が、互いに偏見をなすりつけあって、醜い闘争に至ってしまうだろう。
僕のたどり着いた答えはこうである。
- スケジュール上、入念な設計を検討する余裕はないが、かといって不十分な設計を本番投入する必要もない
- メールアドレスのリストを保存したいだけなら、一時ファイルに保存して適当なストレージにしまっておくだけでよい
これでひとまず顧客要望は実現できるし、不十分な設計を本番投入するリスクもなくなる。将来的にやはりテーブル設計が必要なら、それを検討する時間稼ぎもできる。ファイルに出力してストレージにアップロードするだけのコードなら簡単に書いて簡単に捨てられるわけで、負債化したスキーマを管理したり、本番DBでデータ移行しなければならないようなことも避けられる。まさに三方よしの打開案であって、我ながらよいアイデアを出せたと満足することができている。
あとから振り返ると、この判断はまさに『クリーン・アーキテクチャ』に書いてあるような発想であるし、そう考えると、アーキテクトとしても正しい判断だったのだろうとあらためて満足を深めている。
ソフトウェアをソフトに保つには、できるだけ長い期間、できるだけ多く選択肢を残すことである。では、「残すべき選択肢」とは何だろうか? それは、重要ではない詳細である。2
「メールマガジンを配信する」という大目標がすでに実現できているとき、「誰に配信したか記録する」というのは、重要とはいっても副次的な要件である。それは決して雑に管理すべきではないが、厳密な設計が求められる場面というわけでもない。大事なのは、そこにスケーラビリティが求められるのかどうか、誰もまだ知らないということである。
しかし単に「配信対象者を出力すること」が求められているだけだと考えると、出力装置としてデータベースを使わなければならない、というのは、これは思い込みである。単に出力先を選択するのであれば、テーブルを作るのにも ORM につなぎこむのにも工数がかかり、そしてやがて変更したり廃棄するときにもまたコストがかかる RDBMS に出力するよりも、単にファイルに書き込むだけの方が遥かに安上がりだろう。
適切なテーブル設計は、それが必要とされたときに考えればよいし、ひとまずは時間稼ぎさえできていれば、やがてより時間が詳細な要件を明らかにしてくれるはずである。アンクル・ボブはこういっている。
決定を遅延できれば、その分だけ適切に作るための情報が数多く手に入る。3
手前味噌にほかならないが、不確実な未来を織り込んで判断をくだせた自分に満足できた経験には違いないので、記念としてブログに書き残しておく。「判断をくだした」といっても「いまは判断すべきでないという判断をした」というわけで、先延ばしにしたことを体よくいっているだけではあるが、アーキテクトの態度としてはそれで間違っていないらしい。
エンジニアとして判断や意思決定が求められる場面は大小さまざまある。今回は自分なりに達成感が残せたためこうして振り返っているが、より精度を高め、より複雑な問題に対処するためには、結局のところより多くの判断に立ち合い、このようにそれを振り返る機会を持てるよう努めるべきなのだろう。
少なくともこの一件で、考え方としてはおよそ間違っていない方針を持てているらしいことがわかり、いくらか自信も深められたので、今後は試行数を増やして経験を積んでいきたいなと感じている。「アーキテクト」という職能についてあまり深く考えたことはないものの、結局のところ「いま決めるべきこと」と「いま決めなくてもいいこと」の境界を正しく認識できていれば、おのずと正しい道筋は見えてくるのかもしれない。そのあたりの感覚もこのさき研ぎ澄ませていければ幸福だと思う。