<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Jiachen Yu</title>
    <link>https://www.yujiachen.com/blog/</link>
    <description>Jiachen Yu</description>
    <language>en</language>
    <lastBuildDate>Sun, 08 Feb 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.yujiachen.com/rss-en.xml" rel="self" type="application/rss+xml" />
  <item>
    <title>Thoughts on the Video Quality Evaluation</title>
    <link>https://www.yujiachen.com/thoughts-on-the-video-quality-evaluation/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/thoughts-on-the-video-quality-evaluation/</guid>
    <pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p>Last year, I led the team behind OpusClip’s LLM-as-a-Judge system. Since we already published a post about <a href="https://medium.com/opus-engineering/a-scalable-llm-as-a-judge-framework-for-video-quality-evaluation-74612034bd1e">video quality evaluation</a>, I can share a short recap here. I am currently working on a separate evaluation track for AgentOpus, so I will close with a related question that I find personally interesting.</p>
<h2 id="what-we-did-last-year">What We Did Last Year</h2>
<p>You can check <a href="https://medium.com/opus-engineering/a-scalable-llm-as-a-judge-framework-for-video-quality-evaluation-74612034bd1e">the blog</a> for full details; below is a short summary.</p>
<p>Our goal was to build a video quality judge system that can score video quality across different rubrics.</p>
<h3 id="building-the-first-rubric">Building the First Rubric</h3>
<p>The first step was data collection. We collected 300 samples from both internal and external sources. With a target agreement rate of 80% and a 95% confidence level (±5% margin of error), the minimum required sample size is about 246. <sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
<p>At the same time, we defined rubrics for the judge. We set up a number of rubrics under the hook, content, visual, and audio categories. The first rubric we annotated manually was hook engagement. I asked everyone on the team, as well as external experts, to annotate videos. It was important that human annotators achieve an 80% agreement rate first.</p>
<p>The annotation result for each video is simple: Does the video meet the rubric? The result is 0 (does not meet), 1 (partially meets), or 2 (meets). As the annotation progressed, we needed to rebalance the dataset to ensure the number of 0/1/2 samples remained roughly equal.</p>
<p>Once we had the annotation results, we tested different prompts on Gemini 2.5 Pro (and later Gemini 3 Pro). The prompt that aligned most with human annotations was selected as the “judge” for the current rubric.</p>
<h3 id="scaling-to-more-rubrics">Scaling to More Rubrics</h3>
<p>Once we knew how to build the judge for one rubric, scaling to others was straightforward. We sped up the annotation process via LLM pre-annotation, reducing the number of annotators needed for high-agreement samples. We also built an internal agent to iterate on prompts across different rubrics automatically.</p>
<p>In the end, we had an LLM-as-a-Judge system that gave quality scores for videos. A video’s quality score equals the sum of its per-rubric results (each 0, 1, or 2), yielding a score range of 0 to 2N for N rubrics.</p>
<p>We also cross-validated the judge by testing it on new samples and calculating the correlation between export rate and judge score. The results show that a higher judge score indicates a higher export rate.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/thoughts-on-the-video-quality-evaluation/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/thoughts-on-the-video-quality-evaluation/en/image_1.png" alt="" class="article-image" width="1080" height="602" />
</picture>
</p>
<p><em>Figure 1. Judge score vs. export rate on a holdout set. Each point represents a score bucket. The trend is monotonic: higher judge scores are associated with higher export rates.</em></p>
<h3 id="results">Results</h3>
<p>We used this system to curate clipping strategies produced by another system, and it improved our online export rate. The system also worked effectively on our B2B customer clips, increasing business metrics for other teams.</p>
<p>These results gave me confidence in rubric-based quality judging. At the same time, they raised a question I find personally interesting: can an evaluation infer a plausible execution path from the final result? I am still exploring this direction, but it seems promising for making agent evaluation more interpretable.</p>
<h2 id="what-am-i-curious-about-this-year">What Am I Curious About This Year?</h2>
<p>The core question is: can an evaluation do more than assign a score — can it also explain <em>why</em> the agent failed?</p>
<p>This matters for two reasons:</p>
<ol>
<li>If the agent is too weak (in other words, useless), the evaluation becomes meaningless because we may not have an effective way to use it to improve the agent itself.</li>
<li>On the other hand, if we can infer an execution path from a result, then we can transfer that knowledge into the agent. As a result, the agent will have the same knowledge as the evaluator.</li>
</ol>
<p>OpenAI has <a href="https://github.com/openai/skills/tree/main/skills/.experimental/codex-readiness-integration-test">a very interesting approach</a>: they ask Codex CLI to evaluate its own performance. What I learned from this is that we should try to put the evaluator and the agent at the same level, so that when the evaluator improves the agent, the agent can also improve the evaluator.</p>
<p>With that, we can build a data flywheel — a self-improving bootstrap for the agent. Then we can iterate on its performance quickly by feeding more data and more cases into the system.</p>
<p>I am still early in exploring this direction, but the underlying question feels worth asking: if your evaluator could hand the agent a concrete diagnosis instead of just a score, how would that change the way you build and iterate on agent systems?</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>n = z² · p · (1 − p) / e², with z = 1.96, p = 0.8, e = 0.05. This assumes approximately independent samples and uses raw agreement as the primary metric. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
  </item>
  <item>
    <title>How Engineers and PMs Ship LLM Features Together</title>
    <link>https://www.yujiachen.com/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/</guid>
    <pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<blockquote>
<p>Originally published on Medium via <strong>OpusClip Engineering</strong>; reposted here on my blog.<br>
Original: <a href="https://medium.com/opus-engineering/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together-147a36ab4089">Bridging the Gap: How Engineers and PMs Ship Winning LLM Features Together</a></p>
</blockquote>
<h1 id="how-engineers-and-pms-ship-winning-llm-features-faster-3-technical-decisions">How Engineers and PMs Ship Winning LLM Features Faster: 3 Technical Decisions</h1>
<p><strong>TL;DR:</strong></p>
<ul>
<li><strong>Prompts belong in configs, not code:</strong> Enable rapid iteration without deployments</li>
<li><strong>Variables go last:</strong> Save 90% on costs through KV-cache optimization</li>
<li><strong>Separate semantics from schema</strong>: PMs own meaning, engineers own structure</li>
</ul>
<p>The best LLM features aren’t built in silos. When engineers and PMs at <strong>OpusClip</strong> started collaborating on prompt architecture, <strong>iteration cycles dropped from days to minutes</strong>, <strong>API costs fell by 10x</strong>, and <strong>prompts in production are more reliable</strong>. Here are the three technical decisions that made the biggest difference.</p>
<h3 id="pm-to-prompt-distance">PM to Prompt Distance</h3>
<p><strong>What it is:</strong> The number of hops — and the amount of interpretation — between your product requirement and the exact text/settings the model actually receives.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_1.png" alt="" class="article-image" width="1024" height="1572" />
</picture>
</p>
<p><a href="https://www.linkedin.com/posts/manus-im_product-to-prompt-distance-is-fast-becoming-activity-7349444736048320512-zjjS?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAACdAQCkBye_l2SHKxHq1FoCJdVQJdLz5Boc">Tao Zhang first defines this metric in Manus.</a></p>
<p>Here’s what typically happens:</p>
<ol>
<li><strong>PM writes</strong>: “The assistant should be professional but approachable”</li>
<li><strong>Spec translates</strong>: “Use formal language with occasional casual phrases”</li>
<li><strong>Engineer implements</strong>: “You are a professional assistant. Maintain formal tone while being friendly.”</li>
<li><strong>Runtime adds context</strong>: “You are a professional assistant. Maintain formal tone while being friendly. Current user: {user_name}. Previous context: {history}”</li>
</ol>
<p>Each step adds interpretation and delay.</p>
<p><strong>Why it matters:</strong></p>
<ul>
<li><strong>Speed of iteration.</strong> Fewer hops = faster experiments.</li>
<li><strong>Quality &amp; intent fidelity.</strong> Each handoff (PM → spec → UI copy → template → runtime prompt) adds interpretation risk.</li>
<li><strong>Observability.</strong> When prompts are hidden in code, it’s hard to debug.</li>
</ul>
<p><strong>How to reduce distance:</strong> Ask your engineer teammates to put prompts on dynamic configs, or a prompt management platform. Don’t put prompts on code.</p>
<h3 id="kv-cache">KV‑Cache</h3>
<p><strong>What it is:</strong> KV-Cache (Key-Value Cache) is like a smart notebook that lets LLMs remember their previous calculations. Without it, every time your chatbot generates a new word, it would need to re-read the entire conversation from scratch.</p>
<p>Imagine you’re having a conversation with a chatbot. Each time you send a new message, the chatbot needs to understand the entire conversation history to provide a relevant response. Without a KV-Cache, the model would have to re-read and re-process the whole conversation from the beginning every single time it generates a new word. This is incredibly inefficient and slow, leading to a frustratingly laggy user experience, especially with longer conversations or longer prompts.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_2.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_2.png" alt="" class="article-image" width="1024" height="276" />
</picture>
</p>
<p>KV-Cache has two major benefits:</p>
<ul>
<li><strong>Faster Response Times (Same response qualities but faster speeds):</strong> By avoiding redundant computations, the KV-Cache allows the LLM to generate responses much more quickly. For users, this means less waiting and a more fluid, natural interaction with your AI feature.</li>
<li><strong>Reduced Computational Costs (Same money but more users):</strong> Re-processing less data means using less computational power. This directly translates to lower operational costs for running your LLM, making your product more scalable.</li>
</ul>
<p><strong>Why PMs should care:</strong> modern LLM providers now charge 10x less for cached tokens than new tokens.</p>
<blockquote>
<p>For example, GPT-5 charges $1.25 for 1M input tokens, but it only charges $0.125 for 1M cached tokens.</p>
</blockquote>
<p><strong>How to leverage KV-cache:</strong> Always put variables at the end of your prompts.</p>
<p>❌ <strong>Bad prompt structure</strong> (minimal caching):</p>
<pre class="hljs"><code>User: {{user_name}}  
Question: {{user_question}}  
Conversation history: {{chat_history}}  
  
You are a customer support agent for TechCorp.  
Guidelines:  
- Be empathetic and professional  
- Check our knowledge base before answering  
- Escalate billing issues to human agents  
- Always verify account details first
</code></pre>
<p>✅ <strong>Good prompt structure</strong> (maximum caching):</p>
<pre class="hljs"><code>You are a customer support agent for TechCorp.  
Guidelines:  
- Be empathetic and professional    
- Check our knowledge base before answering  
- Escalate billing issues to human agents  
- Always verify account details first  
  
User: {{user_name}}  
Question: {{user_question}}  
Conversation history: {{chat_history}}
</code></pre>
<p>The static instructions get cached across all requests, while only the dynamic user content changes. For a support bot handling 10,000 daily conversations, this restructuring alone could save $200–300 or more per day.</p>
<h3 id="structured-output-response-schema">Structured Output / Response Schema</h3>
<p><strong>What it is:</strong> Instead of returning free‑form text, the model returns <strong>well‑formed JSON</strong>. You define a schema — fields, types, enums — and the model adheres to it.</p>
<p><strong>This is counterintuitive for many PMs: you don’t need to describe the format in your prompt at all.</strong></p>
<ul>
<li><strong>Structured output is an API-level feature</strong>, not a prompt trick. You turn it on <strong>in code</strong> by registering a schema/response format; then the runtime enforces it.</li>
<li><strong>Once it’s configured, don’t re-specify the format in the prompt.</strong> Tell the model <em>what</em> to fill, not <em>how to format</em> it.</li>
<li><strong>Consumer chatbot UIs (e.g., ChatGPT-style apps) generally don’t expose this.</strong> They’re optimized for human-readable text, not machine-parsable payloads.</li>
</ul>
<p><strong>We have to show some code here to explain it:</strong></p>
<pre class="hljs"><code>import OpenAI from &quot;openai&quot;  
import { z } from &quot;zod&quot;  
import { zodTextFormat } from &quot;openai/helpers/zod&quot;  
  
// 1) Your schema stays in code (Zod)  
const RelevancyItem = z.object({  
  clipId: z.string(),  
  relevant: z.boolean(),  
  relevantReason: z.string(),  
  advertisement: z.boolean(),  
})  
const RelevancyArray = z.array(RelevancyItem)  
type Relevancy = z.infer&lt;typeof RelevancyArray&gt;  
  
// 2) Build the semantics-only prompt (no format instructions)  
const prompt = promptTemplate.join(&quot;\n&quot;).replace(INPUT_REPLACE, input)  
  
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })  
  
// 3) Call Responses API and let the helper parse &amp; validate for you  
const response = await openai.responses.parse({  
  model: &quot;gpt-4o-mini&quot;,  
  input: [  
    { role: &quot;system&quot;, content: &quot;Judge search-trend relevancy using the definitions provided.&quot; },  
    { role: &quot;user&quot;, content: prompt },  
  ],  
  text: {  
    // zodTextFormat drives structured output and runtime validation  
    format: zodTextFormat(RelevancyArray, &quot;relevancy_results&quot;),  
  },  
})  
  
// 4) Already parsed &amp; validated:  
const results: Relevancy = response.output_parsed  
  
// 5) Same post-filtering  
const relatedResults = results.filter(  
  (r) =&gt; r.relevant &amp;&amp; r.evergreen &amp;&amp; !r.advertisement  
)
</code></pre>
<p><strong>⚠️ Conflict Notice (VERY IMPORTANT)</strong></p>
<p>Once a <strong>schema/responseFormat</strong> is set in code, <strong>do not describe output formatting in the prompt</strong>. Mixing prompt-format rules with the engineer-defined schema creates two sources of truth and measurably hurts reliability:</p>
<ul>
<li><strong>Validation failures &amp; retries</strong> → higher latency/cost; occasional data loss if coerced.</li>
<li><strong>Instruction dilution</strong> → worse task quality (model juggles format vs. content).</li>
<li><strong>Downstream breakage</strong> → typed logic fails on “pretty” but invalid payloads.</li>
</ul>
<p><strong>Please remember:</strong> <em>Schema owns format; prompt owns semantics.</em> Keep prompts about what each field should contain (definitions, decision rules), <strong>not</strong> how to format.</p>
<p>❌ <strong>Your prompt should NOT say</strong>:</p>
<pre class="hljs"><code>Analyze these search results and return a JSON object with:  
- videoId: the video identifier  
- isRelevant: boolean indicating if it matches  
- relevanceReason: explanation string  
- isPaid: boolean for sponsored content  
Format as valid JSON with these exact field names.
</code></pre>
<p>✅ <strong>Your prompt SHOULD say</strong>:</p>
<pre class="hljs"><code>Analyze these search results for relevance to the user's query.  
  
For relevance assessment:  
- Consider semantic match, not just keyword overlap  
- Educational content is preferred over entertainment  
- Recent content (last 6 months) is more relevant  
  
For paid content detection:  
- Look for &quot;Sponsored&quot;, &quot;Ad&quot;, or &quot;#ad&quot; markers  
- Check if the channel name includes &quot;Official&quot; or &quot;Brand&quot;  
  
Provide clear reasoning for why content is or isn't relevant.
</code></pre>
<p>Notice: all semantics, zero formatting. The schema handles structure; your prompt handles meaning.</p>
<p>One worth-to-mention common pitfall that PMs to avoid: changing field names in prompts without coordinating with engineering. If your engineer’s schema says videoId but your prompt mentions video_id or clip_id, you’re creating confusion that degrades performance.</p>
<h3 id="putting-it-all-together">Putting It All Together</h3>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_3.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/bridging-the-gap-how-engineers-and-pms-ship-winning-llm-features-together/en/image_3.png" alt="" class="article-image" width="1024" height="512" />
</picture>
</p>
<ol>
<li><strong>Low pm to prompt distance</strong> lets you iterate quickly on prompt improvements</li>
<li><strong>Optimized KV-cache</strong> makes those iterations cheaper to test at scale</li>
<li><strong>Proper structured output</strong> ensures reliable, parseable responses regardless of prompt changes</li>
</ol>
<h3 id="next-steps">Next Steps</h3>
<ol>
<li><strong>Audit your current setup</strong>: Where do your prompts live? Are variables at the end? Are you mixing format and content instructions?</li>
<li><strong>Start one conversation</strong>: Pick the highest-impact improvement and discuss with your engineering team. Most engineers appreciate PMs who understand these constraints.</li>
<li><strong>Measure the impact</strong>: Track iteration speed, cost per request, and error rates before and after changes.</li>
</ol>
<p><strong>Questions?</strong> Drop them in the comments. Our team loves talking about efficient engineering &amp; pm collaborations.</p>
<h3 id="join-our-team">Join Our Team</h3>
<p>If these practical takeaways resonate with you and you’re passionate about solving complex technical challenges at scale, we’d love to hear from you.</p>
<p>Check out our open positions: <a href="https://www.opus.pro/careers">opus.pro/careers</a></p>
]]></content:encoded>
  </item>
  <item>
    <title>How to 1-on-1</title>
    <link>https://www.yujiachen.com/how-to-1-on-1/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/how-to-1-on-1/</guid>
    <pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate>
    <category>Essays</category>
    <content:encoded><![CDATA[<h1 id="why-i-need-to-do-this">Why I need to do this?</h1>
<p>It is your responsibility to drive your own career growth
No one knows what you want unless you speak it with others, so our company forces your manager to help you with that by weekly 1-on-1s.</p>
<p>A more practical reason is that your relationship with your manager largely determines your work experience.</p>
<h1 id="mindsets">Mindsets</h1>
<h2 id="1-your-manager-is-one-of-your-resources">1. Your manager is one of your resources</h2>
<table>
<thead>
<tr>
<th>I’m one of my manager’s resources</th>
<th>My manager is one of my resources</th>
</tr>
</thead>
<tbody>
<tr>
<td>Don’t care about the company’s operations.</td>
<td>Discuss your dissatisfaction (if you have) with the company’s performance in this 2-month cycle and inquired about how cofounders or teams plan to improve the current situation.</td>
</tr>
<tr>
<td>Execute your tasks assigned by your manager without asking.</td>
<td>Asked for an explanation of the task or project from your manager.If you think a project doesn’t have a good impact, just tell your manager why and ask him to stop this project.</td>
</tr>
<tr>
<td>Don’t talk about promotion or performance. Waiting for the manager to invite you for promotion.</td>
<td>Inquired about the standard for promotion and how to achieve it.Asked which preparations are needed for the next calibration.</td>
</tr>
<tr>
<td>The manager is right.</td>
<td>The manager may be wrong.</td>
</tr>
</tbody>
</table>
<h2 id="2-talk-about-things-you-don-t-know-about-each-other">2. Talk about things you don’t know about each other</h2>
<table>
<thead>
<tr>
<th>Talk about things you and your manager already know</th>
<th>Talk about things my manager doesn’t know or I need to know</th>
</tr>
</thead>
<tbody>
<tr>
<td>Report work again after you just report them on daily standup.</td>
<td>Talk about the next thing or project you are planning to do and ask your manager to add it to the next 2-month ORKs. If you find there are too many tasks to deliver on time, please make sure your manager knows that you will miss the timeline.</td>
</tr>
<tr>
<td>Saying everything is fine, even if you receive some bad feedback about your team.</td>
<td>Tell your manager valuable feedback about your team if you received them from others.</td>
</tr>
<tr>
<td>Talked about impacts of a project that you just wrote in a PSA.</td>
<td>Review the project you just launched, ask for feedback from your manager.</td>
</tr>
<tr>
<td>Don’t talk too much.</td>
<td>Talk as much as you can.</td>
</tr>
</tbody>
</table>
<h1 id="other-tips">Other tips</h1>
<p><strong>Whenever you have some thoughts that you want to talk about in the next 1-on-1 meeting, send it to the meeting group and pin it.</strong></p>
<p><strong>Cancel a 1-on-1 meeting if you feel it’s unnecessary.</strong></p>
<p><strong>Don’t overuse 1-on-1. If a topic is public or needs to be public, talk about it in a public group</strong>.</p>
<p><strong>Don’t “manage up”</strong></p>
<blockquote>
<p>Another typical loophole of “manage up” is that it often incurs unnecessary one-on-one meetings. You may not agree with me on this but that’s alright. So what do I mean by “unnecessary”? Put it this way — if someone can explicate an issue with a text message of around 100 characters, they wouldn’t need to set up a one-on-one with me. In fact, I would rather they post it in the group chat so that I wouldn’t have to pass on the information. But the reality is that I’m quite often dragged into one-on-one meetings on such issues.
Why do people prefer one-on-one dialogues in person? I’ve found two main reasons. Firstly, a one-on-one dialogue creates information asymmetry that helps to “manage up” as it prevents potential critics from a third person. But if you post the same issue in a group chat, it’s very easy to see different opinions. Actually I don’t think most issues require one-on-one communication so long as it’s not classified. Secondly, one-on-one dialogues in person or over phone calls help to navigate negotiation strategies constantly. For example, if the person observes that you are pissed off by what he/she said earlier, they tend to pull back or tone down a bit.
There are many other cases of “manage up”. I used to hear this — a PR employee probed into the WeChat and Toutiao channels that his/her boss subscribe to and posted PR contents on those channels for the boss to notice.
I’m not sure how you feel about this. But I would feel that CEOs and leaders are quite troubled if they were to soak in an “manage up” environment where information is “retouched”. Suppose the information that directs to you is specially angled, or what we call “SEO-ed (search engine-optimized) information” at ByteDance, you will have to verify the information via other channels. That would be very counter-efficient.</p>
<p>From: &lt;<a href="https://sourcecodecap.com/code-class-post/bytedances-zhang-yiming-bring-outside-in-and-avoid-managing-up-how-to-protect-the-comp/">ByteDance’s Zhang Yiming: “Bring Outside in” and Avoid “Managing up”— How to Protect the Company from Diseconomies of Scale</a>&gt;</p>
</blockquote>
]]></content:encoded>
  </item>
  <item>
    <title>Simple vs Easy</title>
    <link>https://www.yujiachen.com/simple-vs-easy/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/simple-vs-easy/</guid>
    <pubDate>Sun, 19 Jan 2025 00:00:00 GMT</pubDate>
    <category>Essays</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<ul>
