Refactoring English: Month 8

Michael Lynch

Refactoring English:8ヶ月目

一言で言うと

誰もが私のフォーカスグループに参加したいわけではないのかもしれません。

ハイライト

  • 早期アクセスで本を購入してくれた読者の全員が、草稿にフィードバックをしたいわけではないことに気づきました。
  • 自分の時間がどこに消えているのかを洗い出し、時間のロスを減らす方法を考えました。
  • もともと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のようなものを作っているとしたら?
写真 by Mike MozartCC BY 2.0ライセンスのもとで利用

たとえ本の世界のPlanet Fitnessになれたとしても、それでは続きません。本を経済的に成り立たせるには、読んで気に入ってくれた人たちの口コミに懸かっています。人気のブロガーが「役に立った資料」として私の本を挙げてくれることを期待しています。開発者が「文章をうまくなりたい」と言ったときに、誰もがまず薦める本にしたいのです。

ひょっとすると、お客さんはフォーカスグループに参加したいわけではないのかもしれない

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

友人でもあるお客さんたちと話してみると、多くは「プロジェクトを応援したくて事前購入したので、完成したら読みたい。でも、フォーカスグループに参加したり、草稿にフィードバックしたりしたいわけではない」という反応でした。完成した本を読みたいだけ、というのは至極当然のことです。

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

お客さんに一人ずつ連絡を取り、直接話してみて分かったのは、一部の読者は非常に熱心にフィードバックをしてくれるということです。まだ数人しか見つけられていませんが、いただいたフィードバックはどれも素晴らしいものでした。

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

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

毎年のように一度、先月の成果を振り返って「あれ、今月はこれしかできていないの? ほかの時間は何をしていたんだろう?」と考えてしまいます。

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

そこで、月次の目標以外で自分の時間がどこに消えているのか、振り返って考えてみます。

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

数ヶ月前、言葉を磨くことに時間をかけすぎていることに気づき、文章が80〜90%の出来になった時点で読者に届けるべきだと考えました。そこで、各章に時間制限を設け、その予算内でできることを書き切るようにしました。

7月に書いたメールの章では、予算を5時間に設定していましたが、実際には17.5時間かけてしまいました。途中から、この章はウェブサイトで単独の抜粋記事としても公開できることに気づいたので、意図的に時間をかけた部分もあります。抜粋として書いてから本編に組み戻す作業には、どうしても時間がかかります。

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

解決策: 公開用の抜粋になる章は多めに時間を見積もり、執筆時間は決めた制限内に収めるようにします。

本筋とは関係ないブログ記事

学んだことを、学んですぐのうちにブログ記事として残すのが好きなのですが、書きたい記事は週に20時間かけても書ききれないほどあります。

ブログ記事の中には、本の読者にも響くものもあれば、そうでないものもあります。「ただ楽しいから書く」記事にどれだけ時間を割くかは、意識的に決める必要があります。

先月は、「ZFSプールをRAIDZ1からRAIDZ2へ移行する」というブログ記事を公開しました。本の役には立たないと分かっていましたが、数時間で書き終えられるだろうし、ほかではあまりうまく解説されていないことを説明できるだろうと思ったのです。

しかし実際には、このZFSの記事を書くのに7時間かかってしまいました。「お楽しみ」記事としては、やりすぎな投資でした。とくに、その月にRefactoring Englishの章のような多くの人に響く記事を共有できていなかった中ではなおさらです。

解決策: 本筋と関係ないブログ記事はより厳選し、本の読者にも届く記事を優先します。

よくないSNSの習慣

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

記事がHacker NewsやRedditで大きく取り上げられたときは、コメントへの返信のために1日を確保するのが常です。ZFSの記事はそこまで話題になりませんでしたが、それでもコメントへの返信に1日を費やしてしまいました。

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

解決策: 生産性向上系のブラウザー拡張をもう一度試して、つい見てしまうサイトに摩擦を加えます。

睡眠不足からの回復

幼い子どもがよく眠れず、私も妻も寝不足が続いています。

睡眠の中断そのものよりも、むしろ大きな問題は、その中断を言い訳にして怠けてしまうことです。執筆の時間をサボったり、SNSを見たりしてしまいます。「昨晩あんなに眠れなかったんだから、今日は頑張らなくてもいいだろう」と考えてしまうのです。実際には普段の80〜90%の効率で作業できるはずなのに、睡眠不足をサボる口実にしてしまっています。

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

有料の編集作業を先延ばしにしている

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

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

気づけば、その編集作業を先延ばしにしてしまいます。本来なら1日で終えられる作業でも、だらだらと何日もかけてしまうのです。そして編集作業を先延ばしにしている間は、自分の執筆も先延ばしにしてしまいます。お客さんの仕事を自分の仕事より優先したいと思うからです。

解決策: 編集作業の先延ばしが多くの時間を奪っていることを自覚し、もっと早く取りかかるようにします。

サイドプロジェクト

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

2019年に、What Got Doneというビジネスを作ろうとしました。チームのメンバーが互いに週次の成果を共有できるアプリでした。

What Got Doneは、週次の進捗をみんなで共有できるウェブサイトでした。

Googleにいた頃、社内に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回パースしているような感覚でした。一度目は自作の文字列分割で二度目は字句解析で三度目は実際のパーサーでです。当初は、関数型言語やテキストパーサーについてもっと学べば、字句解析と構文解析をもっとエレガントに行う方法が見つかるのだろうと思っていました。

この混乱を口実に、ようやく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の新しい章を公開します。
  • 残りのマーケティングタスクを完了させます。

お願い

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

原文は Michael Lynch により に公開されました。

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