Refactoring English: Month 8

Michael Lynch

Refactoring English:8ヶ月目

原文は Michael Lynch により に公開されました。 このブログを購読する

一行サマリー

どうやら、誰もが私のフォーカスグループに参加したいわけではないようだ。

ハイライト

  • 本の早期アクセスを購入してくれた読者全員が、草稿にフィードバックをくれるわけではないことに気づいた。
  • 時間がどこに消えているのかを洗い出し、時間の浪費を減らす方法を考えた。
  • 本来300時間かけて作ったウェブアプリを、ゼロから10時間で作り直した。
  • Gleamで関数型プログラミングの学習を続けているが、ちょっとズルをしているかもしれない。

目標の成績表

毎月初めに、その月に達成したいことを宣言している。結果は次のとおりだ。

これまで話したことのない読者と少なくとも10人話す

  • 結果:7人にメールし、3人から返信があり、1人と直接会話した
  • 評価:C

数えてみるまでは、もっと返信率が悪いと思っていたが、実際には多くの読者が返信してくれている。問題は、単純にこちらからの働きかけが足りていないことだ。

おそらく一通一通に時間をかけすぎているのだろう。AIが生成したものではないと一目で分かるような、独自性のある内容にしようとして、相手のブログを1時間も読み込んでしまうのだ。

溜まっていたマーケティング施策を片付ける

  • 結果:予定の約70%を完了
  • 評価:C

ほとんどのタスクは片付いたが、1年前に収録したインタビューをまだ公開できていないので、次はそれを済ませたい。

Refactoring Englishの新章を公開する

この章の出来には満足しているが、SNSでシェアしても当たるかどうかは賭けだと分かっていた。Lobstersでは好意的に受け止められた一方で、Hacker Newsではまったく反応がなかった

Refactoring Englishの指標

指標2025年6月2025年7月変化
ユニーク訪問者数6,5748,061+1,487 (+23%)
予約販売の収益$597.24$800.04+$202.80 (+34%)
コンサルティングの収益$242.45$0.00-$242.45 (-100%)
スポンサー収益$48.25$48.25$0.00 (0%)
総収益$887.94$848.29-$39.65 (-4%)

ウェブサイトへの訪問数は増えている。人気の新着記事がなかったことを考えると良い傾向だ。既存のサンプル抜粋を読んでくれるだけで訪問数が健全に保たれているのは、前向きなサインだと捉えている。

総収益はわずかに減少したが、有料のコンサルティング案件が1件から0件になっただけなので、大きな変化ではない。むしろ予約販売が6月比で34%増えていることの方が嬉しい。

みんな、ただ本を買ったという感覚が好きなだけだったらどうしよう?

読者に連絡を取り、ビデオ通話で話していると、何度も同じフィードバックを聞く。「まだ読み始めていないんです」。

数日前に購入した人からも、数ヶ月前からアクセス権を持っている人からも、同じ言葉を聞く。

一番恐れているのは、この本が「痛み止めではなくビタミン剤」になってしまうことだ。良い文章を書くスキルは、多くの人にとって重要だが緊急ではないものと見なされている。たとえば「次のコーディング面接を突破する方法」という本なら、面接を控えてすぐに読まれるだろうが、文章力の向上はつい先延ばしにしやすい。

本を書く前に予約販売をした動機の一つは、本にお金を払う意思のある人が十分にいるかどうかを確かめることだった。実際には十分にいたし、今も買われ続けているが、それが思ったほど将来を予測するシグナルにならないのではないかと不安になっている。

もしかすると、私の本は、実際に時間をかけて取り組むことなく、文章力に投資している気分を味わうために買われているだけなのではないか? 入会だけして一度も通わない会員から収益を得ていると言われる、人気ジムチェーンのPlanet Fitnessのようなものだったらどうしよう?

私が書いているのは、本のPlanet Fitnessなのではないか?
写真:Mike MozartCC-BY-2.0ライセンスで使用

仮に本のPlanet Fitnessになれたとしても、それは持続可能ではない。本を財政的に成り立たせるには、読んで気に入ってくれた人からの口コミに賭けている。有名なブロガーが「役に立ったリソース」として私の本を挙げてくれることを期待している。開発者が「文章を上達させたい」と言ったときに、誰もがまず勧める本にしたいのだ。

顧客は私のフォーカスグループに参加したくないのかもしれない

予約販売をしたもう一つの理由は、予約購入してくれた読者は執筆過程でのフィードバックにも特に熱心になってくれるだろうと予測したからだ。