<li>
<p>An example of “simple” is a manual juicer—its principle is straightforward and easy to understand. An example of “easy” is an electric juicer—it’s very easy to use, but its internal mechanism is complex.</p>
</li>
<li>
<p>“Simple” in this context means something you can depend on because it’s straightforward. “Easy” means something that appears effortless from the outside, but is actually full of complex structures and uncertainty inside.</p>
</li>
<li>
<p>Another example of “simple”: Duan Yongping figured out that Apple was a good stock, then held it for over a decade without selling. During this time, he didn’t need to do anything else—he could play golf every day and still get 20x returns. Because Apple has high profits, a moat, and a user base that keeps expanding, the stock price will definitely rise in the long run. “Simple” means once you think it through, you don’t need to worry about anything else. When he explained his holding rationale to Warren Buffett, it took just one or two sentences. After that, there was nothing more to discuss about Apple.</p>
</li>
<li>
<p>A corresponding example of “easy”: Yesterday, a certain cryptocurrency skyrocketed. If someone had gone all-in or even leveraged before the surge, they could have made thousands or even tens of thousands times their investment overnight. Sounds easy, but the complexity in execution is enormous—you could easily get liquidated. And the uncertainty of chasing trends is huge. Even if someone came to you right now and told you exactly what to do to make money, how many people would believe them?</p>
</li>
<li>
<p>As for myself: I prefer “simple” things. Simple things are simple because they follow principles and don’t change based on the number of participants or their attitudes. I don’t like “easy” things—they’re hard to replicate, and even if you can, they’re far less reliable than “simple” things. Complex systems change based on the number and attitudes of participants, consuming time and energy with no guaranteed returns.</p>
</li>
</ul>
]]></content:encoded>
  </item>
  <item>
    <title>Learnings from ByteDance</title>
    <link>https://www.yujiachen.com/learnings-from-bytedance/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/learnings-from-bytedance/</guid>
    <pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate>
    <category>Essays</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<ol>
<li><strong>No boundaries</strong>: The market is a company’s boundary; demand is a product’s boundary; impact is work’s boundary.</li>
<li><strong>Efficiency above all</strong>: The core of competition is ROI, not cost level.</li>
<li><strong>Look forward</strong>: Competition only drives up costs. Revenue doesn’t come from competitors—it comes from users.</li>
<li><strong>Escape gravity</strong>: Every company has a sphere of competence. If another company’s gravitational pull is stronger than yours, don’t operate within that sphere.</li>
<li><strong>Supply and demand are the core of markets</strong>: The happiest situation in business is when one group needs your resources, while another group is willing to provide other high-value resources to exchange for capabilities from you. That’s why multi-sided platforms beat two-sided ones, and two-sided platforms beat tools. A tool is merely a localized means to an end.</li>
<li><strong>Focus on facts</strong>: Fantasy doesn’t help reshape reality. Reality has evolved based on the laws of physics—it is necessarily rational.</li>
</ol>
]]></content:encoded>
  </item>
  <item>
    <title>New Things vs Old Things</title>
    <link>https://www.yujiachen.com/new-things-vs-old-things/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/new-things-vs-old-things/</guid>
    <pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate>
    <category>Essays</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>It all started with a question I was asked today.</p>
<hr>
<h1 id="1">1</h1>
<p>Today, a recruiter at my company came to ask me: “Do you think OpenAI is a phenomenal success or just a coincidence (with low technical barriers)?” A candidate had told her that OpenAI’s technical barriers aren’t that high—they were just the first to take the plunge. Looking at it now, OpenAI’s progress has indeed slowed down, and from what I know, other companies are catching up fast, even surpassing OpenAI in some models. But in my mind, OpenAI remains special. I believe OpenAI’s value lies not in its commercial success, but in its technology. Besides, while OpenAI may not be extremely successful commercially, it certainly can’t be called a failure.</p>
<p>Those who followed OpenAI early on would know that GPT and GPT-2 were seen as mere “toys” by outsiders, while Google’s BERT was considered the strongest language model at the time. When GPT-3 came out, people found it interesting but still thought it was useless. It wasn’t until GPT-3.5 that people saw the possibility of AGI. Before GPT-3.5, OpenAI had been persisting with the GPT architecture for over four years. Even with substantial funding from Musk, Altman, and others, it was incredibly difficult for a startup to keep investing in a direction that might not succeed.</p>
<p>In my view, OpenAI’s persistence is what truly differentiates it from other companies. Other companies build large language models because of commercial necessity; OpenAI builds them because it believes this path can create a future with AGI. This is also why what other companies do feels “meaningless”—after OpenAI released GPT, if a newcomer wants to contribute to humanity, they shouldn’t choose to work on large language models. Unless you have insights vastly different from the mainstream, this field has already been thoroughly explored by OpenAI. A newcomer should independently think about what other important fields are being overlooked by the market and remain unexplored. Those are where new value and technology will be created. In other words, <strong>as a newcomer, you should prioritize doing new things rather than rushing to do old things</strong>.</p>
<h1 id="2">2</h1>
<p>Some might disagree with the above—just do whatever makes money, right? But I believe that markets without competition are where the profits are. In the crowded LLM space, unless a clear winner emerges, everyone ends up working at cost.</p>
<p>Take China’s “AI Four Dragons” (SenseTime, Megvii, Yitu, CloudWalk) as an example. Their technology seemed difficult, but they were all highly homogeneous. Eventually, after using their solutions for a while, big companies turned to building in-house, leaving the Four Dragons to pursue ToB and ToG routes. Of course, computer vision also has the problem of limited commercial value, but that’s another story.</p>
<p>Another example is Xiaomi. Xiaomi always enters markets after demand has been validated, which is why they make “heartfelt” high-value products. There’s nothing wrong with this business model, and Xiaomi is a company with great social responsibility. But the reality is that Xiaomi’s hardware profit margins are extremely low, leaving little room for innovation.</p>
<p>So circling back—is building LLMs profitable? Personally, I think it’s a money-losing business. The only exception seems to be ByteDance’s Doubao, because ChatGPT can’t serve China, and Chinese users need a good LLM app.</p>
<h1 id="3">3</h1>
<p>Compared to other companies, what the big tech companies are doing with LLMs is actually the “old thing” that OpenAI already did. What OpenAI did with GPT years ago, with multimodal in the past two years, and recently with post-training—those are the “new things.” I personally prefer doing “new things,” for the following reasons.</p>
<p><strong>First, doing new things is meaningful.</strong> OpenAI’s years of persistence on AGI led to today’s AI application explosion. New technology creates new demands and resources, while old things mostly just redistribute existing resources.</p>
<p><strong>Second, maximizing returns.</strong> If a new thing succeeds, you become the industry leader with substantial commercial profits, and abundant capital allows you to do many things. In contrast, profits from old things are always limited because markets and user demands gradually solidify, and competitors want a piece of your pie. The success rate of new things is actually quite low—failure is the more common outcome. But failure also brings significant returns. In the 1990s, a company called General Magic tried to create a portable touchscreen internet device—something that looks very much like an iPhone today. As you might expect, 1990s technology couldn’t produce an iPhone. General Magic failed, but many of its employees went on to have successful careers. Tony Fadell left General Magic and eventually joined Apple, leading the design and development of the iPod. Kevin Lynch led the development of the Apple Watch. Andy Rubin wrote an operating system called Android that was acquired by Google.</p>
<p><strong>Third, leaders have a huge advantage in copying others.</strong> While the “new things” leaders create will be copied by followers into “old things,” leaders can also copy the “micro-innovations” of followers. Examples abound—from Tencent to Apple to OpenAI. All followers of these companies are merely helping leaders validate demands they didn’t have time to validate. Once validated, leaders can copy at minimal cost, and even take over the followers’ original customers.</p>
<p><strong>Fourth, being misunderstood is actually a huge advantage.</strong> Because short-term profit-seekers can’t see you, you can easily filter out employees who don’t share your vision. Because what you’re doing hasn’t become a trend, you can acquire resources from suppliers at very low cost. And because there’s no competition, you won’t have too much time pressure—the rhythm and feel of doing “new things” will be much better.</p>
<h1 id="4">4</h1>
<p>If we agree that doing “new things” is better, what practices can we adopt?</p>
<ol>
<li>
<p><strong>Career:</strong> Prefer companies that do “new things.” This way, you not only gain more personal growth but also have a small chance of getting significant financial returns.</p>
</li>
<li>
<p><strong>Investing:</strong> Prefer companies that do “new things.” These companies have obvious characteristics—they focus on things others don’t want to do, so their stock prices are low. But once these things succeed, they’ll have great commercial value, and stock prices will rise significantly. However, judging things and companies isn’t just talk—I don’t recommend spending too much time or money on investing.</p>
</li>
<li>
<p><strong>Daily work:</strong> Shift your evaluation criteria from “completeness” to “innovation.” I used to have a bad student mentality—I could get 80% of the results with 20% of the time, but I’d spend 100% of the time to get 100% of the results. The extra 20% is often trivial or unimportant stuff. Now, for me, quickly delivering results and validating the value of ideas is more important. If your 80% is important enough, many people will be willing to do the remaining 20%.</p>
</li>
</ol>
<hr>
<p>What you don’t know is always more valuable than what you already know.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Shape Your Mac</title>
    <link>https://www.yujiachen.com/shape-your-mac/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/shape-your-mac/</guid>
    <pubDate>Sun, 14 Jul 2024 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<h1 id="foreword">📖 Foreword</h1>
<p>Have you ever furnished your own house? To be honest, I haven’t, since I always rent. However, I do arrange things in my home. Right now, I’m lying on my sofa and writing this doc. To make it more engaging, I decided to include a picture of my current setup.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_1.png" alt="" class="article-image" width="1080" height="810" />
</picture>
</p>
<p>I moved to Shanghai less than two months ago, but I’ve already made this place feel familiar. This has allowed me to live happily and somewhat “efficiently” — meaning the room’s arrangement perfectly suits my daily routine.</p>
<p>It’s the same with a computer, especially a work computer. We don’t own our work computers; they belong to the company. But since we spend most of our screen time on them, it’s important to make sure they make us feel happy and efficient.</p>
<h1 id="basic-settings">🔧 Basic Settings</h1>
<h2 id="dotfiles">🗂️ Dotfiles</h2>
<p><a href="https://github.com/yujiachen-y/dotfiles">https://github.com/yujiachen-y/dotfiles</a></p>
<p>Dotfiles can be considered the metadata of your computer. Personally, I use <a href="https://github.com/yujiachen-y/dotfiles/blob/main/macos/Brewfile">a Brewfile</a> to manage my Mac’s dependencies. As long as an app can be installed via Homebrew, you should add it to your Brewfile. In fact, only a few apps cannot be installed using Homebrew.</p>
<p>I also store my <a href="https://github.com/yujiachen-y/dotfiles/blob/main/zsh/.zshrc">.zshrc</a> and <a href="https://github.com/yujiachen-y/dotfiles/blob/main/.vimrc">.vimrc</a> files in Dotfiles, so that I don’t need to configure my zsh and vim repeatedly.</p>
<p>Additionally, it’s common practice to store <a href="https://github.com/yujiachen-y/dotfiles/blob/main/macos/system_settings.sh">Mac settings</a> in dotfiles. However, you still need to go to System Settings to configure some options that can’t be changed via command lines. BTW, every time I set up a Mac, there are often UI changes and new options in System Preferences. So, the manual setup helps me discover what changes Apple has made lol.</p>
<h2 id="command-line-config">💻 Command Line Config</h2>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_2.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_2.png" alt="" class="article-image" width="1080" height="719" />
</picture>
</p>
<p>I use <a href="https://www.warp.dev/">Warp</a> as my terminal emulator. However, I believe the differences between Terminal.app, iTerm2, and Warp are not significant. For me, Warp has 3 main advantages:</p>
<ol>
<li>
<p><strong>Suggestions</strong>: I find them better than <a href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</a>.</p>
</li>
<li>
<p><strong>AI Copilot</strong>: In my opinion, all AI copilot products can be replaced by directly asking ChatGPT. (Update: Warp has agent mode too.)</p>
</li>
<li>
<p><strong>Built-in Shell Prompt</strong>: Although it’s not as flexible or functional as <a href="https://github.com/romkatv/powerlevel10k">p10k</a>, I’m considering reverting to my old <a href="https://github.com/yujiachen-y/dotfiles/blob/main/zsh/.p10k.zsh">.p10k.zsh</a> setup.</p>
</li>
</ol>
<p>Have you ever checked how many lines of code are in your <code>.zshrc</code> file? What do those lines do? I checked mine, and it’s only <a href="https://github.com/yujiachen-y/dotfiles/blob/3a77776963e3fee4a3df70c7e29230d40c1c2419/zsh/.zshrc">15 lines</a>. Typically, a <code>.zshrc</code> file is full of <code>[oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh)</code> configurations, but I only keep a few essential plugins and delete the unnecessary ones. Here are some <code>oh-my-zsh</code> plugins worth mentioning:</p>
<ul>
<li>
<p><strong><a href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/dotenv/README.md">dotenv</a></strong>: Automatically loads your .env files into environment variables.</p>
</li>
<li>
<p><strong><a href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git/README.md">git</a></strong>: I use commands like <code>gaa</code> (<code>git add --all</code>), <code>gsmsg</code> (<code>git commit --message</code>), and <code>gcn!</code> (<code>git commit --verbose --no-edit --amend</code>) every day.</p>
<ul>
<li>A git tip: please use <code>gcn!</code> to avoid unnecessary commit information in PR.</li>
</ul>
</li>
<li>
<p><strong><a href="https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/z/README.md">z</a></strong>: Access your most visited directories with very few keystrokes.</p>
</li>
</ul>
<p>As mentioned earlier, I use Warp for its built-in shell prompt, which looks like this:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_3.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_3.png" alt="" class="article-image" width="1080" height="719" />
</picture>
</p>
<p>However, it’s not very flexible and cannot be used in other terminal emulators. If you use Warp’s shell prompt, you might encounter issues in VSCode or other text editors and IDEs. <a href="https://github.com/warpdotdev/Warp/issues/257#issuecomment-1274198741">Embedding Warp into VSCode is particularly challenging</a>.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_4.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_4.png" alt="" class="article-image" width="1080" height="719" />
</picture>
</p>
<p>I recommend using <a href="https://github.com/romkatv/powerlevel10k">p10k</a> as your shell prompt. It’s highly flexible and can be used across different terminal emulators since it’s not tied to any specific one.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_5.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_5.png" alt="" class="article-image" width="765" height="516" />
</picture>
</p>
<p>I want to mention 2 of my favorite macOS commands here: <a href="https://ss64.com/mac/pbpaste.html">pbpaste</a> and <a href="https://ss64.com/mac/pbcopy.html">pbcopy</a>. There are already some useful examples on the manual pages.</p>
<h2 id="system-settings">⚙️ System Settings</h2>
<blockquote>
<p>As mentioned earlier, some settings below can be incorporated into your dotfiles.</p>
</blockquote>
<p>Implementing these settings will enhance your productivity:</p>
<ul>
<li><strong>Prevent Automatic Sleeping</strong></li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_6.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_6.png" alt="" class="article-image" width="1080" height="767" />
</picture>
</p>
<ul>
<li><strong>Three Finger Drag</strong></li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_7.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_7.png" alt="" class="article-image" width="1080" height="767" />
</picture>
</p>
<p><a href="https://youtu.be/-Fy6imaiHWE" title="Share link">How to select or drag using three fingers on your MacBook track pad</a></p>
<ul>
<li><strong>Reduce Keyboard Delay Time</strong>: Set your keyboard delay time to the shortest possible setting. As coders, we can’t afford to waste time waiting for keyboard delays.</li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_8.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_8.png" alt="" class="article-image" width="1080" height="767" />
</picture>
</p>
<ul>
<li><strong>Disable Double-Space Period</strong>: This feature can be annoying, so it’s best to turn it off.</li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_9.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_9.png" alt="" class="article-image" width="1080" height="767" />
</picture>
</p>
<ul>
<li><strong>Swap Caps Lock and Command Keys</strong>: The keys you use most frequently should be more accessible. Swap the Caps Lock key with the Command key.</li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_10.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_10.png" alt="" class="article-image" width="1080" height="796" />
</picture>
</p>
<ul>
<li><strong>Disable Press and Hold</strong>: This setting can only be disabled via the command line. If you use <a href="https://opusclip.larksuite.com/wiki/Uu5NwIeBFilqHRk2dhSu5MNYsUg#LlCwdefVKo7DO3xvxUquBptVs9f">Vim</a>, you might not need to disable this setting, as pressing and holding a key is not considered good practice in Vim.</li>
</ul>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_11.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_11.png" alt="" class="article-image" width="464" height="170" />
</picture>
</p>
<pre class="hljs"><code>defaults write -g ApplePressAndHoldEnabled -bool <span class="hljs-literal">false</span>
</code></pre>
<h1 id="apps">📱 Apps</h1>
<blockquote>
<p>As I mentioned earlier, all apps listed here should be managed by your Brewfile.</p>
</blockquote>
<h2 id="raycast">🌟 Raycast</h2>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_12.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_12.png" alt="" class="article-image" width="1080" height="701" />
</picture>
</p>
<p><a href="https://www.raycast.com/store">Raycast is an everything store for Mac shortcuts.</a> Here are some shortcuts I often use:</p>
<table>
<thead>
<tr>
<th>Clipboard History</th>
<th>Search Emoji</th>
<th>Window Management</th>
<th>Music Control</th>
<th>Kill Process</th>
<th>Search Browser Tabs</th>
<th>Reminder</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_13.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_13.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_14.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_14.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_15.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_15.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_16.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_16.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_17.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_17.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_18.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_18.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
<td>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_19.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_19.png" alt="" class="article-image" width="1080" height="661" />
</picture>
</td>
</tr>
</tbody>
</table>
<p>Raycast is super useful with your custom hotkeys.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_20.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_20.png" alt="" class="article-image" width="1080" height="702" />
</picture>
</p>
<h2 id="alttab">🌟 AltTab</h2>
<p>On a Mac, you can use Command + Tab to switch between apps. However, the built-in app switcher doesn’t allow you to switch between windows within the same application, which can be inconvenient if you need to frequently switch between windows of a single app.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_21.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_21.png" alt="" class="article-image" width="1080" height="701" />
</picture>
</p>
<p><a href="https://alt-tab-macos.netlify.app/">AltTab</a> solves this problem by allowing you to switch between windows across different Mac desktops. This way, you don’t need to use a mouse to switch between windows and desktops.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_22.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/shape-your-mac/en/image_22.png" alt="" class="article-image" width="1080" height="701" />
</picture>
</p>
<h1 id="backup">💾 Backup</h1>
<blockquote>
<p>There are two types of people:</p>
<ul>
<li>
<p>Those who do backups</p>
</li>
<li>
<p>Those who will do backups</p>
</li>
</ul>
<p>Any data you own that you haven’t backed up is data that could be gone at any moment, forever. Here we will cover some good backup basics and the pitfalls of some approaches.</p>
<h2 id="3-2-1-rule"><strong>3-2-1 Rule</strong></h2>
<p>The <a href="https://www.us-cert.gov/sites/default/files/publications/data_backup_options.pdf">3-2-1 rule</a> is a general recommended strategy for backing up your data. It state that you should have:</p>
<ul>
<li>
<p>at least <strong>3 copies</strong> of your data</p>
</li>
<li>
<p><strong>2</strong> copies in <strong>different mediums</strong></p>
</li>
<li>
<p><strong>1</strong> of the copies being <strong>offsite</strong></p>
</li>
</ul>
<p>The main idea behind this recommendation is not to put all your eggs in one basket. Having 2 different devices/disks ensures that a single hardware failure doesn’t take away all your data. Similarly, if you store your only backup at home and the house burns down or gets robbed you lose everything, that’s what the offsite copy is there for. Onsite backups give you availability and speed, offsite give you the resiliency should a disaster happen.</p>
<p>From <a href="https://missing.csail.mit.edu/2019/backups/">Backups</a></p>
</blockquote>
<h1 id="reflecting-thoughts">💭 Reflecting thoughts</h1>
<ul>
<li>
<p>Routinely review your workflow and try to improve your efficiency.</p>
</li>
<li>
<p><a href="https://www.catb.org/~esr/writings/taoup/html/ch01s06.html#id2878263">Fold knowledge into data, so program logic can be stupid and robust.</a></p>
</li>
<li>
<p><a href="https://www.catb.org/~esr/writings/taoup/html/ch01s06.html#id2878666">Programmer time is expensive; conserve it in preference to machine time.</a></p>
</li>
<li>
<p><a href="https://mcluhangalaxy.wordpress.com/2013/04/01/we-shape-our-tools-and-thereafter-our-tools-shape-us/">We shape our tools and thereafter our tools shape us</a></p>
</li>
</ul>
]]></content:encoded>
  </item>
  <item>
    <title>The Permission Model Myth</title>
    <link>https://www.yujiachen.com/the-permission-model-myth/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/the-permission-model-myth/</guid>
    <pubDate>Sun, 25 Feb 2024 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<h1 id="basic-concepts">Basic Concepts</h1>