顧客でもある友人たちと話してみると、多くの人が「プロジェクトを応援するために予約購入したが、完成したら読みたいだけで、フォーカスグループに参加したり草稿にフィードバックしたりしたいわけではない」と言う。ただ完成版を読みたいだけなのだ。それはまったくもっともなことだ。

とにかく一人ひとりに声をかけ続けるべきだ

顧客に一人ひとり連絡を取り、直接話してみて分かったのは、一部の読者は非常に熱心で、ぜひフィードバックをしたいと思ってくれているということだ。見つかったのはほんの一握りだが、彼らからは素晴らしいフィードバックをもらえた。

熱心な読者を見つけるのに最も効果的だったのは、一人ひとりにメールを送る方法だったので、これからも続けていこうと思う。

時間はどこへ消えているのか?

毎年のように一度、先月に何を成し遂げたかを振り返って、「あれ、先月はこれしかやっていないの? それ以外の時間は何をしていたんだ?」と思う。

7月はまさにそんな月だった。普段どおりフルタイムで働いたのに、振り返ってみると「なぜ新章を1つしか完成させられなかったんだろう?」と思ってしまう。

そこで、月次の目標以外で時間がどこに使われているのかを振り返って考えてみることにした。

章に時間をかけすぎている

数ヶ月前、80〜90%の出来で読者に届けるのではなく、文章をこねくり回すのに時間をかけすぎていることに気づいた。そこで各章に時間制限を設け、予算内でできることを書き切るという解決策を取った。

7月に書いたメールの章は、予算を5時間に設定していたのに、実際には17.5時間かかった。その一部は意図的なものだ。目標を立てたあとで、これがウェブサイトの独立した抜粋としても機能することに気づいたからだ。抜粋として書いてから本に組み戻す方が時間がかかる。

しかし今取り組んでいる章でも同じことをしている。6時間の予算に対してすでに6.5時間使っており、読者に共有できるレベルにするまであと少なくとも3時間はかかりそうだ。これは磨き込みすぎという問題もあるが、本の中で最初にして最も重要な章に対して予算を低く見積もりすぎたという問題でもあると思う。

対策:公開用の抜粋になる章には余分に時間を確保し、執筆時間は決めた上限内に収める。

本筋以外のブログ投稿

学んだことをすぐにブログ記事として残すのが好きだが、週に20時間ブログに費やしても、書きたい記事をすべて書けるわけではない。

私のブログ記事の中には、本を読んでくれそうな人を引きつけるものもあれば、そうでないものもある。「ただ楽しいから」という記事にどれだけ時間を投資するかは、意識的に選ぶ必要がある。

先月、新しいブログ記事「Migrating a ZFS pool from RAIDZ1 to RAIDZ2」を公開した。本の役には立たないと分かっていたが、数時間で書けるだろうし、他ではあまりうまく解説されていないことを説明できると思ったのだ。

実際にはZFSの記事を書くのに7時間かかった。「お楽しみ」記事としては賢明とは言えない投資だった。特に、より多くの人に響くRefactoring Englishの章を共有できなかった月にはなおさらだ。

対策:本と関係のないブログ記事はより厳選し、本を読んでくれそうな読者を引きつける記事を優先する。

悪しきSNS習慣

退屈したり、難しい文章やソフトウェアについて真剣に考えるモチベーションが下がったりすると、ついSNSをチェックしてしまう。「ちょっとだけ見てすぐ戻る」と自分に言い聞かせるが、記事に吸い込まれてしまう。コメントを書けば、今度は返信が気になって何度もチェックしてしまう。

記事がHacker NewsやRedditで大きく話題になれば、コメント返信のために1日確保するのが常だが、ZFSの記事はそこまで話題にならなかったのに、それでも1日をコメント返信に費やしてしまった。

以前、悪しきSNS習慣を抑えるためにLeechBlock NGというブラウザ拡張を使っていたが、メモリリークしてブラウザ全体が重くなるようだったので無効にしていた。今回また試してみたところ、今のところメモリリークは見られないので、今度はうまくいくかもしれない。

対策:生産性向上系のブラウザ拡張をもう一度試し、時間泥棒サイトへのアクセスに摩擦を加える。

睡眠不足からの回復

幼い子どもがよく眠れず、妻も私も睡眠不足になっている。

睡眠の中断自体は、おそらく問題の小さな部分だ。より大きな問題は、睡眠不足を言い訳にして怠けることだ。執筆をサボったりSNSを見たりする。「昨夜はひどく眠れなかったんだから、今日は頑張らなくてもいいだろう」と考えてしまうのだ。実際には普段の80〜90%の効率で働けるのに、睡眠不足をサボる口実にしてしまっている。