<p>Depending on context, “permission” can mean different things. This article focuses on authorization in computing. Related concepts include:</p>
<ul>
<li><strong>Authentication (AuthN)</strong>: The process of confirming the identity of a person or entity. In computer security, this typically involves verifying whether a user or system is truly who it claims to be. Authentication can be performed through various methods, including passwords, biometrics, smart cards, or digital certificates. Authentication is the first step in the access control process—only after successful identity verification will the system consider granting access permissions. Related technical protocols and frameworks include OAuth, OpenID, and SAML.</li>
<li><strong>Authorization (AuthZ)</strong>: After a user’s identity has been verified, the system needs to decide which resources the user can access and what operations they can perform. Authorization is the process of defining and managing access permissions, determining what data or resources a verified user can view, use, modify, or delete. Its goal is to ensure that only users with appropriate permissions can access specific resources or data. Related authorization models include Role-Based Access Control (RBAC), Attribute-Based Access Control (ABAC), and others.</li>
<li><strong>Access Control</strong>: A broader concept that encompasses both authentication and authorization. It refers to the various methods and technologies used to restrict access to and use of systems and data.</li>
</ul>
<h1 id="use-cases">Use Cases</h1>
<p>Below are some access-control scenarios commonly encountered in engineering work.</p>
<h2 id="file-system-permissions">File System Permissions</h2>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_1.png" alt="" class="article-image" width="1080" height="214" />
</picture>
</p>
<p>The <code>drwx------</code> in the image is the mode string for a file-system entry. The first character indicates the file type (<code>-</code> for regular files, <code>d</code> for directories), followed by three groups of three characters representing read, write, and execute permissions for owner, group, and others. Some entries also include <code>@</code> or <code>+</code>. <code>@</code> means the file or directory has extended attributes (not discussed here). <code>+</code> means an Access Control List (ACL) is present. ACLs provide finer-grained rules beyond standard rwx bits, with per-user/per-group configuration. Using <code>ls -le</code>, we can inspect these ACL entries:

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_2.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_2.png" alt="" class="article-image" width="1080" height="349" />
</picture>
</p>
<p>For more details, see <code>man chmod</code>. Two observations:</p>
<ul>
<li>Unix permission granularity is too coarse, only supporting read, write, and execute—three types of operations. Other operating systems chose to add additional ACL mechanisms as a patch.</li>
<li>macOS ACL rule format is a triplet of: <code>(user, allow/deny, operation)</code>.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_3.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_3.png" alt="" class="article-image" width="1080" height="810" />
</picture>
Screenshot from macOS 14.2 manual</li>
</ul>
<h2 id="database-permissions">Database Permissions</h2>
<p>Database systems provide permissions (and permission combinations), allowing administrators to grant and audit access for users or user groups.</p>
<p>The screenshots below show one example of granting and inspecting permissions:

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_4.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_4.png" alt="" class="article-image" width="1080" height="114" />
</picture>
Granting: Granting SELECT permission on the ‘lark.message’ table to the user ‘admin’.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_5.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_5.png" alt="" class="article-image" width="1080" height="232" />
</picture>
Viewing a user’s global permissions: The ‘admin’ user has no global permissions.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_6.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_6.png" alt="" class="article-image" width="1080" height="152" />
</picture>
Viewing a user’s permissions on a specific table: The ‘admin’ user has SELECT permission on the ‘lark.message’ table.</p>
<p>From the examples above, we also observe:</p>
<ol>
<li>MySQL authorization operates at multiple levels—a user can have global permissions and table-level permissions.</li>
<li>MySQL’s permission control also follows the <code>(user, allow/deny, operation)</code> triplet pattern.</li>
</ol>
<h1 id="access-control-models">Access Control Models</h1>
<p>Several commonly used access control models exist in the industry: Discretionary Access Control (DAC), Mandatory Access Control (MAC), Role-Based Access Control (RBAC), Attribute-Based Access Control (ABAC), and Policy-Based Access Control (PBAC).</p>
<blockquote>
<p>Note: While these access control models represent a general consensus, there is currently no unified industry standard for determining which access control model a real-world solution belongs to. Therefore, the descriptions below serve mainly as conceptual explanations rather than definitive definitions and distinctions.</p>
</blockquote>
<h2 id="dac">DAC</h2>
<p>DAC is the most basic access control model, allowing resource owners to control access to their resources. In the DAC model, users can assign access permissions to other users based on their own judgment.</p>
<p>The file system permissions and database permissions examples above are actually DAC systems—they directly specify “who can perform what operations,” such as the file system’s <code>User 1 allow read</code> and the database’s <code>GRANT SELECT ON lark.message TO 'admin'@'%';</code>.</p>
<h2 id="rbac">RBAC</h2>
<p>RBAC is an access control model based on user roles. It allows system administrators to assign users to different roles according to organizational functions and responsibilities, with each role having a set of predefined access permissions. RBAC simplifies permission management because administrators only need to manage the relationship between roles and permissions, rather than each user’s individual permissions.</p>
<p>The following article describes how to configure RBAC in SAP:</p>
<p><a href="https://community.sap.com/t5/human-capital-management-blogs-by-sap/sap-commissions-implementing-authorization-with-user-roles-rbac/ba-p/13554527">SAP Commissions - Implementing Authorization With User Roles (RBAC)</a></p>
<p>In many RBAC implementations, user groups (or roles) bind users to predefined permissions.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_7.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_7.png" alt="" class="article-image" width="1080" height="650" />
</picture>
</p>
<p>RBAC has the advantages of simplified management and flexible configuration, but in practice it also exhibits the following drawbacks:</p>
<ol>
<li><strong>Role explosion</strong>: In complex organizations, a large number of roles may need to be created to cover all access requirements, which can make role management complex and difficult to maintain.</li>
<li><strong>Complex initial setup</strong>: Correctly implementing an RBAC system may require significant upfront planning and configuration, especially when migrating existing permissions to the RBAC model.</li>
<li><strong>Limited flexibility</strong>: While RBAC improves permission management efficiency, in some cases it may limit flexibility for individual users’ specific needs.</li>
<li><strong>Performance issues</strong>: In large systems with many users, roles, and permission rules, permission checks may cause performance problems.</li>
<li><strong>Management challenges</strong>: As organizations grow, managing and updating roles and their permissions can become complex, especially without automated tooling support.</li>
</ol>
<h2 id="abac">ABAC</h2>
<p>ABAC is a more flexible and dynamic access control model that determines access permissions based on attributes of the requester (such as age, position, etc.), resource attributes, and environmental conditions (such as time). ABAC provides more fine-grained access control, supports more complex security policies, and is suitable for scenarios requiring highly customized access control.</p>
<p>The main advantage of ABAC over RBAC is flexibility at the individual record level. RBAC grants are group-based, so members in one group usually share permissions. In a healthcare system, if each nurse should only see records for assigned patients, pure RBAC may require one role/group per nurse. With ABAC, one rule like <code>record.assignedNurse == currentUser</code> can express this directly.</p>
<p>A common pattern is hybrid: use RBAC to assign rule sets to users, then use ABAC within those rules to evaluate user/resource/environment attributes for the final decision.</p>
<h2 id="real-world-case-study">Real-World Case Study</h2>
<p><a href="https://www.youtube.com/watch?v=ZUmzELJ2UcM&amp;list=PLnobS_RgN7JZxK1wjUvQ84jMFqRZoJXbD&amp;index=1">https://www.youtube.com/watch?v=ZUmzELJ2UcM&amp;list=PLnobS_RgN7JZxK1wjUvQ84jMFqRZoJXbD&amp;index=1</a></p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_8.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_8.png" alt="" class="article-image" width="1080" height="608" />
</picture>
</p>
<p>The video demonstrates 4 types of access control:</p>
<ol>
<li>IP and login time determine whether a user can access the organization.</li>
<li>User permission groups determine whether a user can access objects.</li>
<li>Roles and reporting lines determine whether a user can access specific records.</li>
<li>User permission groups determine whether a user can access specific fields on a record.</li>
</ol>
<p>We can see that different access control models exist at different levels of the system, which is one of the key challenges in abstracting permission systems.</p>
<h1 id="industry-solutions">Industry Solutions</h1>
<p>Many early solutions did not clearly separate authentication (AuthN) from authorization (AuthZ). In my observation, RBAC is often the first-class model in these frameworks: once identity is user-centric, downstream authorization also tends to be user-centric.</p>
<ul>
<li><a href="https://github.com/spring-projects/spring-security">Spring Security</a>: No control panel; provides various APIs but doesn’t support configuration files.</li>
<li><a href="https://github.com/apache/shiro">Apache Shiro</a>: No control panel; supports configuration files but doesn’t support ABAC.</li>
</ul>
<p>The above two solutions and other earlier open-source solutions have relatively simple support for access control models and are therefore outside the scope of this article. In recent years, two solutions have provided more comprehensive support for access control models:</p>
<ul>
<li><a href="https://github.com/casbin/casbin">Casbin</a>: Open source, positioned as an SDK, supports various access control models, integrates with existing systems, and defines permissions through configuration.</li>
<li><a href="https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/">Zanzibar</a>: Closed source, positioned as an authorization service, also supports various access control models with finer control granularity, and permissions are configured through API calls.</li>
</ul>
<h2 id="casbin">Casbin</h2>
<p>Here’s an example of creating RBAC with Casbin.</p>
<p>Model file:</p>
<pre class="hljs"><code>[<span class="hljs-string">request_definition</span>]
<span class="hljs-string">r</span> <span class="hljs-string">=</span> <span class="hljs-string">sub,</span> <span class="hljs-string">act,</span> <span class="hljs-string">obj</span>

[<span class="hljs-string">policy_definition</span>]
<span class="hljs-string">p</span> <span class="hljs-string">=</span> <span class="hljs-string">sub,</span> <span class="hljs-string">act,</span> <span class="hljs-string">obj</span>

[<span class="hljs-string">role_definition</span>]
<span class="hljs-string">g</span> <span class="hljs-string">=</span> <span class="hljs-string">_,</span> <span class="hljs-string">_</span>
<span class="hljs-string">g2</span> <span class="hljs-string">=</span> <span class="hljs-string">_,</span> <span class="hljs-string">_</span>

[<span class="hljs-string">policy_effect</span>]
<span class="hljs-string">e</span> <span class="hljs-string">=</span> <span class="hljs-string">some(where</span> <span class="hljs-string">(p.eft</span> <span class="hljs-string">==</span> <span class="hljs-string">allow))</span>

[<span class="hljs-string">matchers</span>]
<span class="hljs-string">m</span> <span class="hljs-string">=</span> <span class="hljs-string">r.sub</span> <span class="hljs-string">==</span> <span class="hljs-string">p.sub</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">g(p.act,</span> <span class="hljs-string">r.act)</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">g2(p.obj,</span> <span class="hljs-string">r.obj)</span>
</code></pre>
<p>Policy file:</p>
<pre class="hljs"><code><span class="hljs-string">p,</span> <span class="hljs-string">alice,</span> <span class="hljs-string">sub-reader,</span> <span class="hljs-string">sub1</span>
<span class="hljs-string">p,</span> <span class="hljs-string">bob,</span> <span class="hljs-string">rg-owner,</span> <span class="hljs-string">rg2</span>

<span class="hljs-string">//</span> <span class="hljs-string">subscription</span> <span class="hljs-string">role</span> <span class="hljs-string">to</span> <span class="hljs-string">subscription</span> <span class="hljs-string">action</span> <span class="hljs-string">mapping</span>
<span class="hljs-string">g,</span> <span class="hljs-string">sub-reader,</span> <span class="hljs-string">sub-read</span>
<span class="hljs-string">g,</span> <span class="hljs-string">sub-owner,</span> <span class="hljs-string">sub-read</span>
<span class="hljs-string">g,</span> <span class="hljs-string">sub-owner,</span> <span class="hljs-string">sub-write</span>

<span class="hljs-string">//</span> <span class="hljs-string">resourceGroup</span> <span class="hljs-string">role</span> <span class="hljs-string">to</span> <span class="hljs-string">resourceGroup</span> <span class="hljs-string">action</span> <span class="hljs-string">mapping</span>
<span class="hljs-string">g,</span> <span class="hljs-string">rg-reader,</span> <span class="hljs-string">rg-read</span>
<span class="hljs-string">g,</span> <span class="hljs-string">rg-owner,</span> <span class="hljs-string">rg-read</span>
<span class="hljs-string">g,</span> <span class="hljs-string">rg-owner,</span> <span class="hljs-string">rg-write</span>

<span class="hljs-string">//</span> <span class="hljs-string">subscription</span> <span class="hljs-string">role</span> <span class="hljs-string">to</span> <span class="hljs-string">resourceGroup</span> <span class="hljs-string">role</span> <span class="hljs-string">mapping</span>
<span class="hljs-string">g,</span> <span class="hljs-string">sub-reader,</span> <span class="hljs-string">rg-reader</span>
<span class="hljs-string">g,</span> <span class="hljs-string">sub-owner,</span> <span class="hljs-string">rg-owner</span>

<span class="hljs-string">//</span> <span class="hljs-string">subscription</span> <span class="hljs-string">resource</span> <span class="hljs-string">to</span> <span class="hljs-string">resourceGroup</span> <span class="hljs-string">resource</span> <span class="hljs-string">mapping</span>
<span class="hljs-string">g2,</span> <span class="hljs-string">sub1,</span> <span class="hljs-string">rg1</span>
<span class="hljs-string">g2,</span> <span class="hljs-string">sub2,</span> <span class="hljs-string">rg2</span>
</code></pre>
<p>Request: <code>alice, rg-read, rg1</code> -&gt; <code>true</code>.</p>
<p>Reasoning flow: first match the subject. Alice’s policy entry is <code>alice, sub-reader, sub1</code>.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_9.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_9.png" alt="" class="article-image" width="1080" height="573" />
</picture>

Then match the action. The requested action is <code>rg-read</code>, while Alice’s role is <code>sub-reader</code>. Through <code>g, sub-reader, rg-reader</code> and <code>g, rg-reader, rg-read</code>, we infer that <code>sub-reader</code> implies <code>rg-read</code>.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_10.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_10.png" alt="" class="article-image" width="1080" height="593" />
</picture>

Finally, match the resource. The requested object is <code>rg1</code>, and Alice’s mapped resource is <code>sub1</code>; <code>g2, sub1, rg1</code> links the two.

<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_11.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/the-permission-model-myth/en/image_11.png" alt="" class="article-image" width="1080" height="583" />
</picture>
</p>
<h2 id="zanzibar">Zanzibar</h2>
<p>This article skips the detailed introduction of Zanzibar. The following URL provides a comprehensive introduction:</p>
<p><a href="https://zanzibar.academy/">Zanzibar: A Global Authorization System - Presented by Auth0</a></p>
<p>One key difference is architecture: Zanzibar is a standalone service, while Casbin is an SDK. A standalone service needs its own relationship-data storage plus caching and consistency management. Zanzibar is generally more full-featured, but that also implies higher operational cost and potentially tougher integration than an embedded SDK.</p>
<h1 id="the-challenge-of-abstraction">The Challenge of Abstraction</h1>
<p>Permission systems do not have a universal standard like TCP/IP, mainly because requirements and implementations vary widely across domains. TCP/IP provides a common interoperability layer for networks, while authorization systems are deeply coupled to business logic, organizational structures, and security constraints that differ by product. Key reasons include:</p>
<ol>
<li><strong>Diverse business requirements</strong>: Different types of applications have different business models and security needs. For example, a financial industry permission management system needs to consider complex compliance and audit requirements, while a social media platform’s permission system may focus more on user privacy and content management. This diversity makes it difficult to establish a unified standard covering all scenarios.</li>
<li><strong>Organizational structure differences</strong>: Different organizations’ structures, policies, and management processes also affect permission management implementation. Large enterprises may need a complex role hierarchy and fine-grained permission control, while small teams may only need a simple permission model.</li>
<li><strong>Rapid technological development</strong>: The rapid advancement of IT technology (including software development frameworks, database technology, cloud services, etc.) means new permission management methods and tools are constantly emerging, making it difficult to maintain a long-term effective unified standard.</li>
<li><strong>Balancing security and flexibility</strong>: Permission systems need to find a balance between security and flexibility. Different application scenarios may prioritize these two aspects differently, leading to different permission management strategies.</li>
<li><strong>Difficulty of standardization</strong>: Although some permission management concepts (such as RBAC) are widely accepted and used, extending these concepts to a universal standard covering all possible use cases is extremely difficult. Attempting to create such a standard could result in something overly complex and unwieldy, unable to adapt to rapidly changing technology and business requirements.</li>
</ol>
<p>Therefore, while some degree of standardization is possible in certain areas (such as authentication and encryption technology), the specific implementation of permission management systems often needs to be customized based on specific application scenarios and requirements.</p>
<h2 id="extension-zero-trust-network-model">Extension: Zero Trust Network Model</h2>
<p>Zero Trust leans more toward identity verification (AuthN), but many of its principles are closely related to AuthZ design. So this article ends with a brief Zero Trust overview.</p>
<p>Zero Trust is a cybersecurity model whose core principle is “never trust, always verify.” This model requires strict identity verification for all users and devices attempting to access network resources, regardless of whether they are inside or outside the network. The Zero Trust model’s starting point is to no longer assume the internal network is safe, but instead to consider that threats can come from anywhere, and therefore every access request needs to be verified and authorized.</p>
<p>The core principles of the Zero Trust model include:</p>
<ol>
<li><strong>Principle of least privilege</strong>: Users and devices receive only the minimum permissions needed to complete their tasks, limiting access to sensitive information and systems.</li>
<li><strong>Continuous verification</strong>: The system continuously monitors and verifies the trust status of users and devices, never assuming they are secure at any point.</li>
<li><strong>Explicit verification</strong>: All access attempts must undergo authentication and authorization, regardless of user or device location.</li>
<li><strong>Multi-factor authentication (MFA)</strong>: Adding security layers by requiring two or more identity-proving factors to reduce the risk of password compromise.</li>
<li><strong>Micro-segmentation</strong>: Dividing the network into small, managed segments to reduce the potential attacker’s range of movement and limit access to sensitive data.</li>
<li><strong>Risk-based adaptive policies</strong>: Dynamically adjusting access control policies based on factors such as user behavior, device security status, access time, and location.</li>
</ol>
]]></content:encoded>
  </item>
  <item>
    <title>Golang Generics</title>
    <link>https://www.yujiachen.com/golang-generics/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/golang-generics/</guid>
    <pubDate>Fri, 26 May 2023 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>Generics let us write reusable, type-safe logic over a family of types. In languages such as Java, C++, and C#, generics have long been a core feature. Go intentionally launched without generics, which kept the language simpler but made some abstractions awkward. As Go evolved, the community introduced generics to better support reusable data structures and common algorithms in production code.</p>
<p>An example:</p>
<pre class="hljs"><code><span class="hljs-keyword">import</span> <span class="hljs-string">&quot;golang.org/x/exp/constraints&quot;</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GMin</span>[<span class="hljs-title">T</span> <span class="hljs-title">constraints</span>.<span class="hljs-title">Ordered</span>]<span class="hljs-params">(x, y T)</span></span> T {
    <span class="hljs-keyword">if</span> x &lt; y {
        <span class="hljs-keyword">return</span> x
    }
    <span class="hljs-keyword">return</span> y
}

</code></pre>
<p>Go generics are similar in spirit to other languages, but different in several important details. This article starts from Go’s type-parameters proposal and uses implementation snippets plus examples to explain key behavior, with the goal of helping you use generics more effectively in real code.</p>
<blockquote>
<p>Main reference: <a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md">https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md</a></p>
</blockquote>
<h1 id="terminology">Terminology</h1>
<ul>
<li><strong>parameter</strong> (formal parameter): A placeholder declared in a function definition, e.g., <code>a</code> and <code>b</code> in <code>func Add(a, b int) int</code>. In this article, “type parameter” refers to this concept; for example, <code>T</code> in <code>func Add[T any](a, b T) T</code>.</li>
<li><strong>argument</strong> (actual argument): A concrete value passed at a call site, e.g., <code>a</code> and <code>2</code> in <code>sum := Add[int](a, 2)</code>. In this article, “type argument” refers to this concept; for example, <code>int</code> in <code>Add[int]</code>.</li>
<li><strong>function</strong>: Refers to a function in Go, such as <code>func Add(a, b int) int</code>. <a href="https://go.dev/ref/spec#Function_types">https://go.dev/ref/spec#Function_types</a></li>
<li><strong>method</strong>: Refers to a struct method in Go, such as <code>func (s *Struct) Get() string</code>. <a href="https://go.dev/ref/spec#Method_declarations">https://go.dev/ref/spec#Method_declarations</a></li>
<li><strong>operation</strong>: Can be understood as operators supported by Go’s built-in types (see below). The Go spec uses the term “operator.” <a href="https://go.dev/ref/spec#Operators">https://go.dev/ref/spec#Operators</a></li>
</ul>
<pre class="hljs"><code>Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = &quot;||&quot; | &quot;&amp;&amp;&quot; | rel_op | add_op | mul_op .
rel_op     = &quot;==&quot; | &quot;!=&quot; | &quot;&lt;&quot; | &quot;&lt;=&quot; | &quot;&gt;&quot; | &quot;&gt;=&quot; .
add_op     = &quot;+&quot; | &quot;-&quot; | &quot;|&quot; | &quot;^&quot; .
mul_op     = &quot;*&quot; | &quot;/&quot; | &quot;%&quot; | &quot;&lt;&lt;&quot; | &quot;&gt;&gt;&quot; | &quot;&amp;&quot; | &quot;&amp;^&quot; .

unary_op   = &quot;+&quot; | &quot;-&quot; | &quot;!&quot; | &quot;^&quot; | &quot;*&quot; | &quot;&amp;&quot; | &quot;&lt;-&quot; .

</code></pre>
<h1 id="overview">Overview</h1>
<p>The Go type parameters proposal summarizes several key points (<a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#summary">https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#summary</a>):</p>
<ul>
<li>Functions and types can have type parameters, and those parameters are constrained by interface types. (Methods are not included here; methods cannot declare their own type parameters.)</li>
<li>Constraints define which concrete types are allowed as type arguments, and which methods those arguments must provide.</li>
<li>Constraints also determine which operations are valid on type parameters inside generic code.</li>
<li>At call sites, type inference may infer missing type arguments, so explicit arguments are not always required. For example, with <code>func Add[T any](a, b T) T</code>, we can often write <code>sum := Add(a, 2)</code> instead of <code>sum := Add[int](a, 2)</code>.</li>
</ul>
<p>Next, we’ll explain Go generics design and usage from these perspectives:</p>
<ul>
<li>The specific definition of type constraints</li>
<li>How type inference is implemented</li>
<li>Some other related topics</li>
<li>Selected type inference code</li>
</ul>
<h1 id="type-constraints">Type Constraints</h1>
<p>Go type constraints can be seen as the “type” of type parameters. For example, in the following, <code>T</code> is the type parameter and <code>Stringer</code> is the type constraint—i.e., the type of <code>T</code>.</p>
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Stringify</span>[<span class="hljs-title">T</span> <span class="hljs-title">Stringer</span>]<span class="hljs-params">(s []T)</span></span> (ret []<span class="hljs-type">string</span>) {
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> s {
                ret = <span class="hljs-built_in">append</span>(ret, v.String())
        }
        <span class="hljs-keyword">return</span> ret
}

</code></pre>
<p>The type of a type constraint itself is <code>interface</code>. Before Go 1.18, an interface was a collection of methods. In the example above, <code>Stringer</code> describes that T should be a type with the method <code>func (T) String() string</code>. But if an interface can only be a collection of methods, the following function cannot be implemented:</p>
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Add</span>[<span class="hljs-title">T</span> <span class="hljs-title">constraints</span>.<span class="hljs-title">Integer</span>]<span class="hljs-params">(a, b T)</span></span> T {
        <span class="hljs-keyword">return</span> a + b
}

</code></pre>
<p>How can <code>constraints.Integer</code>, as an interface, express support for the <code>+</code> operation? The actual definition in the <a href="https://pkg.go.dev/golang.org/x/exp/constraints">constraints</a> package is:</p>
<pre class="hljs"><code><span class="hljs-keyword">type</span> Integer <span class="hljs-keyword">interface</span> {
        Signed | Unsigned
}

<span class="hljs-keyword">type</span> Signed <span class="hljs-keyword">interface</span> {
        ~<span class="hljs-type">int</span> | ~<span class="hljs-type">int8</span> | ~<span class="hljs-type">int16</span> | ~<span class="hljs-type">int32</span> | ~<span class="hljs-type">int64</span>
}

<span class="hljs-keyword">type</span> Unsigned <span class="hljs-keyword">interface</span> {
        ~<span class="hljs-type">uint</span> | ~<span class="hljs-type">uint8</span> | ~<span class="hljs-type">uint16</span> | ~<span class="hljs-type">uint32</span> | ~<span class="hljs-type">uint64</span> | ~<span class="hljs-type">uintptr</span>
}

</code></pre>
<p>As we can see, the <code>Integer</code> interface doesn’t define any methods. This brings us to the concept of underlying types.</p>
<h2 id="underlying-type">Underlying Type</h2>
<blockquote>
<p>Reference: <a href="https://go.dev/ref/spec#Underlying_types">https://go.dev/ref/spec#Underlying_types</a></p>
</blockquote>
<p>To make constraints more expressive, generics expanded interfaces from “sets of methods” to “sets of types.” The key difference is built-in operators. In Go, operators such as <code>&lt;</code> and <code>==</code> cannot be overloaded, so user-defined methods alone cannot describe operator support. With underlying types, a named type can reuse operators supported by its underlying type. As a result, interfaces can now express both method sets and type sets (for example, with syntax like <code>~underlying_type</code>).</p>
<p>The rules for determining a type’s underlying type are:</p>
<ol>
<li>Types that are their own underlying type:
<ol>
<li>boolean</li>
<li>numeric</li>
<li>string</li>
<li>type literal
<ol>
<li>ArrayType</li>
<li>StructType</li>
<li>PointerType</li>
<li>FunctionType</li>
<li>InterfaceType</li>
<li>SliceType</li>
<li>MapType</li>
<li>ChannelType</li>
</ol>
</li>
</ol>
</li>
<li>When none of the above apply, a type’s underlying type is the underlying type of the type it was created from. For example:</li>
</ol>
<pre class="hljs"><code><span class="hljs-keyword">type</span> (
    A1 = <span class="hljs-type">string</span>
    A2 = A1
)

</code></pre>
<p>Here A1 follows rule 1, so its underlying type is string. A2 follows rule 2, and its underlying type is also string.</p>
<p>Looking back at the definition of <code>constraints.Integer</code>, it represents the set of all types whose underlying type is <code>int</code>, <code>int8</code>, <code>int16</code>, <code>int32</code>, <code>int64</code>, <code>uint</code>, <code>uint8</code>, <code>uint16</code>, <code>uint32</code>, <code>uint64</code>, or <code>uintptr</code>. All these types support the <code>+</code> operation, so the <code>Add</code> function can be defined.</p>
<h2 id="difference-between-using-type-parameters-and-using-interfaces-directly">Difference Between Using Type Parameters and Using Interfaces Directly</h2>
<blockquote>
<p>Reference: <a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#values-of-type-parameters-are-not-boxed">https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#values-of-type-parameters-are-not-boxed</a></p>
</blockquote>
<p>Because constraints are interfaces, Go generics can look similar to interface-based programming. A key difference is that generic functions can preserve and return concrete types, rather than forcing everything through <code>interface{}</code>.</p>
<p>Using the <code>Add</code> example, if we remove the type parameter, it becomes:</p>
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Add</span><span class="hljs-params">(a, b <span class="hljs-keyword">interface</span>{})</span></span> <span class="hljs-keyword">interface</span>{} {
        n, _ := a.(<span class="hljs-type">int</span>)
        m, _ := b.(<span class="hljs-type">int</span>)
        <span class="hljs-keyword">return</span> n + m
}

</code></pre>
<p>With this non-generic implementation, the function returns <code>interface{}</code> rather than a caller-specific concrete type. The caller then needs a type assertion to recover the concrete type:</p>
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
        a, b := <span class="hljs-number">1</span>, <span class="hljs-number">2</span>
        c := Add(a, b)
        d, ok := c.(<span class="hljs-type">int</span>)
        ...
}

</code></pre>
<p>Also recall how Go interfaces are represented: an interface value carries both dynamic type information and the underlying value. That extra boxing/unboxing path may add overhead, which generics can often avoid.</p>
<h1 id="type-inference">Type Inference</h1>
<p>Not every call to a generic function requires explicit type arguments. Under certain conditions, the compiler can infer missing type arguments. Note that inference itself does not complete all semantic checks; some checks happen afterward.</p>
<p>The following sections introduce 3 key concepts of type inference, followed by the complete type inference process.</p>
<h2 id="type-unification">Type Unification</h2>
<blockquote>
<p>Reference: <a href="https://go.dev/ref/spec#Type_unification">https://go.dev/ref/spec#Type_unification</a></p>
</blockquote>
<h3 id="description">Description</h3>
<p><strong>Input</strong></p>
<ol>
<li>A mapping P -&gt; A, where P is a type parameter and A is a known type argument. For example, for <code>func Add[T any](a, b T) T</code>, a possible mapping is T -&gt; int.</li>
<li>Two types, which may or may not contain type parameters.</li>
</ol>
<p><strong>Output</strong></p>
<ol>
<li>Given known mappings, determine whether the two input types can be unified.</li>
</ol>
<h3 id="operations">Operations</h3>
<ol>
<li>For types without type parameters: the type must be equivalent to the comparison type, otherwise unification fails.
<ol>
<li>Two identical types are naturally equivalent.</li>
<li>If both types are channel types, they can be considered equivalent if they are identical after ignoring channel direction.</li>
<li>If two types have the same underlying types, they can also be considered equivalent.</li>
</ol>
</li>
<li>For types with type parameters: after abstracting over type parameters, the structure must still align; otherwise unification fails.
<ol>
<li>For example, <code>[]map[T1]T2</code> and <code>[]T3</code> are structurally consistent—<code>T3</code> can be substituted with <code>map[T1]T2</code>. Similarly, <code>[]map[T1]bool</code> and <code>[]map[string]T2</code> are structurally consistent.</li>
<li>For example, <code>[]map[T1]T2</code> and <code>int</code>, <code>struct{}</code>, <code>[]struct{}</code> etc. cannot possibly be structurally consistent.</li>
</ol>
</li>
<li>If matching succeeds and the type contains type parameters, we learn a new P’ -&gt; A’ mapping, which is added to the existing mappings.</li>
</ol>
<h2 id="function-argument-type-inference">Function Argument Type Inference</h2>
<blockquote>
<p>References:</p>
<ul>
<li><a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#function-argument-type-inference">https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#function-argument-type-inference</a></li>
<li><a href="https://go.dev/ref/spec#Function_argument_type_inference">https://go.dev/ref/spec#Function_argument_type_inference</a></li>
</ul>
</blockquote>
<h3 id="description-2">Description</h3>
<ol>
<li>When calling a function with type parameters, if the caller doesn’t pass type arguments, infer the type arguments from the actual arguments.</li>
</ol>
<h3 id="implementation">Implementation</h3>
<ol>
<li>Get a set of <code>(parameter, argument)</code> pairs from the caller’s actual arguments.</li>
<li>First ignore combinations where the <code>argument</code> has no type (i.e., constants, which have their own type inference rules). For typed <code>(parameter, argument)</code> pairs, perform type unification on their corresponding types and continuously update the mapping P -&gt; A.</li>
<li>Next, handle constant <code>(parameter, argument)</code> pairs. If a parameter’s corresponding type parameter was already inferred in step 2, ignore it. If not, treat the constant argument as its default type and perform type unification.</li>
<li>When all <code>(parameter, argument)</code> pairs have been processed, inference is complete. If any processing fails along the way, inference fails.</li>
</ol>
<p>Here’s an example illustrating the above steps:</p>
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">scale</span>[<span class="hljs-title">Number</span> ~<span class="hljs-title">int64</span>|~<span class="hljs-title">float64</span>|~<span class="hljs-title">complex128</span>]<span class="hljs-params">(v []Number, s Number)</span></span> []Number {
        ...
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> vector []<span class="hljs-type">float64</span>
        scaledVector := scale(vector, <span class="hljs-number">42</span>)
        ...
}

</code></pre>
<p>When function argument type inference begins, we get two <code>(parameter, argument)</code> pairs:</p>
<ol>
<li><code>(v []Number, vector []float64)</code></li>
<li><code>(s Number, 42)</code></li>
</ol>
<p>First, perform type unification on <code>(v []Number, vector []float64)</code>, yielding the mapping <code>Number -&gt; float64</code>.</p>
<p>Since we’ve already inferred the mapping <code>Number -&gt; float64</code>, the <code>(s Number, 42)</code> pair doesn’t need type unification.</p>
<p>If there were no mapping <code>Number -&gt; float64</code>, the type of 42 in <code>(s Number, 42)</code> would be treated as the default type <code>int</code>, and the mapping would be <code>Number -&gt; int</code>.</p>
<h2 id="constraint-type-inference">Constraint Type Inference</h2>
<blockquote>
<p>Reference: <a href="https://go.dev/ref/spec#Constraint_type_inference">https://go.dev/ref/spec#Constraint_type_inference</a></p>
</blockquote>
<h3 id="description-3">Description</h3>
<ol>
<li>Based on defined type parameter constraints, infer other unknown type parameters from a known type parameter.</li>
<li>For example, given a function <code>func Double[S ~[]E, E constraints.Integer](s S) S</code>, called as <code>Double([]int{1, 2, 3})</code>, we can infer <code>E -&gt; int</code> from the type constraint <code>S ~[]E</code> and <code>S -&gt; []int</code>.</li>
</ol>
<h3 id="implementation-2">Implementation</h3>
<ol>
<li>Iterate through all type parameters:
<ol>
<li>If a type parameter already has a corresponding argument, perform unification on their underlying types. In the <code>Double</code> example, the underlying type of <code>S</code> is <code>[]E</code>, so we unify <code>[]E</code> and the known argument <code>[]int</code>, inferring <code>E -&gt; int</code>.</li>
<li>If a type parameter doesn’t have a corresponding argument, but its type constraint has only one type, infer that the type parameter’s argument is the constraint type.</li>
</ol>
</li>
<li>In known mappings, if we have P -&gt; A and Q -&gt; B and A contains Q, substitute Q in A with B. For example, in <code>func Copy[T any, P *T](value T, dst P)</code>, given <code>T -&gt; int</code> and <code>P -&gt; *T</code>, we can infer <code>P -&gt; *int</code>.</li>
<li>Repeat step 2 until no type parameter P can be found that is contained in some type argument A.</li>
</ol>
<h2 id="type-inference-execution-steps">Type Inference Execution Steps</h2>
<blockquote>
<p>Reference: <a href="https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L33">https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L33</a></p>
</blockquote>
<p>Based on comments in the compiler code, inference proceeds in these steps:</p>
<ol>
<li>Perform function argument type inference using type arguments.</li>
<li>Perform constraint type inference.</li>
<li>Perform function argument type inference on remaining untyped arguments.</li>
<li>Perform a final round of constraint type inference.</li>
</ol>
<p>An example:</p>
<pre class="hljs"><code><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">&quot;fmt&quot;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">&quot;golang.org/x/exp/constraints&quot;</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Multiple</span>[<span class="hljs-title">S</span> ~[]<span class="hljs-title">E</span>, <span class="hljs-title">E</span>, <span class="hljs-title">X</span> <span class="hljs-title">constraints</span>.<span class="hljs-title">Integer</span>]<span class="hljs-params">(s S, x X)</span></span> S {
        <span class="hljs-keyword">for</span> i, e := <span class="hljs-keyword">range</span> s {
                s[i] *= x
        }
        <span class="hljs-keyword">return</span> s
}

<span class="hljs-keyword">type</span> IntVector []<span class="hljs-type">int</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
        vector := IntVector{<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>}
        vector = Multiple(vector, <span class="hljs-number">3</span>)
        fmt.Printf(<span class="hljs-string">&quot;%s\\n&quot;</span>, vector)
        <span class="hljs-comment">// output: [0, 3, 6, 9, 12]</span>
}

</code></pre>
<p>The type inference steps for <code>Multiple</code>:</p>
<ol>
<li>Perform type inference on typed function arguments, i.e., on <code>(s S, vector IntVector)</code>, yielding: <code>S -&gt; IntVector</code>.</li>
<li>Perform constraint type inference. <code>S</code>’s constraint is <code>[]E</code>, and <code>IntVector</code>’s underlying type is <code>[]int</code>, so unify <code>[]E</code> and <code>[]int</code>, yielding <code>E -&gt; int</code>.</li>
<li>Perform type inference on untyped function arguments, i.e., on <code>(x X, 3)</code>. Take the default value <code>int</code> for constant <code>3</code>, yielding <code>X -&gt; int</code>.</li>
<li>Perform constraint type inference again, but since all parameter types are known, it terminates early.</li>
</ol>
<h1 id="other-topics">Other Topics</h1>
<p>A few related notes.</p>
<h2 id="the-generics-dilemma">The Generics Dilemma</h2>
<blockquote>
<p>Reference: <a href="https://research.swtch.com/generic">https://research.swtch.com/generic</a></p>
</blockquote>
<p>Adding generics to a programming language inevitably increases complexity for at least one of three parties:</p>
<ul>
<li><strong>The programmer</strong>: C takes this approach—it only supports generic syntax, but the compiler and runtime don’t address problems introduced by generics. If there are issues, the programmer debugs them.</li>
<li><strong>Compilation</strong>: C++ takes this approach—types are resolved at compile time and templates are instantiated into concrete code. This increases compilation time and binary size.</li>
<li><strong>Runtime</strong>: Java takes a different approach centered on erasure, with trade-offs in runtime behavior and type expressiveness.</li>
</ul>
<p>As discussed above, Go mainly chooses to pay more at compile time: type information needed for generic function instantiation is resolved during compilation.</p>
<h2 id="using-t-instead-of-t">Using [T] Instead of <code>&lt;T&gt;</code></h2>
<p>Many developers’ first impression of generics comes from the <code>&lt;T&gt;</code> syntax in C++ or Java. <a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#why-not-use-the-syntax-like-c_and-java">The proposal explains</a> that since <code>&lt;</code> and <code>&gt;</code> are also used as comparison operators, distinguishing whether <code>&lt;</code> and <code>&gt;</code> represent comparison or type parameters would create additional burden. So <code>[T]</code> was chosen instead.</p>
<h2 id="why-don-t-go-generics-support-methods">Why Don’t Go Generics Support Methods?</h2>
<blockquote>
<p>This section is somewhat brief. For more detailed explanations and examples, see: <a href="https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#no-parameterized-methods">https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md#no-parameterized-methods</a></p>
</blockquote>
<p>In Go, structs can use type parameters, but a struct’s methods are not allowed to have type parameters. The primary reason is Go’s interface characteristics. As mentioned above, interfaces can express “collections of methods”—meaning an interface can represent all structs that implement its defined methods. If methods supported generics, we’d have interface definitions like:</p>
<pre class="hljs"><code><span class="hljs-keyword">type</span> Phone <span class="hljs-keyword">interface</span> {
        Call[N PhoneNumber](n N)
        Download[A App](a A)
}

</code></pre>
<p>Since Go has no explicit <code>implements</code> or <code>extends</code> declaration between structs and interfaces, adding method-level type parameters would require much heavier inference to decide interface conformance, with significant compiler complexity and cost.</p>
<h2 id="supporting-pointer-methods">Supporting Pointer Methods</h2>
<p>When we define a generic function <code>F[T C]</code> and constraint <code>C</code> requires methods, passing a type <code>X</code> will fail if those required methods exist on <code>*X</code> rather than on <code>X</code>.</p>
<p>A concrete example:</p>
<pre class="hljs"><code><span class="hljs-keyword">type</span> Setter <span class="hljs-keyword">interface</span> {
        Set(<span class="hljs-type">string</span>)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">FromStrings</span>[<span class="hljs-title">T</span> <span class="hljs-title">Setter</span>]<span class="hljs-params">(s []<span class="hljs-type">string</span>)</span></span> []T {
        result := <span class="hljs-built_in">make</span>([]T, <span class="hljs-built_in">len</span>(s))
        <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> s {
                result[i].Set(v)
        }
        <span class="hljs-keyword">return</span> result
}

<span class="hljs-keyword">type</span> Settable <span class="hljs-type">int</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *Settable)</span></span> Set(s <span class="hljs-type">string</span>) {
        i, _ := strconv.Atoi(s) <span class="hljs-comment">// real code should not ignore the error</span>
        *p = Settable(i)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">F</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// INVALID</span>
        nums := FromStrings[Settable]([]<span class="hljs-type">string</span>{<span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;2&quot;</span>})
        <span class="hljs-comment">// Here we want nums to be []Settable{1, 2}.</span>
        ...
}

</code></pre>
<p>In the example above, <code>result</code>’s type is <code>[]Settable</code>, but <code>Settable</code> doesn’t support the <code>Set</code> method—<code>*Settable</code> does. So <code>result[i].Set(v)</code> can’t be called normally.</p>
<p>The solution:</p>
<pre class="hljs"><code><span class="hljs-keyword">type</span> Setter2[B any] <span class="hljs-keyword">interface</span> {
        Set(<span class="hljs-type">string</span>)
        *B <span class="hljs-comment">// non-interface type constraint element</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">FromStrings2</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>, <span class="hljs-title">PT</span> <span class="hljs-title">Setter2</span>[<span class="hljs-title">T</span>]]<span class="hljs-params">(s []<span class="hljs-type">string</span>)</span></span> []T {
        result := <span class="hljs-built_in">make</span>([]T, <span class="hljs-built_in">len</span>(s))
        <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> s {
                <span class="hljs-comment">// The type of &amp;result[i] is *T which is in the type set</span>
                <span class="hljs-comment">// of Setter2, so we can convert it to PT.</span>
                p := PT(&amp;result[i])
                <span class="hljs-comment">// PT has a Set method.</span>
                p.Set(v)
        }
        <span class="hljs-keyword">return</span> result
}