対策:睡眠不足をサボる言い訳にするのをやめる。

有料の編集業務の先延ばし

Refactoring Englishの一環として提供しているサービスで、編集の仕事も請け負っている。

他人のブログ記事を編集するのは好きだが、精神的にかなり疲れる。自分の文章を編集するのも大変なのに、他人となるとさらに難しい。自分の文章なら、なぜそう直すのかを説明しなくても感覚で編集できるが、他のブロガーに編集コメントを返すときは、草稿のどこに弱点があるのか、なぜ自分の提案の方が良いのかを言葉にしなければならない。

気づけば編集の仕事を先延ばしにしてしまい、1日で終わらせられるのに何日も引きずってしまう。そして編集を先延ばしにしている間は、クライアントの仕事を自分の執筆より優先したいと思うあまり、自分の執筆も先延ばしにしてしまう。

対策:編集の先延ばしが多くの時間を奪っていることを自覚し、早めに取りかかる。

サイドプロジェクト

300時間かけたVueアプリを静的サイトジェネレーターで10時間で置き換える

2019年に、What Got Doneというビジネスを作ろうとした。チームメンバー同士で週ごとの仕事のサマリーを共有できるアプリだった。

What Got Doneは、人々が仕事の週次アップデートを共有できるウェブサイトだった。

Google在籍時、What Got Doneと同じことをするSnippetsという社内ツールがあった。それが気に入っていて、Googleを辞めた後も、一人で働くようになってからも毎週のアップデートを書き続けていた

What Got Doneの顧客は結局見つからなかったので、オープンソース化し、ここ6年間は趣味のプロジェクトとして維持してきた。

当初What Got DoneはVue、Firestore、App Engineで作ったが、今ではこれらの技術がどれも大嫌いになっている。FirestoreをSQLiteに、App Engineをfly.ioに置き換えるのに長い時間を費やしたが、Vueだけは残り、開発を不快なものにしていた。

毎週What Got Doneにアップデートを投稿するたびに、VS CodeとHugoを使ったブログ執筆ワークフローの方がずっと好みだなと思っていた。そこで、ある週末にWhat Got DoneをHugoを使ったシンプルな静的サイトとして作り直し、現在はweeks.mtlynch.ioでホストしている。

What Got DoneをHugoで生成できる静的サイトとして作り直した。

つまり6年間で、Go + Vue + SQLite + fly.ioのアプリとしてWhat Got Doneの実装と保守に約300時間を費やしたことになる。それをシンプルなMarkdownファイルとHugoによる静的サイトとして再実装するのにかかったのは、わずか10時間だった。

新しいバージョンは完全に自分用のアプリなので、gitのコミットから週次アップデートを自動入力するようなパーソナルな機能も追加できる。もちろん、フロントエンド、バックエンド、データベースで別々の技術スタックを抱えたフルスタックのウェブアプリではなく、ソース管理されたただの静的サイトなので、ホスティングも保守もバックアップも桁違いにシンプルで安価だ。

What Got Doneの提供終了

特に自分自身がもう使っていない今、What Got Doneを永遠に維持し続けたいとは思わない。

What Got Doneのアクティブユーザーはほんの一握りだが、自分が提供したものを使い始めてくれた人を見捨てるのは嫌なので、オフボーディング体験をできるだけ丁寧にしようとした:

GleamによるAIMログパーサーの進捗

高校や大学時代の古いAIMログのパーサーをいじりながら、プログラミング言語Gleamの学習を続けている。最も基本的なログは次のような形式だ:

Session Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005
[18:44] Jane: hi
[18:55] Me: hey whats up
Session Close (Jane): Mon Sep 12 18:56:02 2005

タイムスタンプのパース

6月の時点では、パーサーは基本的なレベルで動作しており、上のログから送信者とメッセージを次のように抽出できていた:

[
  Message(sender: "Jane", body: "hi"),
  Message(sender: "Me", body: "hey whats up"),
]

7月に進展があり、パーサーはタイムスタンプも理解できるようになった。セッションメタデータの日付と、メッセージに含まれる単純なHH:MM情報を組み合わせる必要があるため、少し厄介だ。これにより、ログパーサーは上のログを次のように変換できる:

[
  Message(
    timestamp: must_parse_rfc3339("2005-09-12T18:44:00-04:00"),
    sender: "Jane",
    body: "hi",
  ),
  Message(
    timestamp: must_parse_rfc3339("2005-09-12T18:55:00-04:00"),
    sender: "Me",
    body: "hey whats up",
  ),
]