</code></pre>
<p>This pattern explicitly distinguishes value type <code>T</code> from pointer type <code>PT</code>, and encodes their relationship in <code>Setter2[B any]</code>. We then convert <code>&amp;result[i]</code> to <code>PT</code>, so calling <code>Set</code> succeeds.</p>
<h2 id="using-generics-in-practice">Using Generics in Practice</h2>
<p>With the above knowledge of defining generic functions through type constraints and understanding of type inference, let’s discuss practical applications of generics:</p>
<ul>
<li>Container operations.</li>
<li>General-purpose data structures.</li>
<li>General-purpose operation logic. In my own code, I call a data-gateway service that returns a fixed structure. I used generics to wrap request/response handling so data from different sources can be converted into target structs with less boilerplate.</li>
</ul>
<p>If you have other ways of using generics in your work or have recommendations for useful generic libraries, feel free to share in the comments :)</p>
<h1 id="type-inference-code">Type Inference Code</h1>
<h2 id="type-unification-2">Type Unification</h2>
<blockquote>
<p>Reference: <a href="https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/unify.go">https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/unify.go</a></p>
</blockquote>
<p>Recursively checks whether <code>x, y Type</code> can unify under mapping <code>p *ifacePair</code>. If <code>x</code> or <code>y</code> is an uninferred type parameter, it can be matched and inference proceeds.</p>
<pre class="hljs"><code><span class="hljs-comment">// nify implements the core unification algorithm which is an</span>
<span class="hljs-comment">// adapted version of Checker.identical. For changes to that</span>
<span class="hljs-comment">// code the corresponding changes should be made here.</span>
<span class="hljs-comment">// Must not be called directly from outside the unifier.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(u *unifier)</span></span> nify(x, y Type, p *ifacePair) (result <span class="hljs-type">bool</span>) {

        ......

        <span class="hljs-comment">// Cases where at least one of x or y is a type parameter.</span>
        <span class="hljs-keyword">switch</span> i, j := u.x.index(x), u.y.index(y); {
        <span class="hljs-keyword">case</span> i &gt;= <span class="hljs-number">0</span> &amp;&amp; j &gt;= <span class="hljs-number">0</span>:
                <span class="hljs-comment">// both x and y are type parameters</span>
                <span class="hljs-keyword">if</span> u.join(i, j) {
                        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
                }
                <span class="hljs-comment">// both x and y have an inferred type - they must match</span>
                <span class="hljs-keyword">return</span> u.nifyEq(u.x.at(i), u.y.at(j), p)

        <span class="hljs-keyword">case</span> i &gt;= <span class="hljs-number">0</span>:
                <span class="hljs-comment">// x is a type parameter, y is not</span>
                <span class="hljs-keyword">if</span> tx := u.x.at(i); tx != <span class="hljs-literal">nil</span> {
                        <span class="hljs-keyword">return</span> u.nifyEq(tx, y, p)
                }
                <span class="hljs-comment">// otherwise, infer type from y</span>
                u.x.set(i, y)
                <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>

        <span class="hljs-keyword">case</span> j &gt;= <span class="hljs-number">0</span>:

                ......

        }

        ......

        <span class="hljs-keyword">switch</span> x := x.(<span class="hljs-keyword">type</span>) {

        ......

        <span class="hljs-keyword">case</span> *Slice:
                <span class="hljs-comment">// Two slice types are identical if they have identical element types.</span>
                <span class="hljs-keyword">if</span> y, ok := y.(*Slice); ok {
                        <span class="hljs-keyword">return</span> u.nify(x.elem, y.elem, p)
                }

        <span class="hljs-keyword">case</span> *Struct:
                <span class="hljs-comment">// Two struct types are identical if they have the same sequence of fields,</span>
                <span class="hljs-comment">// and if corresponding fields have the same names, and identical types,</span>
                <span class="hljs-comment">// and identical tags. Two embedded fields are considered to have the same</span>
                <span class="hljs-comment">// name. Lower-case field names from different packages are always different.</span>
                <span class="hljs-keyword">if</span> y, ok := y.(*Struct); ok {
                        <span class="hljs-keyword">if</span> x.NumFields() == y.NumFields() {
                                <span class="hljs-keyword">for</span> i, f := <span class="hljs-keyword">range</span> x.fields {
                                        g := y.fields[i]
                                        <span class="hljs-keyword">if</span> f.embedded != g.embedded ||
                                                x.Tag(i) != y.Tag(i) ||
                                                !f.sameId(g.pkg, g.name) ||
                                                !u.nify(f.typ, g.typ, p) {
                                                <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
                                        }
                                }
                                <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
                        }
                }

        ......

        <span class="hljs-keyword">default</span>:
                <span class="hljs-built_in">panic</span>(sprintf(<span class="hljs-literal">nil</span>, <span class="hljs-literal">true</span>, <span class="hljs-string">&quot;u.nify(%s, %s), u.x.tparams = %s&quot;</span>, x, y, u.x.tparams))
        }

        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}

</code></pre>
<h2 id="function-argument-type-inference-2">Function Argument Type Inference</h2>
<h3 id="typed-arguments-are-directly-unified">Typed Arguments Are Directly Unified</h3>
<blockquote>
<p>Reference: <a href="https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L250">https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L250</a></p>
</blockquote>
<pre class="hljs"><code>        <span class="hljs-comment">// indices of the generic parameters with untyped arguments - save for later</span>
        <span class="hljs-keyword">var</span> indices []<span class="hljs-type">int</span>
        <span class="hljs-keyword">for</span> i, arg := <span class="hljs-keyword">range</span> args {
                par := params.At(i)
                <span class="hljs-comment">// If we permit bidirectional unification, this conditional code needs to be</span>
                <span class="hljs-comment">// executed even if par.typ is not parameterized since the argument may be a</span>
                <span class="hljs-comment">// generic function (for which we want to infer its type arguments).</span>
                <span class="hljs-keyword">if</span> isParameterized(tparams, par.typ) {
                        <span class="hljs-keyword">if</span> arg.mode == invalid {
                                <span class="hljs-comment">// An error was reported earlier. Ignore this targ</span>
                                <span class="hljs-comment">// and continue, we may still be able to infer all</span>
                                <span class="hljs-comment">// targs resulting in fewer follow-on errors.</span>
                                <span class="hljs-keyword">continue</span>
                        }
                        <span class="hljs-keyword">if</span> targ := arg.typ; isTyped(targ) {
                                <span class="hljs-comment">// If we permit bidirectional unification, and targ is</span>
                                <span class="hljs-comment">// a generic function, we need to initialize u.y with</span>
                                <span class="hljs-comment">// the respective type parameters of targ.</span>
                                <span class="hljs-keyword">if</span> !u.unify(par.typ, targ) {
                                        errorf(<span class="hljs-string">&quot;type&quot;</span>, par.typ, targ, arg)
                                        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
                                }
                        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> _, ok := par.typ.(*TypeParam); ok {
                                <span class="hljs-comment">// Since default types are all basic (i.e., non-composite) types, an</span>
                                <span class="hljs-comment">// untyped argument will never match a composite parameter type; the</span>
                                <span class="hljs-comment">// only parameter type it can possibly match against is a *TypeParam.</span>
                                <span class="hljs-comment">// Thus, for untyped arguments we only need to look at parameter types</span>
                                <span class="hljs-comment">// that are single type parameters.</span>
                                indices = <span class="hljs-built_in">append</span>(indices, i)
                        }
                }
        }

</code></pre>
<h3 id="untyped-arguments-are-assigned-constant-default-values-then-unified">Untyped Arguments Are Assigned Constant Default Values Then Unified</h3>
<blockquote>
<p>Reference: <a href="https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L297">https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L297</a></p>
</blockquote>
<pre class="hljs"><code>        <span class="hljs-comment">// Use any untyped arguments to infer additional type arguments.</span>
        <span class="hljs-comment">// Some generic parameters with untyped arguments may have been given</span>
        <span class="hljs-comment">// a type by now, we can ignore them.</span>
        <span class="hljs-keyword">for</span> _, i := <span class="hljs-keyword">range</span> indices {
                tpar := params.At(i).typ.(*TypeParam) <span class="hljs-comment">// is type parameter by construction of indices</span>
                <span class="hljs-comment">// Only consider untyped arguments for which the corresponding type</span>
                <span class="hljs-comment">// parameter doesn&#x27;t have an inferred type yet.</span>
                <span class="hljs-keyword">if</span> targs[tpar.index] == <span class="hljs-literal">nil</span> {
                        arg := args[i]
                        targ := Default(arg.typ)
                        <span class="hljs-comment">// The default type for an untyped nil is untyped nil. We must not</span>
                        <span class="hljs-comment">// infer an untyped nil type as type parameter type. Ignore untyped</span>
                        <span class="hljs-comment">// nil by making sure all default argument types are typed.</span>
                        <span class="hljs-keyword">if</span> isTyped(targ) &amp;&amp; !u.unify(tpar, targ) {
                                errorf(<span class="hljs-string">&quot;default type&quot;</span>, tpar, targ, arg)
                                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
                        }
                }
        }

</code></pre>
<h2 id="constraint-type-inference-2">Constraint Type Inference</h2>
<blockquote>
<p>Reference: <a href="https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L468">https://github.com/golang/go/blob/go1.18/src/cmd/compile/internal/types2/infer.go#L468</a></p>
</blockquote>
<h3 id="core-type-processing-for-type-parameters">Core Type Processing for Type Parameters</h3>
<p>In the first phase of constraint type inference, a new concept called “core type” is introduced. We won’t go into too much detail here—it can be understood as the underlying type of the type corresponding to a type constraint. Using core types and known arguments, some type inferences can be completed.</p>
<pre class="hljs"><code>                <span class="hljs-keyword">for</span> i, tpar := <span class="hljs-keyword">range</span> tparams {
                        <span class="hljs-comment">// If there is a core term (i.e., a core type with tilde information)</span>
                        <span class="hljs-comment">// unify the type parameter with the core type.</span>
                        <span class="hljs-keyword">if</span> core, single := coreTerm(tpar); core != <span class="hljs-literal">nil</span> {
                                <span class="hljs-comment">// A type parameter can be unified with its core type in two cases.</span>
                                tx := u.x.at(i)
                                <span class="hljs-keyword">switch</span> {
                                <span class="hljs-keyword">case</span> tx != <span class="hljs-literal">nil</span>:

                                        ......

                                        <span class="hljs-keyword">if</span> !u.unify(tx, core.typ) {
                                                <span class="hljs-comment">// TODO(gri) improve error message by providing the type arguments</span>
                                                <span class="hljs-comment">//           which we know already</span>
                                                <span class="hljs-comment">// Don&#x27;t use term.String() as it always qualifies types, even if they</span>
                                                <span class="hljs-comment">// are in the current package.</span>
                                                tilde := <span class="hljs-string">&quot;&quot;</span>
                                                <span class="hljs-keyword">if</span> core.tilde {
                                                        tilde = <span class="hljs-string">&quot;~&quot;</span>
                                                }
                                                check.errorf(pos, <span class="hljs-string">&quot;%s does not match %s%s&quot;</span>, tpar, tilde, core.typ)
                                                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, <span class="hljs-number">0</span>
                                        }

                                <span class="hljs-keyword">case</span> single &amp;&amp; !core.tilde:
                                        <span class="hljs-comment">// The corresponding type argument tx is unknown and there&#x27;s a single</span>
                                        <span class="hljs-comment">// specific type and no tilde.</span>
                                        <span class="hljs-comment">// In this case the type argument must be that single type; set it.</span>
                                        u.x.set(i, core.typ)

                                <span class="hljs-keyword">default</span>:
                                        <span class="hljs-comment">// Unification is not possible and no progress was made.</span>
                                        <span class="hljs-keyword">continue</span>
                                }

                                ......

                        }
                }

</code></pre>
<h3 id="mapping-simplification">Mapping Simplification</h3>
<p>The second phase of constraint type inference continuously simplifies the mappings.</p>
<pre class="hljs"><code>                smap := makeSubstMap(tparams, types)
                n := <span class="hljs-number">0</span>
                <span class="hljs-keyword">for</span> _, index := <span class="hljs-keyword">range</span> dirty {
                        t0 := types[index]
                        <span class="hljs-keyword">if</span> t1 := check.subst(nopos, t0, smap, <span class="hljs-literal">nil</span>); t1 != t0 {
                                types[index] = t1
                                dirty[n] = index
                                n++
                        }
                }

</code></pre>
]]></content:encoded>
  </item>
  <item>
    <title>Notes on A Philosophy of Software Design</title>
    <link>https://www.yujiachen.com/software-design-book-notes/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/software-design-book-notes/</guid>
    <pubDate>Sun, 09 Oct 2022 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>About six months ago, I borrowed <em>A Philosophy of Software Design</em> from a friend who had not finished it yet, and then left it unread myself. During the National Day holiday, I spent three on-and-off days finishing it. The most useful part for me was its revised-edition pushback against some views in <em>Clean Code</em>. Software engineering is still a young discipline: there are best practices that many people agree on, but almost nothing is universally correct. I had also seen many of this book’s ideas in other books, sometimes in a stronger form. For example, this book explains why inheritance can be problematic but spends less time on alternatives, while <em>Effective Java</em> explicitly argues to “prefer composition over inheritance,” and <em>The Pragmatic Programmer</em> also discusses extensibility.</p>
<p>The book’s discussions on complexity are very helpful for software development, while the other parts are more ordinary—experienced developers can skim them quickly.</p>
<h2 id="definition-of-complexity">Definition of Complexity</h2>
<p>Summarized by a formula: software complexity is the sum of each component’s complexity multiplied by how frequently it’s modified. That is, to reduce software complexity, we can either lower individual component complexity or take a holistic view and place highly complex logic in modules that are modified less frequently.</p>
<p>Growing complexity brings 3 problems:</p>
<ol>
<li>Increased modification complexity—one change requires changes everywhere.</li>
<li>Heavier cognitive burden—needing to understand too many concepts before knowing how to make changes.</li>
<li>Unknown unknowns—not knowing what knowledge is needed to understand a problem, let alone how to acquire it. Even if the engineer makes incorrect changes or misses some changes, they have no way of knowing.</li>
</ol>
<p>Tangled dependencies between code lead to problems 1 and 2, while missing critical information (e.g., inconsistencies between code and documentation) leads to problems 3 and 2.</p>
<h2 id="strategic-programming-vs-tactical-programming">Strategic Programming vs Tactical Programming</h2>
<ul>
<li><strong>Strategic programming</strong>: Prioritizes overall software design. Minimum viability and shortest time are not considered in isolation (though reasonable development costs and working code are important parts of design).</li>
<li><strong>Tactical programming</strong>: Using the shortest programming time to produce minimally viable code.</li>
</ul>
<p>Note that “strategic” here still operates within agile cycles, not the waterfall model. In waterfall, projects are too large with execution times and feedback cycles too long, which is unfavorable for designing better software architecture.</p>
<p>The book says that investing 10%-20% more time beyond tactical programming can transform it into strategic programming. I disagree—strategic programming generates a lot of throwaway work from various comparisons and research. Overall, I believe <strong>rigorous</strong> strategic programming costs about twice as much as <strong>bare-minimum</strong> tactical programming. However, I very much agree with one illustration in the book: with strategic programming, progress and time spent have a nearly linear relationship, while with tactical programming, achieving one unit of progress requires nearly exponential development time investment. This is because strategic programming leverages good architecture to hide complexity, making each unit of progress as independent as possible from existing code. Tactical programming, on the other hand, often means every piece of new code must account for existing old code, making the burden increasingly heavy.</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/software-design-book-notes/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/software-design-book-notes/en/image_1.png" alt="" class="article-image" width="640" height="391" />
</picture>
</p>
<p>The 10-20% time mentioned in the book reminds me of:</p>
<ul>
<li>Google’s 20% free work time.</li>
<li>Asana dedicates one week per quarter specifically to software refactoring. (Accounting for vacation and the fact that engineers don’t spend all their time coding, this is also close to 10%?) I read about this in <em>The Effective Engineer</em>—I helped review the Chinese edition, so go buy it!</li>
</ul>
<p>The book argues that good code attracts better developers, and that some startups believe tactical programming lets them move faster and they can hire better developers to fix the code later once the business takes off—but this approach is inadvisable. I think this is certainly true for a technology company, but for a business-driven company, it may depend on whether the business itself actually needs strong technical support. Some businesses genuinely don’t.</p>
<h2 id="modules-should-be-deep">Modules Should Be Deep</h2>
<p>A module is “deep” when it hides substantial implementation complexity behind a small and convenient interface.</p>
<p><img src="https://img2.doubanio.com/view/thing_review/l/public/p7997103.jpg" alt="" /></p>
<p>The book gives a fascinating example: GC in Go and Java. The implementation is very complex, but GC means the language does not need to expose manual memory-management interfaces to users. Even when we add substantial internal complexity, good modularization can still reduce the number of exposed interfaces.</p>
<p>The GC example feels similar to the server-compute/client-render pattern: centralizing complex computation in one place means clients do not each need to maintain their own copy of that logic.</p>
<p>The author also notes that some modern developers tend to make functions and classes smaller and smaller, which can also make them shallow (not deep). This is worth keeping in mind during everyday development.</p>
<h2 id="information-hiding-and-leaking">Information Hiding (and Leaking)</h2>
<p>This chapter argues that different modules should own orthogonal (unrelated) concerns. I summarized the traits emphasized by the examples:</p>
<ul>
<li><strong>Low coupling</strong>
<ul>
<li>After breaking a large function into multiple smaller ones, those smaller functions should not depend on each other. Pay special attention to call order: whether a function executes correctly should not depend on specific preceding functions having run.</li>
<li>It’s unreasonable for both a file-read module and a file-write module to carry file-format parsing knowledge. Consider merging them into one read-write module, or extracting file-format parsing into a separate module.</li>
</ul>
</li>
<li><strong>High cohesion</strong>
<ul>
<li>Code generating an HTTP response should not first set the HTTP version and then delegate the response to other modules. Instead, responsibility for setting protocol details should be encapsulated in the relevant modules.</li>
</ul>
</li>
<li><strong>Hide implementation</strong>
<ul>
<li>A class that internally uses a map should not expose that map directly. Otherwise external code can mutate internal state, and replacing the map with another implementation later becomes much harder.</li>
</ul>
</li>
</ul>
<h2 id="the-more-general-the-simpler">The More General, the Simpler</h2>
<p>(My loose translation—the original heading is “General-Purpose Modules are Deeper.” In this book, “deep” refers to modules that hide significant complexity behind a simple interface.)</p>
<p>My understanding is: once complexity is reduced to a certain point, it is often transferred rather than eliminated. Distributing complexity between interface and implementation determines how often users encounter that complexity. Usually, the simpler and more general the interface, the more complex the implementation, and thus the deeper the module.</p>
<p>The author also emphasizes that designing general interfaces does not mean over-designing. A general interface may accommodate future needs, but those needs may never arrive, or may even break the existing interface. So we can preserve interface generality while implementing only current requirements. If new needs emerge later, we can extend internals behind the same interface and make the module deeper.</p>
<h2 id="different-layers-different-abstractions">Different Layers, Different Abstractions</h2>
<p>Different layers should have different abstractions. If different layers share the same abstraction, it often indicates that the code at those layers isn’t deep enough.</p>
<p>For example, the classic Principle of Least Knowledge: in a call chain A -&gt; B -&gt; C, if B only forwards requests to C, then A should call C directly instead of B. Adding code always increases complexity, so we must evaluate whether new code brings enough benefit. In this case, B adds complexity without benefit and increases future maintenance costs.</p>
<p>The book also discusses decorators and pass-through variables.</p>
<p>Regarding decorators, Java and Python provide relatively ergonomic language support. In other languages, the author suggests considering alternatives to decorators:</p>
<ul>
<li>Add the new functionality directly into the decorated method or object.</li>
<li>If the decorator adds functionality for special cases while the decorated method handles general cases, consider whether these special cases can be handled elsewhere.</li>
<li>Add the new functionality into other existing decorators.</li>
</ul>
<p>Pass-through variables are values that must be threaded through every method in a call chain, such as Go’s <code>ctx context.Context</code>. The author suggests using something like thread-local storage to keep context in an instance, but I don’t think there is a universally good solution today. A potentially cleaner approach might be thread-scoped dependency injection (for example, via frameworks like Guice): delegate thread-local read/write mechanics to the framework, let it inject available values automatically, and expose only remaining values to callers. I have not used this approach in production yet.</p>
<h2 id="in-defense-of-long-functions">In Defense of Long Functions</h2>
<p>The book mentions Robert Martin’s <em>Clean Code</em>, which advocates that functions should be as short as possible.</p>
<p>However, the author disagrees that all functions should be as short as possible: “Each method should do one thing and do it completely.” If a function is hard to decompose into shorter independent units, or if the extracted functions still depend on shared context, that kind of decomposition can increase complexity and maintenance cost.</p>
<h2 id="comments">Comments</h2>
<h3 id="should-we-write-comments">Should We Write Comments?</h3>
<p>Here the author again explicitly disagrees with <em>Clean Code</em>, which treats comments as a “necessary evil” and argues that comments often indicate failure to write expressive code.</p>
<p>The author argues that comments and code are complementary: together they reduce complexity, and missing comments can increase it. For example, comments can reduce the need for excessively long function names (the book’s example: <code>isLeastRelevantMultipleOfNextLargerPrimeFactor</code>) and can avoid forced decomposition into many short but interdependent functions.</p>
<p>Function comments let callers use code without reading implementations, which is itself a form of abstraction. Comments can also capture design information that cannot be fully expressed in code alone.</p>
<h3 id="comments-should-describe-non-obvious-parts-of-code">Comments Should Describe Non-Obvious Parts of Code</h3>
<p>The author discourages writing information in comments that can be directly derived from reading the code. The book’s counterexample:</p>
<pre class="hljs"><code>ptr_copy=get_copy(obj)    #Get pointer copy
if is_unlocked(ptr_copy):  #Is obj free?
return obj                 #return current obj
</code></pre>
<p>Comments can be classified into these categories, each with different standards:</p>
<ul>
<li><strong>Lower-level comments</strong>: Help developers understand certain details in the code more precisely.</li>
<li><strong>Higher-level comments</strong>: Help developers intuitively understand what the code does without reading it.</li>
<li><strong>Interface documentation</strong>: Doesn’t describe implementation details, but lets developers know how to use the corresponding interface. Good interface documentation represents good abstraction; if interface documentation must describe internal implementation, the underlying implementation may be too shallow.</li>
<li><strong>Implementation comments</strong>: Describe what the code does and why, but don’t need to describe how—because the code itself contains that information (what and why, not how).</li>
</ul>
<p>For cross-module designs shared by multiple modules, the author suggests writing notes in shared structural definitions, or adding a <code>designNotes</code> file to the source code.</p>
<h3 id="write-comments-first-code-second">Write Comments First, Code Second</h3>
<p>The author believes writing comments after finishing code isn’t a good habit, because writing comments after all code is done increases the resistance to writing them. Also, after finishing code, some critical information may have already been discarded by the brain and is difficult to recover.</p>
<p>This is similar to how I write reading notes immediately after finishing a book—it’s the most efficient approach.</p>
<p>So the author advocates writing comments first, then code.</p>
<p>A common counterargument is that code usually goes through several rounds of changes before it stabilizes, so writing comments only at the end seems more efficient. But the author argues that repeatedly modifying code is more expensive than iterating on comments first. Writing comments in advance helps stabilize structure; if the design keeps changing, revise the comments first, where iteration is cheaper.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Don&apos;t Be a Programmer</title>
    <link>https://www.yujiachen.com/dont-be-a-programmer/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/dont-be-a-programmer/</guid>
    <pubDate>Sun, 01 May 2022 00:00:00 GMT</pubDate>
    <category>Essays</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>Late at night, taking a hot shower alone, hiding behind the bathroom curtain, as if hiding in a greenhouse—or perhaps a petri dish. Not wanting to stop the water, not wanting to leave this enclosed space. I feel so much pressure. At times like these, I often do things to relieve stress and anxiety, but the process of relief wastes time, which only adds to my anxiety. Being a programmer often means facing such situations. The imagined life is filled with glamorous phrases like “financial freedom,” but in reality, we’re stuck in the daily grind.</p>
<p>I suddenly remembered a phrase I once read: “Think more about how to build the product, less about how to be a product manager.” I think this can be extended to programming: “Think more about how to write programs, less about how to be a programmer.” That’s right—you don’t need to become a programmer, spending most of your day staring at a screen with swollen eyes. Your body gradually becoming out of shape, wanting to exercise but never finding the time. An increasingly rigid, mechanical way of thinking, and less and less contact with people. The ideal is beautiful, but reality is so disappointing. After all, programmers of the previous era had so many opportunities for financial freedom. So many stories of wealth creation, even world-changing tales, happened to programmers. But that era has passed. We can only watch its afterglow as the sun slowly sets.</p>
<p>Recently, while preparing to change jobs, I discovered that the interview prep materials are full of things I don’t know. It’s hard not to feel anxious. And I noticed that even summarizing interview prep has become someone’s thriving side business and personal brand. Envy and jealousy—these are negative emotions programmers often have. You wonder why that person is better than you; why they can work so hard while you can’t; why some things come naturally to them but not to you. The longing for opportunities and uncertainty makes this industry exciting. It inflates your jealousy endlessly, making you believe that through effort you can gain infinitely, even making you feel that the distance between yourself and the world, between yourself and your dreams, is constantly shrinking. But is that really true? We always have all kinds of pressure. Society has developed quite a stereotypical image of programmers. How I wish there wasn’t so much overtime. How I wish life could be easy while also becoming a great developer, showing off online and being envied by others.</p>
<p>With such desires, the work of a programmer can feel cold and abstract. Exceptional comprehension, even wisdom, yet losing so much rich emotion. Life becomes increasingly monotonous, even childish.</p>
<p>Why did I choose to become a programmer? After finishing my shower, I looked at the night sky outside the window and asked myself. I think it was the inspiration and sentiment from various startup stories, and also a love for abstraction. A program is essentially a simpler language, yet with this language we can describe all kinds of problems and solutions in the world. Language is so important—just as I’m writing this article in Chinese right now. Language—when we discuss programming, what are we really discussing? We discuss programming itself: how code abstracts reality, how to design, what makes a good or bad design. But we’re not discussing programmers.</p>
<p>Can you be a programmer and still love life? Of course you can. I don’t know how, but everyone has their own solution. Though I’m also anxious about my future—I don’t know how many more years I can stay in this industry, when I’ll become obsolete, what my income will be, what kind of life I’ll live, or how many layoffs await me. But this uncertainty brings so much excitement. Disappointment and excitement rise and fall like tides. This is what makes this industry special—risk and reward always come together.</p>
<p>Embrace more uncertainty. Embrace true passion. Use language to reorganize life. Love code, love programming, love structure and design patterns—but don’t become what people call a programmer.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Key Iterations: Trustworthy Online Controlled Experiments</title>
    <link>https://www.yujiachen.com/key-iterations-trustworthy-online-controlled-experiments/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/key-iterations-trustworthy-online-controlled-experiments/</guid>
    <pubDate>Fri, 24 Dec 2021 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>This article is a summary with excerpts from <em>Trustworthy Online Controlled Experiments</em> (the Chinese edition is titled “Key Iterations”). More specifically, these are notes from an Airbnb reader on a technical book whose authors and translators are also from Airbnb. Please point out any errors; comments are welcome.</p>
<blockquote>
<p>If all you have is a hammer, everything looks like a nail. —Abraham Maslow</p>
</blockquote>
<h1 id="preparing-for-tests">Preparing for Tests</h1>
<h2 id="to-succeed-fail-more">To Succeed, Fail More</h2>
<p>The cognitive shift brought by experiments is also related to the gap between expectations and reality.</p>
<p>If you think something will happen and it does, you won’t learn much. If you think something will happen but it doesn’t, you’ll learn something important. If you originally thought something was insignificant but it produced surprising or breakthrough results, you’ll learn something extremely valuable.</p>
<p>Teams develop a product feature because they believe it’s useful. However, in many domains, most ideas fail to improve key metrics. At Microsoft, only one-third of tested ideas improved target metrics. In already highly optimized product areas like Bing and Google, success is even harder—test success rates are only 10%-20%. Bing’s team of hundreds of ranking algorithm engineers has an annual goal of improving a single OEC metric by 2%.</p>
<h2 id="testing-is-tactical-decision-making-is-strategic">Testing Is Tactical, Decision-Making Is Strategic</h2>
<p>When testing bold ideas, the way experiments are run and evaluated also changes.</p>
<ul>
<li><strong>Experiment duration</strong>: A small temperature increase won’t cause change in a room, but once the melting point is reached, ice begins to melt.</li>
<li><strong>Number of ideas tested</strong>: Each experiment tests only one specific tactic—a component of the overall strategy. Individual experiment failures don’t mean the overall strategy is flawed. But if many tactics evaluated through controlled experiments fail, it’s likely a strategy problem. Using experiments to validate strategy is extremely costly; strategy should primarily be informed by existing data.</li>
</ul>
<p>One interpretation of OKR is: define the strategic O (Objective), then derive a series of tactics from the KRs (Key Results) to achieve those KRs and ultimately feed back into O.</p>
<h2 id="what-are-significant-results">What Are Significant Results?</h2>
<p>Not all statistically significant results have practical meaning. Take revenue per user as an example: how large a difference is important from a business perspective? In other words, what level of change is <strong>practically significant</strong>? Building this substantive boundary is important—it helps understand whether a difference is worth the cost of the corresponding change. If your website, like Google and Bing, has billions of dollars in revenue, a 0.2% change is practically significant. By comparison, a startup might consider even 2% growth too small, as they’re pursuing 10% or greater improvements.</p>
<p>In 2012, every 10-millisecond performance improvement at Bing (1/30th of a blink) was enough to justify the cost of hiring a full-time engineer for a year. By 2015, that number had dropped to 4 milliseconds.</p>
<h1 id="setting-up-tests">Setting Up Tests</h1>
<h2 id="variant-assignment">Variant Assignment</h2>
<p>How to assign experiments to users:</p>
<ul>
<li><strong>Single-layer method</strong>: Bucket users—say into 1,000 buckets—and use two user buckets per experiment.</li>
<li><strong>Parallel experiments</strong>:
<ul>
<li>Divide the code architecture into layers, with only one experiment per layer.</li>
<li>Don’t worry about experiments interfering with each other; assume all experiments are orthogonal. If non-orthogonal experiments exist, rely on the execution team to discover and resolve conflicts.</li>
</ul>
</li>
</ul>
<h2 id="metric-classification">Metric Classification</h2>
<ul>
<li><strong>Goal metrics</strong>, also called <strong>success metrics</strong> or <strong>North Star metrics</strong>, reflect what the organization ultimately cares about. Being able to clearly articulate your goal in words is important, because the translation from goal to metric is usually imperfect. Your goal metric may only be an approximation of what you truly care about and needs iterative improvement over time. Helping people understand this limitation—and the distinction between the metric and the goal statement—is crucial for keeping the company on the right track.</li>
<li><strong>Driver metrics</strong>, also called <strong>signpost metrics</strong>, <strong>proxy metrics</strong>, <strong>indirect metrics</strong>, or <strong>predictive metrics</strong>, are generally shorter-term than goal metrics, changing faster and more sensitively. Driver metrics reflect a mental causal model—a hypothesis about how to make the organization more successful, i.e., assumptions about the drivers of success, rather than what success itself looks like.</li>
<li><strong>Guardrail metrics</strong> ensure we make the right trade-offs on the path to success and don’t violate important constraints.</li>
</ul>
<h2 id="metric-selection">Metric Selection</h2>
<p>Metric selection, beyond statistical validity, also depends on the designer’s value choices (“people are ends, not means”). Metrics represent how you want the system to understand data and what you care about. For example, use P95/P99 to focus on extreme values, or mean and median for overall trends.</p>
<h3 id="proxy-metrics">Proxy Metrics</h3>
<p>Some subscription services renew on an annual basis. Unless you’re willing to run a year-long experiment, it’s hard to measure the impact on renewal rates. In such cases, we can’t use renewal rate as the experiment metric and instead need to find proxy metrics, such as service usage, which can indicate user satisfaction early and ultimately influence renewal rates.</p>
<h3 id="metric-normalization">Metric Normalization</h3>
<p>Even if you want to increase total revenue, it’s not recommended to use total revenue as the metric, since it depends on the number of users in each variant. Even with equal allocation, actual user counts may differ due to randomness. We recommend normalizing key metrics by actual sample size—hence <strong>revenue per user</strong> is a good overall evaluation criterion.</p>
<h3 id="duality">Duality</h3>
<p>Sometimes it’s simpler to precisely measure what you <em>don’t</em> want rather than what you do want—such as user dissatisfaction or unhappiness. Establishing causal relationships from observational data is difficult, but a carefully conducted observational study can help disprove false hypotheses.</p>
<h3 id="preventing-sub-metric-conflicts">Preventing Sub-Metric Conflicts</h3>
<p>Carefully examine each sub-metric under a major metric—sub-metrics may conflict with each other. For example, search engine queries per user = sessions per user × unique queries per session.</p>
<p>A session starts when a user begins their first query and ends when the user has no activity on the search engine for 30 minutes.</p>
<ul>
<li><strong>Sessions per user</strong> is most likely a positive metric—the more users like the search engine, the more frequently they use it, increasing sessions per user.</li>
<li><strong>Unique queries per session</strong> should decrease, which conflicts with the overall goal. A decrease in unique queries per session means users need fewer steps to solve their problem—but it could also mean users are abandoning their queries. We should ideally pair this metric with a query abandonment metric.</li>
</ul>
<h2 id="pitfalls">Pitfalls</h2>
<h3 id="beware-of-extreme-results">Beware of Extreme Results</h3>
<p>When we see surprisingly positive results (e.g., major improvements to key metrics), we tend to construct a narrative around them, share, and celebrate. When results are surprisingly negative, we tend to find some limitation or minor flaw in the study and dismiss it. Experience tells us that many extreme results are more likely caused by instrumentation (e.g., logging) errors, data loss (or duplication), or calculation errors.</p>
<h3 id="simpson-s-paradox">Simpson’s Paradox</h3>
<p>Two groups of data that satisfy a certain property when examined separately may lead to the opposite conclusion when combined.</p>
<p><a href="https://en.wikipedia.org/wiki/Simpson%27s_paradox">Simpson’s paradox - Wikipedia</a></p>
<p>When an experimental feature causes individuals to migrate between two mutually exclusive and exhaustive segments, similar situations can occur (though the book emphasizes this isn’t Simpson’s paradox per se).</p>
<p>For example, a feature that moves Level 2 users back to Level 1 might improve both levels’ data—Level 2’s user pool removes poorly performing users while Level 1’s pool gains better-performing users—but overall performance across both levels may stay the same or even worsen.</p>
<p>Ideally, segmentation should only use values determined before the experiment, so the experiment doesn’t cause users to change segments. In practice, however, this is hard to enforce in some cases.</p>
<h3 id="changing-user-base">Changing User Base</h3>
<p>When computing metrics or running experiments, all data comes from the existing user base. Especially for early-stage products and startups, early users may not represent the user base the business hopes to acquire for long-term growth.</p>
<h3 id="uncertain-confounding-factors">Uncertain Confounding Factors</h3>
<p>Shared resources and dependencies can cause experiments to fail:</p>
<ul>
<li><strong>Market resources</strong>: Homestays and ride-sharing have this problem—once supply becomes constrained, one group capturing too many resources deprives the other group of its original resources.</li>
<li><strong>Advertising campaign budgets</strong>.</li>
<li><strong>Model training for recommendation systems</strong>: Once both models are trained on full user data, knowledge sharing between the two models is likely within days.</li>
<li><strong>CPU or other computational resources</strong>.</li>
</ul>
<h3 id="others">Others</h3>
<p>Goodhart’s Law: When a measure becomes a target, it ceases to be a good measure.</p>
<p>The Lucas Critique observes that relationships found in historical observational data cannot be considered structural or causal. Policy decisions change the structure of economic models, so historical correlations no longer hold. Over time, even the causal relationships we previously relied on may change.</p>
<h2 id="long-running-experiments">Long-Running Experiments</h2>
<p>Reasons why short-term and long-term experiment results may differ:</p>
<ul>
<li>User learning effects.</li>
<li>Network effects.</li>
<li>Delayed experience and evaluation.</li>
<li>Ecosystem changes:
<ul>
<li>Launching other new features.</li>
<li>Seasonality.</li>
<li>Competitive landscape.</li>
<li>Government policies.</li>
<li>Concept drift.</li>
<li>Software performance degradation.</li>
</ul>
</li>
</ul>
<p>Methods for improving long-running experiments:</p>
<ul>
<li><strong>Cohort analysis</strong>: Long-term tracking of a stable cohort’s user data.</li>
<li><strong>Post-period analysis</strong>: After running for a period, close the experiment (or roll it out to all users), then continue measuring the difference between control and treatment groups.</li>
<li><strong>Time-staggered experiments</strong>: Use experiment start time t as a variable to create multiple treatment groups, comparing user performance differences across different values of t.</li>
<li><strong>Holdback and reversal experiments</strong>:
<ul>
<li>Holdback: Keep 10% of users in the control group.</li>
<li>Reverse: After several months, move those 10% back into the control group. Then measure user performance.</li>
</ul>
</li>
</ul>
<h2 id="ethics">Ethics</h2>
<ul>
<li><strong>Respect for persons</strong>: Respect experiment participants as autonomous individuals and protect them when they lack this capacity. This principle focuses on transparency, truthfulness, and voluntariness (choice and consent).</li>
<li><strong>Beneficence</strong>: Protect people from harm. Properly assess risks and benefits, and appropriately balance them when reviewing proposed research.</li>
<li><strong>Justice</strong>: Ensure participants are not exploited and that risks and benefits are fairly distributed.</li>
</ul>
<h2 id="an-example-designing-a-slowdown-experiment">An Example: Designing a Slowdown Experiment</h2>
<p>Why can a slowdown experiment measure the impact of speed improvement on a product?</p>
<p>Assume that the relationship between a relevant metric (e.g., revenue) and performance (e.g., speed) can be well approximated by a linear fit near the current value. This is essentially a first-order Taylor expansion, or linear approximation.</p>
<p>That is, if we improve performance, the resulting metric change can be approximated by the metric change obtained from degrading performance.</p>
<p>Two additional reasons support this assumption:</p>
<ul>
<li>From our own experience as users, faster is always better for website speed.</li>
<li>We can also verify the linearity assumption—for example, test the metrics at 100ms delay and 250ms delay, and check whether the metrics at these two points follow a linear relationship.</li>
</ul>
<h2 id="alternative-methods">Alternative Methods</h2>
<p>How do we measure counterfactuals? Consider an experiment with a human population as test subjects. To determine the deviation between counterfactual and reality:</p>
<pre class="hljs"><code>  Outcome of affected group - Outcome of unaffected group
= (Outcome of affected group - Outcome of affected group had they not been affected) +
  (Outcome of affected group had they not been affected - Outcome of unaffected group)
= Effect of the change on the affected group + Selection bias
</code></pre>
<p>What we can observe is the performance of affected vs. unaffected groups, but what we want to know is the effect of the change on the affected group. So we want the selection bias in the system to be zero.</p>
<p>A/B testing is a system with zero selection bias, but in many real situations we can’t apply A/B testing to real problems—for example, there’s no randomization unit, the experiment would waste significant (opportunity) costs, or the experiment would be unethical.</p>
<p>These methods generally serve as alternatives to A/B testing, but they usually have larger estimation error and bias:</p>
<ul>
<li><strong>Interrupted time series</strong>: Split the experiment time into many small segments, uniformly (and randomly, or just round-robin) assign each segment to different treatments, then measure and analyze (some time-series models can be useful, such as Bayesian structural time series analysis).</li>
<li><strong>Interleaved experiments</strong>: Often used for ranking algorithms—show Algorithm A at odd positions and Algorithm B at even positions to compare differences.</li>
<li><strong>Regression discontinuity design</strong>: Consider this method when the affected population is identified by a clear threshold. Use the group just below the threshold as the control and the group just above as the treatment, comparing these two groups to reduce selection bias. For example, studying the impact of scholarships on students where students scoring 80+ receive scholarships—we study the 80-84 and 75-79 score groups.</li>
<li><strong>Instrumental variables and natural experiments</strong>: If we can find an instrument within the studied process that helps achieve random grouping, we can use it for causal analysis. For example, draft lotteries for military conscription or school assignment—if the lottery process is random, it can serve as the randomization unit.</li>
<li><strong>Propensity score matching</strong>: Although we can’t randomize, if we can identify confounding factors (covariates) between groups that might affect our judgment of the variable, or if we can determine that unidentifiable confounding factors won’t affect analysis, then we can analyze causal relationships between variables and outcomes. This is usually very difficult to achieve.</li>
<li><strong>Difference-in-differences</strong>: Identify a control group as similar as possible to the treatment group, assuming both groups share the same trends. For example, select two cities with similar characteristics as control cities—enable the new feature in one but not the other, then compare differences between the two cities.</li>
</ul>
<p>These types of analyses should be used carefully, as they may fail due to:</p>
<ul>
<li><strong>Common causes</strong>: Smaller palms correlate with longer lifespans—actually, women live longer.</li>
<li><strong>Spurious or deceptive correlations</strong>: The length of words in the National Spelling Bee positively correlates with the number of people killed by venomous spiders that year—but this is obviously spurious.</li>
</ul>
<h1 id="mathematical-derivation">Mathematical Derivation</h1>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/key-iterations-trustworthy-online-controlled-experiments/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/key-iterations-trustworthy-online-controlled-experiments/en/image_1.png" alt="" class="article-image" width="1055" height="506" />
</picture>
</p>
<p>We use a two-sample t-test to calculate the p-value, which matches the actual experimental setup.</p>
<section><eqn><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>T</mi><mo>=</mo><mfrac><mi>Δ</mi><msqrt><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mi>Δ</mi><mo stretchy="false">)</mo></mrow></msqrt></mfrac></mrow><annotation encoding="application/x-tex"> T = \frac{\mathit{\Delta}}{\sqrt{\text{var} (\mathit{\Delta})}} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.4903em;vertical-align:-1.13em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.175em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.935em;"><span class="svg-align" style="top:-3.2em;"><span class="pstrut" style="height:3.2em;"></span><span class="mord" style="padding-left:1em;"><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathit">Δ</span><span class="mclose">)</span></span></span><span style="top:-2.895em;"><span class="pstrut" style="height:3.2em;"></span><span class="hide-tail" style="min-width:1.02em;height:1.28em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.28em" viewBox="0 0 400000 1296" preserveAspectRatio="xMinYMin slice"><path d="M263,681c0.7,0,18,39.7,52,119
c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120
c340,-704.7,510.7,-1060.3,512,-1067
l0 -0
c4.7,-7.3,11,-11,19,-11
H40000v40H1012.3
s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232
c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1
s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26
c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z
M1001 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.305em;"><span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathit">Δ</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.13em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><p>Where <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Δ</mi><mo>=</mo><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover><mo>−</mo><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover></mrow><annotation encoding="application/x-tex">\mathit{\Delta} = \overline{Y^t} - \overline{Y^c}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathit">Δ</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.0029em;vertical-align:-0.0833em;"></span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8833em;"></span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span></eq> is the difference between the treatment group mean and control group mean. <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover></mrow><annotation encoding="application/x-tex">\overline{Y^t}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9196em;"></span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span></eq> and <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover></mrow><annotation encoding="application/x-tex">\overline{Y^c}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8833em;"></span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span></eq> follow normal distributions due to the Central Limit Theorem.</p>
<p>Considering the variance formula for independent variables <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mi>a</mi><mi>x</mi><mo>+</mo><mi>b</mi><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><msup><mi>a</mi><mn>2</mn></msup><mtext>var</mtext><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup><mtext>var</mtext><mo stretchy="false">(</mo><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\text{var}(ax+by) = a^2\text{var}(x) + b^2\text{var}(y)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathnormal">a</span><span class="mord mathnormal">x</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">b</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">a</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathnormal">x</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span></eq>:</p>
<section><eqn><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mi>Δ</mi><mo stretchy="false">)</mo><mo>=</mo><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover><mo>−</mo><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo><mo>=</mo><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo><mo>+</mo><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex"> \text{var}(\mathit{\Delta}) = \text{var}(\overline{Y^t} - \overline{Y^c}) = \text{var}(\overline{Y^t}) + \text{var}(\overline{Y^c}) </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathit">Δ</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1696em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1333em;vertical-align:-0.25em;"></span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1696em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1.1333em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></eqn></section><p>Where <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><mi>Y</mi><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo><mo>=</mo><mtext>var</mtext><mo stretchy="false">(</mo><mfrac><mn>1</mn><mi>n</mi></mfrac><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></msubsup><msub><mi>Y</mi><mi>i</mi></msub><mo stretchy="false">)</mo><mo>=</mo><mfrac><mn>1</mn><msup><mi>n</mi><mn>2</mn></msup></mfrac><mo>∗</mo><mi>n</mi><mo>∗</mo><mtext>var</mtext><mo stretchy="false">(</mo><mi>Y</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mi>Y</mi><mo stretchy="false">)</mo></mrow><mi>n</mi></mfrac></mrow><annotation encoding="application/x-tex">\text{var}(\overline Y) = \text{var}(\frac 1n \sum^n_{i=1}Y_i)=\frac 1 {n^2} * n * \text{var}(Y) = \frac{\text{var}(Y)}n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1333em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop"><span class="mop op-symbol small-op" style="position:relative;top:0em;">∑</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8043em;"><span style="top:-2.4003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.2029em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2997em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.1901em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8451em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7463em;"><span style="top:-2.786em;margin-right:0.0714em;"><span class="pstrut" style="height:2.5em;"></span><span class="sizing reset-size3 size1 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4653em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1.355em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.01em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.485em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord mtight">var</span></span><span class="mopen mtight">(</span><span class="mord mathnormal mtight" style="margin-right:0.22222em;">Y</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></eq></p>
<p>The p-value is essentially a conditional probability: “the probability of observing the current difference or an even larger one, given that the null hypothesis is true,” i.e., <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mo>∣</mo><msub><mi>H</mi><mn>0</mn></msub><mtext> is true</mtext><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">P(\text{observing } \mathit{\Delta}\mid H_0 \text{ is true})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">∣</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> is true</span></span><span class="mclose">)</span></span></span></span></eq>. The <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Δ</mi></mrow><annotation encoding="application/x-tex">\mathit{\Delta}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathit">Δ</span></span></span></span></eq> here is represented by the normalized statistic <eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>T</mi></mrow><annotation encoding="application/x-tex">T</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span></span></span></span></eq>. This is why p-values have a common interpretation across experiments under the null.</p>
<p>Some people mistakenly interpret the p-value as “the probability that the null hypothesis is true, given that we observed the current or larger difference.” The implicit condition “observing the current or larger difference” has a probability specific to each experiment and lacks universality. The relationship can be derived using Bayes’ theorem:</p>
<section><eqn><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mtable rowspacing="0.25em" columnalign="right left" columnspacing="0em"><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mi>P</mi><mo stretchy="false">(</mo><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mi mathvariant="normal">∣</mi><mtext>observing </mtext><mi>Δ</mi><mo stretchy="false">)</mo></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mfrac><mrow><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mi mathvariant="normal">∣</mi><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mo stretchy="false">)</mo><mi>P</mi><mo stretchy="false">(</mo><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mo stretchy="false">)</mo></mrow><mrow><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mo stretchy="false">)</mo></mrow></mfrac></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mfrac><mrow><mi>P</mi><mo stretchy="false">(</mo><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mo stretchy="false">)</mo></mrow><mrow><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mo stretchy="false">)</mo></mrow></mfrac><mo>∗</mo><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mi mathvariant="normal">∣</mi><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mo stretchy="false">)</mo></mrow></mstyle></mtd></mtr><mtr><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow></mrow></mstyle></mtd><mtd><mstyle scriptlevel="0" displaystyle="true"><mrow><mrow></mrow><mo>=</mo><mfrac><mrow><mi>P</mi><mo stretchy="false">(</mo><msub><mi>H</mi><mn>0</mn></msub><mtext> true</mtext><mo stretchy="false">)</mo></mrow><mrow><mi>P</mi><mo stretchy="false">(</mo><mtext>observing </mtext><mi>Δ</mi><mo stretchy="false">)</mo></mrow></mfrac><mo>∗</mo><mtext>p-value</mtext></mrow></mstyle></mtd></mtr></mtable><annotation encoding="application/x-tex"> \begin{align*} P(H_0 \text{ true}|\text{observing } \mathit{\Delta}) &amp;= \frac {P(\text{observing } \mathit{\Delta}|H_0 \text{ true})P(H_0 \text{ true})} {P(\text{observing } \mathit{\Delta})} \\ &amp;= \frac {P(H_0 \text{ true})}{P(\text{observing } \mathit{\Delta})} * P(\text{observing } \mathit{\Delta}|H_0 \text{ true}) \\ &amp;= \frac {P(H_0 \text{ true})}{P(\text{observing } \mathit{\Delta})} * \text{p-value} \end{align*} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:7.989em;vertical-align:-3.7445em;"></span><span class="mord"><span class="mtable"><span class="col-align-r"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.2445em;"><span style="top:-6.2445em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mord">∣</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mclose">)</span></span></span><span style="top:-3.5815em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"></span></span><span style="top:-0.9185em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:3.7445em;"><span></span></span></span></span></span><span class="col-align-l"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:4.2445em;"><span style="top:-6.2445em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mclose">)</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mord">∣</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mclose">)</span><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span><span style="top:-3.5815em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mclose">)</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mord">∣</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mclose">)</span></span></span><span style="top:-0.9185em;"><span class="pstrut" style="height:3.427em;"></span><span class="mord"><span class="mord"></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.427em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord text"><span class="mord">observing </span></span><span class="mord mathit">Δ</span><span class="mclose">)</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3011em;"><span style="top:-2.55em;margin-left:-0.0813em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">0</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord text"><span class="mord"> true</span></span><span class="mclose">)</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord text"><span class="mord">p-value</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:3.7445em;"><span></span></span></span></span></span></span></span></span></span></span></span></eqn></section><h2 id="variance-related-issues">Variance-Related Issues</h2>
<p>P-value calculation depends on variance. Common issues with variance estimation:</p>
<h3 id="variance-calculation-for-percentage-deltas-and-ratio-metrics">Variance Calculation for Percentage Deltas and Ratio Metrics</h3>
<p>Note that if the metric’s meaning and the analysis unit differ, values may not be independently and identically distributed (i.i.d.). For example, calculating page conversion rate while the experimental unit is the user—users visiting a page multiple times makes page-level data non-i.i.d. In this case, variance needs to be calculated from the numerator and denominator of the ratio formula separately, not by first computing the ratio and then calculating variance.</p>
<p>The correct calculation: we express the ratio metric as the ratio of two user-level metric averages:</p>
<section><eqn><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>M</mi><mo>=</mo><mfrac><mover accent="true"><mi>X</mi><mo stretchy="true">‾</mo></mover><mover accent="true"><mi>Y</mi><mo stretchy="true">‾</mo></mover></mfrac></mrow><annotation encoding="application/x-tex"> M = \frac {\overline X} {\overline Y} </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.10903em;">M</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.3337em;vertical-align:-0.7733em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.5603em;"><span style="top:-2.2267em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07847em;">X</span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7733em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></eqn></section><p>Via the delta method (skipping the derivation), the final variance formula is:</p>
<section><eqn><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>var</mtext><mo stretchy="false">(</mo><mi>Δ</mi><mi mathvariant="normal">%</mi><mo stretchy="false">)</mo><mo>=</mo><mfrac><mn>1</mn><msup><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover><mn>2</mn></msup></mfrac><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo><mo>+</mo><mfrac><msup><mover accent="true"><msup><mi>Y</mi><mi>t</mi></msup><mo stretchy="true">‾</mo></mover><mn>2</mn></msup><msup><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover><mn>4</mn></msup></mfrac><mtext>var</mtext><mo stretchy="false">(</mo><mover accent="true"><msup><mi>Y</mi><mi>c</mi></msup><mo stretchy="true">‾</mo></mover><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex"> \text{var}(\mathit \Delta \%) = \frac 1 {\overline{Y^c}^2}\text{var}(\overline{Y^t}) + \frac{\overline{Y^t}^2}{\overline{Y^c}^4}\text{var}(\overline{Y^c}) </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord mathit">Δ</span><span class="mord">%</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.2988em;vertical-align:-0.9773em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3214em;"><span style="top:-2.11em;"><span class="pstrut" style="height:3.0873em;"></span><span class="mord"><span class="mord"><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:1.0873em;"><span style="top:-3.3362em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span><span style="top:-3.3173em;"><span class="pstrut" style="height:3.0873em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.7643em;"><span class="pstrut" style="height:3.0873em;"></span><span class="mord"><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9773em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:2.7779em;vertical-align:-0.9773em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.8006em;"><span style="top:-2.1462em;"><span class="pstrut" style="height:3.1236em;"></span><span class="mord"><span class="mord"><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:1.0873em;"><span style="top:-3.3362em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">4</span></span></span></span></span></span></span></span></span></span><span style="top:-3.3536em;"><span class="pstrut" style="height:3.1236em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.8006em;"><span class="pstrut" style="height:3.1236em;"></span><span class="mord"><span class="mord"><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.9196em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7196em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">t</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8396em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:1.1236em;"><span style="top:-3.3725em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.9773em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span><span class="mord text"><span class="mord">var</span></span><span class="mopen">(</span><span class="mord overline"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8833em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.5904em;"><span style="top:-2.989em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span></span></span></span></span></span></span><span style="top:-3.8033em;"><span class="pstrut" style="height:3em;"></span><span class="overline-line" style="border-bottom-width:0.04em;"></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></eqn></section><h3 id="outliers">Outliers</h3>
<p>Outliers can interfere with experiments. A reasonable threshold can be added to observed metrics; other methods can be researched independently.</p>
<h3 id="improving-sensitivity">Improving Sensitivity</h3>
<p>Lower variance means higher significance. These methods can reduce variance:</p>
<ul>
<li><strong>Transform metrics through thresholding, binarization, and log transformation</strong>: Purchase amounts have larger variance than whether a purchase was made; per-user watch time has larger variance than whether viewing exceeded x hours.</li>
<li><strong>Remove noise through trigger analysis</strong>.</li>
<li><strong>Stratify within the sampling scope through stratified sampling, control variables, or CUPED</strong>. Combine results from each stratum to get the total—variance calculated this way is smaller.</li>
<li><strong>Choose finer-grained randomization units</strong>.</li>
<li><strong>Shared control group</strong>: Calculate an appropriately sized control group and compare it against multiple treatment groups. This reduces variance and can improve statistical power for all treatment groups, though it introduces some issues.</li>
</ul>
<h2 id="p-value-threshold-issues">P-Value Threshold Issues</h2>
<p>Beware of this fact: when the significance threshold is 0.05, testing a no-op feature (where the null hypothesis is true) 100 times means that, statistically, about 5 tests will incorrectly conclude significance (false positives). Here we introduce two concepts:</p>
<ul>
<li><strong>Type I error</strong>: The measurement shows a significant difference, but in reality there is no difference.</li>
<li><strong>Type II error</strong>: The measurement shows no significant difference, but in reality there is a difference.</li>
</ul>
<p>A smaller significance threshold (<eq><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>α</mi></mrow><annotation encoding="application/x-tex">\alpha</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.0037em;">α</span></span></span></span></eq>) reduces Type I errors but increases Type II errors, and vice versa.</p>
<p>To reduce the aforementioned “5 out of 100 tests incorrectly concluding significance” Type I errors, we can apply a stricter significance threshold to metrics confirmed to be unrelated (or indirectly related) to the experiment. <strong>The more confidence we need, the lower the significance threshold should be.</strong></p>
<p>Conversely, for sensitive guardrail metrics where missing a real change is costly, we may use a less strict significance threshold together with explicit power targets. Especially for metrics where we explicitly require that fluctuations not exceed x%, we should follow the industry principle that tests should have 80% statistical power (statistical power = 1 - Type II error rate; see specific approximation formulas elsewhere), and calculate thresholds based on x.</p>
<h2 id="how-large-should-sample-size-n-be">How Large Should Sample Size n Be?</h2>
<p>This section overlaps somewhat with the sensitivity improvement section.</p>
<p>What’s the theoretical basis for “larger n is better”? In practice, many t-test settings rely on the sample mean being approximately normal. According to the <strong>Central Limit Theorem</strong>, under appropriate conditions, the mean of a large number of mutually independent random variables converges in distribution to a normal distribution after proper standardization. So n needs to be sufficiently large.</p>
<p>The statistical power approximation formulas mentioned earlier can help calculate n given certain conditions, but these formulas require specifying the effect size x%. This is only suitable for some guardrail metrics.</p>
<p>To better approximate normality of the mean, we can also use empirical formulas based on skewness to calculate n. Metrics with large variation often also have high skewness. We can reduce skewness and n by capping differences (e.g., treating all values above 10 as 10), as long as capping doesn’t violate our assumptions.</p>
<p>We can also verify by constructing a null distribution, but the concepts involved are more complex, so we’ll skip that here.</p>
<h2 id="tbd-why-choose-the-t-distribution">TBD: Why Choose the t-Distribution?</h2>
<p>This requires comparing the characteristics of several sampling distributions—refer to the summaries in university textbooks.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Transactions and Consensus from DDIA</title>
    <link>https://www.yujiachen.com/transactions-and-consensus-from-ddia/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/transactions-and-consensus-from-ddia/</guid>
    <pubDate>Mon, 27 Sep 2021 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<h1 id="some-reflections">Some Reflections</h1>
<p>Most applications are built by layering data models on top of one another.</p>
<p>The problem of surplus and scarcity of computing resources will always exist. “Resource-saving technologies only lead to increased resource usage” (<a href="https://en.wikipedia.org/wiki/Jevons_paradox">Jevons paradox</a>).</p>
<p>Many problems discussed in the book follow a pattern: under constraint P (the real-world problem), find the lowest-cost C (consistency level) that still achieves the best A (availability/outcome). One example in the book is multi-core CPUs. While multi-core CPUs can be viewed as distributed systems, they are not constrained by inter-machine network latency, so partition tolerance is effectively assumed. Even so, to maximize throughput, multi-core designs may still trade some consistency for higher availability, accepting occasional redundant or incorrect computation to squeeze out more performance.</p>
<h1 id="are-paradigms-useful">Are Paradigms Useful?</h1>
<p>Paradigms exist to standardize how we use data. But as hardware improves and application scenarios expand, breaking those rules can sometimes bring bigger gains. Otherwise NoSQL would not have emerged: store a large JSON document, skip strict relational modeling, and iteration speed can increase dramatically.</p>
<p>So should we still follow paradigms? Actually, paradigms are just a yardstick that tells us what to do in which scenario and what happens if we don’t—not rules we must rigidly follow.</p>
<h1 id="what-are-concurrency-problems">What Are Concurrency Problems?</h1>
<p>A single machine with a single thread can always avoid various concurrency problems, but this approach is too slow. So we develop multi-threading, multi-processing, multi-machine, and distributed systems. Along with these come concurrency problems—problems that fit the following condition:</p>
<blockquote>
<p>Under identical conditions, if the results of two tasks executed serially differ from those executed in parallel, a race condition has occurred, causing a concurrency problem.</p>
</blockquote>
<h1 id="single-machine-concurrency-problems">Single-Machine Concurrency Problems</h1>
<p>Let’s first discuss concurrency problems that arise even with a single machine running multiple threads/processes.</p>
<p>Discussion model:</p>
<ol>
<li>There are exactly 2 transactions involved in the problem scenario.</li>
<li>For real-world problems caused by more than 2 transactions, we use induction to reduce the problem to 2 concurrent transactions. For example, we group multiple non-conflicting transactions into 1, or split the problem into different causes, each corresponding to 2 transactions.</li>
</ol>
<p>Scenario 1—Transaction 1 is reading, Transaction 2 is writing, leading to:</p>
<ul>
<li>Dirty reads</li>
<li>Non-repeatable reads / Read skew</li>
</ul>
<p>Scenario 2—Both transactions are writing, leading to:</p>
<ul>
<li>Lost updates</li>
<li>Dirty writes</li>
<li>Write skew</li>
</ul>
<h2 id="dirty-reads">Dirty Reads</h2>
<blockquote>
<p>Prerequisite: One transaction reads partial modification results of another uncommitted transaction.</p>
</blockquote>
<p>Without read-committed isolation, dirty reads occur in these scenarios:</p>
<ul>
<li>One transaction updates multiple objects while another transaction sees only some of the updated objects, not all.</li>
<li>One transaction aborts, and during rollback another transaction sees partially un-rolled-back objects.</li>
</ul>
<p>Examples:</p>
<p>Example 1: Alice has two bank accounts—Account 1 has 100 yuan and Account 2 has 0. Account 1 transfers 100 yuan to Account 2. The transaction deducts 100 from Account 1 and adds 100 to Account 2. If Alice reads her total balance after the deduction from Account 1 but before the addition to Account 2, the result is 0. (Note: non-repeatable reads can also cause the same situation—we’ll discuss this in the non-repeatable reads section.)</p>
<p>Example 2: Building on Example 1, Account 2 is a Class II account with a single-transfer limit of 50 yuan. If the transfer exceeds the limit, the transaction rolls back. After deducting 100 from Account 1, another query reads Account 1’s balance as 0 yuan. Then the transfer fails and rolls back. Due to transaction atomicity, Account 1’s balance never actually became 0—yet at a certain moment, another transaction saw it as 0.</p>
<h2 id="dirty-writes">Dirty Writes</h2>
<blockquote>
<p>Prerequisite: One transaction modifies partial modification results of another uncommitted transaction.</p>
</blockquote>
<p>Without read-committed isolation, dirty writes occur in this scenario:</p>
<ul>
<li>Transaction 1 updates multiple objects while Transaction 2 modifies some of those objects (through updates, creates, deletes, or their rollbacks). Then Transaction 1 updates the remaining unmodified objects.</li>
</ul>
<p>Example:</p>
<p>On a trading website, a purchase updates both the item’s recipient and the payer. Alice and Bob simultaneously buy the same item. Alice’s transaction sets the recipient to Alice. Then Bob’s transaction changes the recipient to Bob and declares Bob as the payer. Then Alice’s transaction updates the payer to Alice. Result: Bob receives the item but Alice pays.</p>
<h2 id="non-repeatable-reads-read-skew">Non-Repeatable Reads / Read Skew</h2>
<blockquote>
<p>Prerequisite: Transaction 2’s execution starts after Transaction 1 begins but completes before Transaction 1 ends, and Transaction 1 reads Transaction 2’s modifications.</p>
</blockquote>
<p>Most business scenarios don’t want non-repeatable reads. These scenarios especially cannot tolerate them:</p>
<ul>
<li>Backups</li>
<li>Long-running analytical queries and integrity checks</li>
</ul>
<p>Non-repeatable reads are also called “read skew” because: ideally, a transaction should read all data in an instant. When non-repeatable reads occur, the transaction’s reads are spread across the timeline rather than happening at a single point—hence the read is “skewed.”</p>
<p>Example:</p>
<p>Continuing from dirty read Example 1: Alice starts a balance-reading Transaction 1 before initiating transfer Transaction 2. Transaction 1 first reads Account 2’s 0 yuan (this conforms to read-committed). Then Transaction 2 completes—Account 1 becomes 0, Account 2 becomes 100. Finally Transaction 1 reads Account 1’s 0 yuan (also conforming to read-committed). Total: 0 yuan.</p>
<h2 id="lost-updates">Lost Updates</h2>
<blockquote>
<p>Two transactions simultaneously execute read-modify-write sequences, where one overwrites the other’s write without incorporating the other’s latest value, ultimately causing some modified data to be lost.</p>
</blockquote>
<p>Examples:</p>
<ul>
<li>Incrementing counters</li>
<li>Modifying part of a complex object (e.g., multiple users editing a large JSON)</li>
</ul>
<h2 id="write-skew">Write Skew</h2>
<p>An escalated version of lost updates, differing in its greater dependency on application-layer logic. It follows this pattern:</p>
<ol>
<li><strong>Read</strong>: Input some matching conditions and query.</li>
<li><strong>Decide</strong>: Based on query results, application-layer code decides the next action.</li>
<li><strong>Write</strong>: If the application decides to proceed, it initiates a database write.</li>
</ol>
<p>Generally, if 2 transactions updating different objects cause errors (usually semantic errors at the application layer), that’s write skew. If they update the same object, it’s likely dirty writes or lost updates.</p>
<p>Why “write skew”: Referencing the meaning of read skew—ideally, a transaction’s write operations should happen in an instant. But when write skew occurs, a transaction typically reads stale data, the data gets modified, and the transaction unknowingly writes invalid data. The transaction’s reads and writes are spread across the timeline—hence the write is “skewed.”</p>
<p>Example: A user has an expense ledger with a balance constraint. Two transactions each insert expense items that individually don’t exceed the balance. But since neither notices the other, the combined expenses push the balance negative (application-layer logic violation).</p>
<p>Here we discuss one solution with limited feasibility:</p>
<h3 id="materializing-conflicts">Materializing Conflicts</h3>
<blockquote>
<p>Write skew often occurs because query results contain no objects, so there’s nothing to lock. Solution: pre-create lockable objects.</p>
</blockquote>
<p>For the balance example above, we create a total balance table with a new field for pending changes—meaning only one change can affect the balance at a time. This transforms the problem into a lost update or dirty write problem.</p>
<p>The main issue with materializing conflicts is excessive database storage. For instance, should we materialize all room-and-time combinations for the next 6 months in a meeting room booking system?</p>
<p><strong>This method is generally not used unless absolutely necessary.</strong></p>
<h2 id="phantom-reads">Phantom Reads</h2>
<blockquote>
<p>A write in one transaction changing the query results of another transaction is called a phantom read.</p>
</blockquote>
<p>Phantom reads are a highly general concept. I believe most concurrency problems discussed so far could be classified as phantom reads.</p>
<h1 id="preventing-lost-updates">Preventing Lost Updates</h1>
<blockquote>
<p>We must prevent lost updates to avoid many other phantom-read-like anomalies. As we’ll see, the key to distributed transactions is achieving consensus, and consensus is largely about preventing lost updates.</p>
</blockquote>
<p>Lost update scenarios are simpler than other concurrency problems, and the solutions are easier to understand. So we discuss lost update prevention first.</p>
<h2 id="atomic-write-operations">Atomic Write Operations</h2>
<p>If the DB supports atomic write operations, use them whenever possible.</p>
<p>The DB can implement atomic writes through exclusive locks or by executing all atomic operations on a single thread.</p>
<h2 id="explicit-locking">Explicit Locking</h2>
<p>The application explicitly locks relevant objects. DDIA uses the <code>FOR UPDATE</code> keyword to represent application-layer requests to lock <code>SELECT</code> results.</p>
<p>This approach might seem to solve write skew too, but write skew also includes cases like “checking whether rows matching a search condition exist (expected result: empty).” In that case, there is nothing concrete to lock. We discuss write-skew solutions in other sections.</p>
<hr>
<p>Above we discussed two lock-based solutions. Now let’s discuss lock-free solutions.</p>
<h2 id="automatically-detecting-lost-updates">Automatically Detecting Lost Updates</h2>
<p>Allow updates to execute concurrently. If the transaction manager detects a lost-update risk, it aborts the current transaction and retries using a safe read-modify-write path.</p>
<p>The database can use snapshot-level isolation for detection. A rough intuition is that an object can only have one uncommitted version at a given moment, though finer-grained detection methods likely exist.</p>
<h2 id="atomic-compare-and-set">Atomic Compare-and-Set</h2>
<p>Only allow updates when the data hasn’t changed since the last read. If it has changed, fall back to another read-modify-write approach or retry.</p>
<p>Implementation: CAS (compare-and-swap)—modern CPUs support this instruction. Or explicitly add conditions like <code>WHERE content = 'old content'</code> during execution. The danger is that if <code>WHERE</code> executes against a snapshot, the <code>content</code> value may not be the latest.</p>
<h2 id="conflict-resolution-and-replication">Conflict Resolution and Replication</h2>
<p>Details are discussed in the distributed concurrency section. The general idea: the application layer has logic, or data structures have logic, to handle conflicting writes. Or, design operations to be order-independent, so update conflicts naturally don’t occur.</p>
<h1 id="isolation-levels">Isolation Levels</h1>
<h2 id="read-committed">Read Committed</h2>
<blockquote>
<p>The most basic transaction isolation level: a transaction’s internal execution won’t be affected by other transactions.</p>
</blockquote>
<p>Solves:</p>
<ul>
<li>Dirty reads</li>
<li>Dirty writes</li>
</ul>
<p>Why “read committed”? My interpretation is: reads should observe only committed data.</p>
<p>Implementation methods:</p>
<h3 id="row-level-read-locks">Row-Level Read Locks</h3>
<p>Read locks certainly achieve read committed, but with drawbacks:</p>
<ul>
<li>Poor performance</li>
<li>Potential deadlocks</li>
</ul>
<h3 id="old-new-value-snapshots">Old/New Value Snapshots</h3>
<p>For each object pending update, maintain two versions: the old value and the new value the lock-holding transaction will set. Before the transaction commits, all other reads return the old value. Only after the write transaction commits does the system switch to the new value.</p>
<h3 id="multi-version-concurrency-control-mvcc">Multi-Version Concurrency Control / MVCC</h3>
<p>Discussed in the next section.</p>
<h2 id="snapshot-isolation">Snapshot Isolation</h2>
<blockquote>
<p>Each transaction reads a consistent snapshot of the database—once read, data doesn’t change.</p>
</blockquote>
<p>Prevents:</p>
<ul>
<li>Non-repeatable reads / Read skew</li>
</ul>
<h3 id="multi-version-concurrency-control-mvcc-2">Multi-Version Concurrency Control / MVCC</h3>
<p>The database maintains multiple committed versions of objects, adding <code>created_by</code> and <code>deleted_by</code> fields to each row, representing versions created by different transaction operations. A periodic garbage collection task cleans up versions no longer needed.</p>
<p>A transaction cannot see:</p>
<ul>
<li>Changes made by transactions still running when this transaction started</li>
<li>Changes made by any aborted transactions</li>
<li>Changes made by transactions that start after this transaction</li>
<li>All other changes are visible to this transaction (I understand “other changes” refers to non-transactional changes)</li>
</ul>
<p>Conversely, a transaction can see:</p>
<ul>
<li>Objects created or updated by already-committed transactions before this transaction started</li>
<li>Objects not deleted by uncommitted transactions before this transaction started</li>
</ul>
<p>MVCC indexing has roughly two implementation approaches:</p>
<ul>
<li><strong>Index points to all versions of an object</strong>: PostgreSQL uses this method, placing all versions on a single memory page for performance optimization.</li>
<li><strong>Persistent data structures</strong>: Typically a persistent B-tree. Different transactions create their own database entry nodes when writing. When reading, use the node corresponding to the latest committed transaction as the entry point.</li>
</ul>
<h2 id="serializable-isolation">Serializable Isolation</h2>
<blockquote>
<p>Even if transactions may execute in parallel, the final result is the same as if they executed one at a time (serially).</p>
</blockquote>
<p>Generally considered the strongest isolation level.</p>
<p>But achieving serializability in practice is very difficult. Here are three implementation approaches.</p>
<h3 id="actual-serial-execution">Actual Serial Execution</h3>
<p>With continuous hardware advances, database researchers have recognized that single-threaded transaction execution is feasible and efficient.</p>
<p>These conditions help achieve serializable isolation using memory and a single CPU:</p>
<ul>
<li>Transactions must be short and efficient—otherwise a slow transaction affects all others (since it’s single-threaded).</li>
<li>Limited to scenarios where the active dataset fits entirely in memory.</li>
<li>Write throughput must be low enough for a single CPU core to handle; otherwise partitioning is needed, ideally without cross-partition transactions.</li>
<li>Cross-partition transactions can be supported but must be a very small proportion.</li>
</ul>
<p>In practice, we can encapsulate transactions in stored procedures. Typical OLTP operations are short, and as long as user I/O is excluded from the transaction path, single-threaded execution can be very efficient. The business server packages logic as data and sends it to the database server, which executes it directly in memory. Historically, stored procedures were criticized because each database had its own language. A modern approach is to use general-purpose languages where possible (for example, Redis uses Lua).</p>
<h3 id="two-phase-locking">Two-Phase Locking</h3>
<p>The only widely used serialization algorithm for nearly 30 years.</p>
<ol>
<li>Use locking to achieve serializable isolation: transactions need shared locks before reading objects and exclusive locks before modifying them, excluding all other transactions from reading or writing the modified objects (two phases: acquire locks before start, release after end). The database system automatically detects deadlocks between transactions and forcibly terminates one to break the deadlock.</li>
<li>For the “nothing to lock” case discussed in the read-skew section (execute only when query results are empty), apply predicate locks. Predicate locks apply to all rows matching certain search conditions (similar to locking a <code>WHERE</code> predicate so overlapping ranges from concurrent transactions are disallowed). However, predicate locks are hard to implement and inefficient.</li>
<li>In practice, index-range locks often replace predicate locks by widening the protected scope. By locking one or more index ranges of queried objects, those ranges become exclusively locked. In the worst case, a single transaction may lock the whole table.</li>
</ol>
<h3 id="serializable-snapshot-isolation">Serializable Snapshot Isolation</h3>
<p>An optimistic-control algorithm proposed on top of MVCC in 2008, with limited real-world adoption. The DDIA author believes it will become a future standard in databases for these reasons:</p>
<ul>
<li>Pessimistic control shuts down too many transactions—retry costs are too high.</li>
<li>Hardware still has much room for improvement. In the future we’ll encounter fewer concurrency problems, so we should adopt optimistic control strategies.</li>
</ul>
<p>Building on MVCC snapshot isolation, we apply optimistic control with these principles:</p>
<ul>
<li>Before a transaction reads, check whether concurrent uncommitted writes could make that read stale.</li>
<li>Before a transaction commits, check whether writes that occurred after its read phase could create conflicts with other transactions.</li>
</ul>
<p>If conflicts are found, roll back.</p>
<p>Serializable Snapshot Isolation (SSI) relies on SSI locks, similar to index-range locks, but SSI locks only record—they don’t block. After a transaction commits, SSI locks notify other related transactions and are discarded.</p>
<p>The implementation tradeoff is lock granularity: too coarse may misjudge conflicts and expand a transaction’s impact; too fine may cause excessive metadata overhead.</p>
<h1 id="distributed-system-challenges">Distributed System Challenges</h1>
<ul>
<li>Network latency</li>
<li>Clock synchronization</li>
<li>Process pausing/crashing</li>
</ul>
<p>Why go distributed?</p>
<ul>
<li>Scalability</li>
<li>Fault tolerance</li>
<li>Low latency</li>
<li>If you can avoid opening Pandora’s box, keeping everything on one machine is worth trying</li>
</ul>
<p>Properties:</p>
<ul>
<li><strong>Safety</strong>: Properties that must never be violated—once violated, the system design has failed.</li>
<li><strong>Liveness</strong>: Availability the system guarantees under certain preconditions. If preconditions fail, restoring them returns the system to normal.</li>
</ul>
<p>The following discussions assume we’ve already solved some problems through transactions.</p>
<h2 id="byzantine-faults">Byzantine Faults</h2>
<p>Not worth considering.</p>
<ul>
<li>Too expensive.</li>
<li>Environmental issues like radiation can cause Byzantine faults, but the probability on Earth’s surface is extremely low. Of course, machines operating in space must consider this.</li>
<li>Software bugs can cause machine errors, but all machines run the same code. Bugs can’t be prevented unless all machines’ software is independently developed and only a few have bugs—which is clearly unrealistic.</li>
<li>Network intrusions could cause machine errors, but once an intruder can compromise one machine, there’s no reason to believe they can’t compromise all machines. Authentication, encryption, and firewalls are better approaches to network intrusion.</li>
</ul>
<h1 id="consensus">Consensus</h1>
<p>Consensus is one of the most important abstractions in distributed systems: all nodes agree on a proposal. Based on this, many distributed-system challenges can be addressed. The solutions below can achieve consensus. In that sense, consensus is a bit like Turing completeness: once you implement one strong consensus mechanism, you can derive many others from it.</p>
<h2 id="linearizability">Linearizability</h2>
<blockquote>
<p>Basic idea: Make a system appear as if there’s only one data copy, and all operations are atomic. As we’ll see, because linearizability has a simple definition, we can use whether other solutions can achieve linearizability to verify whether they’re consensus algorithms.</p>
</blockquote>
<p>Notes:</p>
<ul>
<li>Linearizability’s most intuitive requirement: once the system returns the latest value for a read, even if the related write hasn’t committed, all subsequent reads must return the latest value.</li>
<li>Atomic operations. This property can be expressed as CAS (compare-and-set), similar to preventing lost updates in single-machine transactions.</li>
<li>Building on the above, we <strong>don’t consider the effects of external network latency when observing the system</strong>—i.e., we don’t need to consider phantom reads. Once linearizability is achieved, we can naturally build distributed transactions to handle phantom reads. But when thinking about the problem model, be clear: transactions address data inconsistency between tables (a business-layer concern); distributed system challenges address data synchronization inconsistency between replicas (an infrastructure-layer concern).</li>
<li>Note the distinction between serializability and linearizability: the former concerns <strong>concurrent</strong> transaction results matching serial execution; the latter concerns data replicas appearing as a single copy—the strongest linearizability prevents the outside world from perceiving <strong>parallelism</strong>.</li>
<li>Actual serial execution and two-phase locking first achieve linearizability by restricting parallelism and concurrency, thereby achieving serializability. Serializable snapshot isolation, however, uses different snapshots for optimistic concurrency control; snapshot states and their changes can proceed in parallel, so linearizability is not guaranteed. Multiple SSI-based transactions may read different values, but conflicting ones cannot both commit. The first two approaches try to prevent this situation up front.</li>
</ul>
<p>Quorum can achieve linearizability, provided there’s no uncertain network latency.</p>
<blockquote>
<p>As long as there’s an unreliable network, there’s a risk of violating linearizability. This is CAP theory: the network is definitely unreliable; between availability and consistency, you can only choose one.</p>
</blockquote>
<h2 id="ordering-guarantees">Ordering Guarantees</h2>
<h3 id="causal-consistency">Causal Consistency</h3>
<p>Linearizability’s constraints are too strict. Can we achieve consensus with weaker requirements? We think of causal consistency: as long as causally related events occur in order and other events happen in parallel, the difficulty should be less than linearizability (which is equivalent to no parallelism).</p>
<blockquote>
<p>If a system obeys the order prescribed by causal relationships, we call it causally consistent. If neither of two operations happened before the other, they’re concurrent; otherwise they’re causal and can be ordered.</p>
</blockquote>
<p>How do we causally order operations? One approach is Lamport timestamps: assign each operation/client a sequence number composed of an incrementing counter plus a node ID. Sequence numbers are ordered by counter first, then node ID on ties. Every request carries a timestamp. Whenever a node or client observes a larger sequence number, it advances its own counter so the next request uses an even larger value. This allows ordering of operations.</p>
<p>As long as there’s no uncertain network latency, ordering is guaranteed. With network latency, implementing CAS with Lamport timestamps requires each node to first confirm no concurrent CAS requests (resolving ties by sequence number), so network latency directly stalls the system. (Lamport timestamps’ operating environment requires more assumptions that we won’t discuss.)</p>
<h3 id="total-order-broadcast">Total Order Broadcast</h3>
<p>Lamport timestamps and other causal-consistency approaches can fail here because they behave like synchronous coordination models (decision waits for information from all nodes). If we switch to an asynchronous model, CAS can be implemented.</p>
<p>Regardless of implementation environment, CAS can be achieved with these two conditions:</p>
<ul>
<li><strong>Reliable delivery</strong>: No message loss. If a message is sent to one node, it must be sent to all nodes.</li>
<li><strong>Strict ordering</strong>: Messages are always delivered to each node in the same order.</li>
</ul>
<p>With this, CAS only needs to be implemented on one node, and all other nodes must follow that CAS operation because operations are reliably delivered and strictly ordered.</p>
<p>So does implementing total order broadcast achieve consensus? We can verify by checking whether total order broadcast can achieve linearizability. It turns out that asynchronous models can’t handle the reading problem: <strong>system-internal network latency</strong> (not external—we said we don’t need to consider external network latency) may cause the outside world to read a new value followed by an old value.</p>
<p>Therefore:</p>
<ul>
<li>We say total order broadcast satisfies write linearizability (which is essentially serializability) but not read linearizability.</li>
<li>But is that really the case? Actually, if we treat reads (or reads requiring strict accuracy) as operations and add them to the operation queue, read linearizability is satisfied. ZooKeeper and etcd have similar implementations.</li>
</ul>
<p>The key takeaway from the two paragraphs above is:</p>
<blockquote>
<p>Fully asynchronous consensus is impossible to achieve. Synchronous consensus is possible but has performance issues.</p>
</blockquote>
<p>So how do we implement total order broadcast? (Answer: using linearizability…)</p>
<h2 id="implementing-total-order-broadcast">Implementing Total Order Broadcast</h2>
<p>Finally, we discuss achieving consensus through total order broadcast.</p>
<p>We first discuss a feasible approximate solution, then extend it to total order broadcast.</p>
<h3 id="two-phase-commit-2pc">Two-Phase Commit (2PC)</h3>
<p>Introduce a new component: the coordinator. The coordinator and nodes implement two-phase commit as follows:</p>
<ol>
<li>Send a prepare request to all nodes, asking if they can commit.</li>
<li>If all nodes return “yes,” the coordinator issues a commit request and nodes commit. If any node returns “no,” the coordinator tells all nodes to abort.</li>
</ol>
<p>This can clearly produce agreement in the happy path. The problem, as always, is network latency and failures. So we enhance the coordinator’s fault tolerance:</p>
<h3 id="fault-tolerant-consensus">Fault-Tolerant Consensus</h3>
<p>The book introduces several fault-tolerant consensus algorithms: VSR, Paxos, Raft, and Zab. I’m most familiar with Raft (MIT 6.824), so I’ll use Raft to explain how to extend 2PC for better fault tolerance.</p>
<p>The coordinator can also be the leader node. Whenever a problem occurs, if we can elect a new leader from available nodes to serve as coordinator, consensus remains valid. Electing a new node is itself a consensus problem—this consensus only needs to be acknowledged by more than half the nodes.</p>
<p>Whenever a node detects leader failure, it can declare itself the new leader. As long as it gains acknowledgment from more than half the nodes, it becomes the leader. This leader then acts as coordinator, ordering operations for follower nodes—any operation approved by more than half the nodes is immediately executed.</p>
<p>If more than half the nodes go down, the system enters a crash state. The benefit of requiring a majority: there’s always at least one node that participated in every vote, ensuring split-brain doesn’t occur and consensus is maintained.</p>
<h3 id="membership-and-coordination-services">Membership and Coordination Services</h3>
<p>Now we know how to achieve consensus. We also see that not all operations need consensus participation; only certain critical operations do. Therefore, we can use packaged systems like ZooKeeper (Zab) and etcd (Raft) to establish consensus for small, memory-resident datasets and build highly reliable coordination paths.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Introduction to Ring-Allreduce</title>
    <link>https://www.yujiachen.com/ring-allreduce/</link>
    <guid isPermaLink="true">https://www.yujiachen.com/ring-allreduce/</guid>
    <pubDate>Thu, 24 May 2018 00:00:00 GMT</pubDate>
    <category>Tech</category>
    <content:encoded><![CDATA[<p><em>Translated by Claude from the Chinese original.</em></p>
<p>Today I stumbled upon the ring-allreduce GPU communication algorithm out of curiosity. I originally wanted to read about it on <a href="http://research.baidu.com/bringing-hpc-techniques-deep-learning/">Baidu Research’s page</a>, but couldn’t find the relevant content on their site. After reading the comments in the <a href="https://github.com/baidu-research/baidu-allreduce">baidu-allreduce</a> code, I understood it. It’s actually a fairly simple algorithm to explain, so I’ll give a brief overview.</p>
<p>If you want implementation details, you can read the comments on GitHub directly—they’re very clear: <a href="https://github.com/baidu-research/baidu-allreduce/blob/master/collectives.cu#L156">https://github.com/baidu-research/baidu-allreduce/blob/master/collectives.cu#L156</a></p>
<ul>
<li><em>Unless otherwise noted, the images in this post are from <a href="https://www.zhihu.com/question/57799212/answer/292494636?utm_source=ZHShareTargetIDMore&amp;utm_medium=social&amp;utm_oi=37729630945280">a Zhihu answer</a>, because I could not find the original Baidu Research article containing these diagrams.</em></li>
</ul>
<p>A major drawback of typical multi-GPU training is that one GPU needs to collect gradients from all other GPUs each time, then distribute the updated model back to all other GPUs. As shown below:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_1.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_1.png" alt="" class="article-image" width="452" height="310" />
</picture>
</p>
<p>The biggest drawback of this model is that GPU 0’s communication time grows linearly with the number of GPUs. This is why ring-allreduce was developed, as shown below:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_2.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_2.png" alt="" class="article-image" width="419" height="330" />
</picture>

The basic idea of this algorithm is to eliminate the central reducer and let data flow through a ring formed by the GPUs. The entire ring-allreduce process consists of two major steps: the first step is reduce-scatter, and the second step is allgather.</p>
<p>First step (reduce-scatter): We have n GPUs. We divide the data on each GPU into n equal chunks and assign each GPU its left and right neighbors (in the diagram, GPU 0’s left neighbor is GPU 4 and right neighbor is GPU 1; GPU 1’s left neighbor is GPU 0 and right neighbor is GPU 2, and so on). Then we perform n-1 rounds. In round i, GPU j sends its chunk (j - i) mod n to GPU j+1 and receives chunk (j - i - 1) mod n from GPU j-1, then performs a reduce operation on the received data. (All indices use modulo-n wrap-around, e.g., -1 mod n = n - 1.) The diagram below illustrates this:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_3.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_3.png" alt="" class="article-image" width="480" height="335" />
</picture>

After n-1 rounds, the first step (reduce-scatter) of ring-allreduce is complete. At this point, each GPU holds one fully reduced chunk. The algorithm then enters allgather, which also takes n-1 rounds.</p>
<p>The second step, allgather, is straightforward: through n-1 rounds, each GPU forwards its reduced chunk to other GPUs. In round i, GPU j sends its (j - i - 1) mod n chunk to its right neighbor and receives the (j - i - 2) mod n chunk from its left neighbor. Unlike the first step, the received data does not need a reduce operation; it is copied directly into place.</p>
<p>Finally, the data on each GPU looks like this:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_4.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_4.png" alt="" class="article-image" width="572" height="321" />
</picture>

If it’s still unclear, let’s walk through a 3-GPU example:</p>
<p>First, the reduce-scatter step:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_5.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_5.png" alt="" class="article-image" width="772" height="913" />
</picture>
</p>
<p>Then the allgather step:</p>
<p>
<picture>
  <source srcset="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_6.webp" type="image/webp" />
  <img src="https://www.yujiachen.com/assets/posts/ring-allreduce/en/image_6.png" alt="" class="article-image" width="1080" height="1321" />
</picture>
</p>
<p>References:</p>
<p><a href="https://github.com/baidu-research/baidu-allreduce">https://github.com/baidu-research/baidu-allreduce</a></p>
<p><a href="https://www.zhihu.com/question/57799212/answer/292494636?utm_source=ZHShareTargetIDMore&amp;utm_medium=social&amp;utm_oi=37729630945280">https://www.zhihu.com/question/57799212/answer/292494636?utm_source=ZHShareTargetIDMore&amp;utm_medium=social&amp;utm_oi=37729630945280</a></p>
]]></content:encoded>
  </item>
  </channel>
</rss>