字句解析と構文解析を1ステップに統合する

また、字句解析と構文解析を分けずに1パスで処理するようパーサーをシンプルにした。

当初は、ログをトークンのリストに分割してからパースする方が、より正統でエレガントだと思っていた。つまり、パーサーがSession Start (AIM - DumbAIMScreenName:Jane): Mon Sep 12 18:44:17 2005のような行を直接パースするのではなく、まず字句解析器がそれを一連のトークンに変換するようにしたかった:

[
    SessionStart,
    Word("(DumbAIMScreenName:Jane)"),
    ColonSpace,
    Word("Mon"),
    Word("Sep"),
    ...
]

しかしそうすると、トークナイザーが認識できるように文字列を["Session", " ", "Start"]のような部分文字列に分割するための隠れた最初のパスが必要になり、独自の文字列分割ロジックを実装しなければならなかった。Gleamの標準ライブラリには、文字列を部分文字列で分割しつつ区切り文字も保持する方法がないからだ。例えば改行で分割してからスペースで分割すれば文字列のリストは得られるが、区切り文字がスペースだったのか改行だったのかが分からなくなってしまう。

結局、入力を3回パースしているような感覚だった。独自の文字列分割で1回字句解析で1回、そして実際のパースで1回だ。当初は、関数型言語やテキストパーサーについて十分に知らないせいで、もっとエレガントな字句解析・構文解析の方法が見つかるだろうと思っていた。

この混乱を口実に、ついにCrafting Interpretersの紙の本を買った。これまで見た中で最も美しくデザインされたソフトウェアの本だ。

Gleamのプロジェクトが、ついにCrafting Interpretersの紙の本を買う口実をくれた。

本の字句解析の章を読んで、自分のAIMログは字句解析をするほど構造化されていないと結論づけた。すべてを1文字ずつ読み込むパーサーに統合してみたところ、その方がシンプルに感じられた。

1文字ずつパースする残念な点は、Gleamのパターンマッチングがかなり醜くなることだ。以前の実装では、次のように文字列パターンを探すことができた:

  case contents {
    ["Session", "Start", ..rest] -> tokenize_list(rest, [SessionStart, ..acc])
    ["Session", "Close", ..rest] -> tokenize_list(rest, [SessionClose, ..acc])

それが今ではより長く、ごちゃごちゃしたパターンマッチになっている:

  case state.remaining_graphemes {
    [
      "S",
      "e",
      "s",
      "s",
      "i",
      "o",
      "n",
      " ",
      "S",
      "t",
      "a",
      "r",
      "t",
      ...

Gleamにクラスを無理やり持ち込んでいないか?

パーサーを進めていくうちに、同じシグネチャの関数を何度も書いていることに気づいた:

fn parse_tokens_with_messages(
  tokens: List(Token),
  messages: List(Message),
) -> List(Message) {

多くの関数が同じ引数を取り、同じ戻り値を返し、コードを追加するにつれて引数と戻り値のリストはどんどん大きくなっていった。

そこでParseState型を作り、代わりにそれを渡し回すようにした

type ParseState {
  ParseState(
    last_timestamp: timestamp.Timestamp,
    messages: List(Message),
    remaining_graphemes: List(String),
  )
}

fn parse_graphemes(state: ParseState) -> ParseState {

しかし、これはGleamにオブジェクト指向のクラスをこっそり持ち込んでいるような気がした。というのも、これがGoならコードは次のようになるからだ:

type Parser struct {
  LastTimestamp       time.Time
  Messages            []Message
  RemainingGraphemes  []rune
}

func (p Parser) Parse() {

関数型プログラミングに詳しい皆さん、これはズルをしているのだろうか? それとも関数型言語で状態を渡し回す正しい方法なのだろうか?

まとめ

何ができたか?

学んだこと

  • Refactoring Englishの初期読者全員がフィードバックをしたいわけでなくても構わない。読者に声をかけ続け、より積極的に関わりたいと思ってくれる数少ない人を見つければいい。
  • 時間の無駄遣いのパターンを評価し、対策を考えることは常に有用だ。

来月の目標

  • これまで話したことのない読者20人にパーソナライズしたメールを書く。
  • Refactoring Englishの新章を公開する。
  • 残りのマーケティングタスクを完了する。

お願い

文章力を向上させたいと考えている開発者の方、あるいはそういう人をご存知の方は、ぜひご連絡ください

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント