<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ValueNotifier on Tarragon</title><link>https://tarrragon.github.io/blog/tags/valuenotifier/</link><description>Recent content in ValueNotifier on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/valuenotifier/index.xml" rel="self" type="application/rss+xml"/><item><title>為什麼這個場景適合用高階函式？以 Flutter 設定更新為例，比較 typedef 改寫前後</title><link>https://tarrragon.github.io/blog/work-log/dart_hof_typedef_readability/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/dart_hof_typedef_readability/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心議題&lt;/strong>：高階函式是特定場景的自然解 — 當「流程固定、變化點單一且開放」時，把變化點抽成函式參數最省。要不要用它，由場景特徵決定。本文先論證這個場景為何適合 HOF，再比較同一 pattern 的兩種表達（裸函式型別 vs &lt;code>typedef&lt;/code>）各自的優缺點。
&lt;strong>案例骨幹&lt;/strong>：&lt;code>SettingsController.update(transform)&lt;/code> — 9 個設定欄位共用同一條「取值→算新值→去重→通知」流程，唯一的變化是「改哪個欄位」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-案例一個收函式的設定更新方法">1. 案例：一個收函式的設定更新方法&lt;/h2>
&lt;p>設定有 9 個欄位（字型、顏色、描邊、時間格式、目標螢幕、開機啟動…）。每個欄位變更都要走同一串流程：取當前設定 → 算出新設定 → 比對是否確實改變 → 賦回並通知 UI 重繪。把這串流程封裝成一個方法：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">SettingsController&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">ValueNotifier&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SettingsModel&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">next&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>呼叫端只描述「改哪個欄位」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">controller&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">copyWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">fillColor:&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">controller&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">copyWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">fontSize:&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="p">));&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>update&lt;/code> 收的這個參數本身是「一個函式」 — 把函式當成可傳遞的值。這就是 higher-order function。&lt;/p>
&lt;h3 id="簽章的型別與名字拆解">簽章的型別與名字拆解&lt;/h3>
&lt;p>這個簽章的關鍵是分清「哪裡是型別、哪裡是名字」。它是一個普通的參數宣告，順序跟常見的 &lt;code>int count&lt;/code>、&lt;code>Color color&lt;/code> 一樣是 &lt;strong>&lt;code>型別 名字&lt;/code>&lt;/strong>，只是這次型別換成了較長的函式型別：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="o">//&lt;/span> &lt;span class="err">└────────────&lt;/span> &lt;span class="err">型別（函式型別）────────────┘&lt;/span> &lt;span class="err">└名字┘&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>mutate&lt;/code> 是&lt;strong>這個參數的名字&lt;/strong> — 方法內部靠它指涉傳進來的那個函式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// ← 用名字 mutate「呼叫」傳進來的函式
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>容易混淆的是型別裡面那個 &lt;code>current&lt;/code>：它和 &lt;code>mutate&lt;/code> 不同層級 — &lt;code>current&lt;/code> 只是函式型別內標記參數的名字，&lt;strong>純文件性&lt;/strong>，寫成 &lt;code>SettingsModel Function(SettingsModel)&lt;/code> 行為完全一樣，只是讓型別讀起來更清楚。換句話說，前半的函式型別規定「這個名字必須是什麼形狀的函式」，最後的 &lt;code>mutate&lt;/code> 則是「這個函式參數叫什麼」。下一節先補 HOF 的基礎，第 4 節再回頭談「前半那串型別裸寫在簽章」造成的閱讀摩擦。&lt;/p>
&lt;hr>
&lt;h2 id="2-higher-order-function-是什麼最小定義">2. Higher-order function 是什麼（最小定義）&lt;/h2>
&lt;p>&lt;strong>把函式當資料處理的函式&lt;/strong> — 接收函式當參數，或回傳函式，符合其一即是。前提是語言把函式視為一等公民（first-class），能像變數一樣傳遞。Dart、JS、Kotlin、Swift 皆成立。&lt;/p>
&lt;p>常見的 &lt;code>list.map((x) =&amp;gt; x*2)&lt;/code>、&lt;code>list.where((x) =&amp;gt; x&amp;gt;0)&lt;/code>、&lt;code>onPressed: () =&amp;gt; ...&lt;/code> 都屬此類。&lt;code>update((s) =&amp;gt; ...)&lt;/code> 是同一家族。&lt;/p>
&lt;hr>
&lt;h2 id="3-為什麼這個場景適合用-hof">3. 為什麼這個場景適合用 HOF&lt;/h2>
&lt;p>這個場景有三個特徵，剛好對上 HOF 的強項 — HOF 適不適用，由這些特徵決定。&lt;/p>
&lt;h3 id="31-流程固定變化點單一">3.1 流程固定、變化點單一&lt;/h3>
&lt;p>9 個欄位的更新，&lt;strong>流程 100% 相同&lt;/strong>（取值、去重、賦回、通知），&lt;strong>唯一差異&lt;/strong>是中間那一步「&lt;code>copyWith(哪個欄位: 值)&lt;/code>」。&lt;/p>
&lt;p>當「共用流程」與「變化點」能這樣切乾淨時，HOF 正好對上這個結構：把固定流程寫死在 &lt;code>update&lt;/code> 裡，把變化點抽成函式參數 &lt;code>transform&lt;/code> 由呼叫端帶入。&lt;code>map&lt;/code> 對「走訪迴圈（固定）+ 元素變換（變化）」做的是同一件事。&lt;/p>
&lt;h3 id="32-模型不可變本來就是current--next">3.2 模型不可變，本來就是「current → next」&lt;/h3>
&lt;p>&lt;code>SettingsModel&lt;/code> 是不可變物件（&lt;code>@immutable&lt;/code> + 全 &lt;code>final&lt;/code>）：要改 &lt;code>fillColor&lt;/code>，得用 &lt;code>copyWith&lt;/code> 產生新副本、再把整個物件替換回去。&lt;/p>
&lt;p>也就是說，不可變模型下的更新，在語意上&lt;strong>就是一個 &lt;code>(current) =&amp;gt; next&lt;/code> 的函式&lt;/strong> — 拿舊值算出新值。用函式參數表達這件事，是最貼合的形狀。&lt;/p>
&lt;h3 id="33-變化點開放難以列舉">3.3 變化點開放、難以列舉&lt;/h3>
&lt;p>「未來會改哪些欄位、怎麼組合」是開放的（可能同時改兩個欄位、可能有條件邏輯）。函式參數能表達任意轉換；若改用「enum 指定欄位 + switch」則被固定的列舉鎖死，每加一種改法都要動 &lt;code>update&lt;/code> 內部。HOF 把「怎麼改」的決定權留在呼叫端，&lt;code>update&lt;/code> 不需要知道。&lt;/p>
&lt;p>反過來說，當「變化集合是封閉的、而且需要被序列化或跨層比對」時，enum + switch 反而較好 — 例如要把「使用者改了哪個欄位」存進 undo 堆疊、或透過網路傳給後端，列舉值是可序列化的資料，閉包不是。本案例的變化點純粹發生在呼叫端、不需要 persist，HOF 才站得住。所以「開放」算不算優點，要跟「變化是否需要被當資料搬運」一起看。&lt;/p>
&lt;blockquote>
&lt;p>判準：&lt;strong>流程固定 + 變化點單一 + 變化開放&lt;/strong> 三者同時成立時，HOF 幾乎總是比「列舉 + 分支」或「複製多個方法」更省。&lt;/p>&lt;/blockquote>
&lt;p>對照反例放進具體場景更清楚。假設一個只有「深色模式開關」單一布林設定的 controller，更新邏輯就是 &lt;code>value = !value&lt;/code>，既沒有共用流程、也沒有開放的變化點 — 這時把它包成收函式的 &lt;code>update&lt;/code>，只是逼讀者解析一串函式型別去做一件 &lt;code>toggleDarkMode()&lt;/code> 就講完的事，抽象成本大於收益。另一種反向情境是：9 個欄位看似共用流程，實際每個的更新路徑各不相同（有的要打 API、有的要寫檔、有的純記憶體），那麼「固定流程」的前提根本不成立，硬抽進 &lt;code>update&lt;/code> 反而把三條不同的路徑塞進同一個殼裡。三條件少一條，具名方法通常更省 — 場景不對時硬用，才是過度設計。&lt;/p>
&lt;hr>
&lt;h2 id="4-原始寫法的優缺點裸函式型別">4. 原始寫法的優缺點（裸函式型別）&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">next&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="什麼是函式型別裸寫在簽章">什麼是「函式型別裸寫在簽章」&lt;/h3>
&lt;p>這是整個討論的起點，值得單獨講清楚。把術語拆三個詞：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>函式型別&lt;/strong>：描述「一個函式長什麼樣」的型別，例如 &lt;code>SettingsModel Function(SettingsModel current)&lt;/code> — 收一個 &lt;code>SettingsModel&lt;/code>、回傳一個 &lt;code>SettingsModel&lt;/code>。&lt;/li>
&lt;li>&lt;strong>裸寫&lt;/strong>：把完整型別&lt;strong>整串攤開寫出來&lt;/strong>，沒有先取名包裝（對比「裸數字 / magic number」直接寫 &lt;code>120&lt;/code> 而非具名常數）。&lt;/li>
&lt;li>&lt;strong>在簽章&lt;/strong>：寫在方法的參數列（signature）裡。&lt;/li>
&lt;/ul>
&lt;p>合起來就是：&lt;strong>把那串 &lt;code>SettingsModel Function(SettingsModel current)&lt;/code> 原封不動塞進參數位，而不是先用 &lt;code>typedef&lt;/code> 取個名字再引用。&lt;/strong>&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心議題</strong>：高階函式是特定場景的自然解 — 當「流程固定、變化點單一且開放」時，把變化點抽成函式參數最省。要不要用它，由場景特徵決定。本文先論證這個場景為何適合 HOF，再比較同一 pattern 的兩種表達（裸函式型別 vs <code>typedef</code>）各自的優缺點。
<strong>案例骨幹</strong>：<code>SettingsController.update(transform)</code> — 9 個設定欄位共用同一條「取值→算新值→去重→通知」流程，唯一的變化是「改哪個欄位」。</p></blockquote>
<hr>
<h2 id="1-案例一個收函式的設定更新方法">1. 案例：一個收函式的設定更新方法</h2>
<p>設定有 9 個欄位（字型、顏色、描邊、時間格式、目標螢幕、開機啟動…）。每個欄位變更都要走同一串流程：取當前設定 → 算出新設定 → 比對是否確實改變 → 賦回並通知 UI 重繪。把這串流程封裝成一個方法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">SettingsController</span> <span class="kd">extends</span> <span class="n">ValueNotifier</span><span class="o">&lt;</span><span class="n">SettingsModel</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫端只描述「改哪個欄位」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">controller</span><span class="p">.</span><span class="n">update</span><span class="p">((</span><span class="n">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">fillColor:</span> <span class="n">c</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">controller</span><span class="p">.</span><span class="n">update</span><span class="p">((</span><span class="n">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">fontSize:</span> <span class="n">v</span><span class="p">));</span></span></span></code></pre></div><p><code>update</code> 收的這個參數本身是「一個函式」 — 把函式當成可傳遞的值。這就是 higher-order function。</p>
<h3 id="簽章的型別與名字拆解">簽章的型別與名字拆解</h3>
<p>這個簽章的關鍵是分清「哪裡是型別、哪裡是名字」。它是一個普通的參數宣告，順序跟常見的 <code>int count</code>、<code>Color color</code> 一樣是 <strong><code>型別 名字</code></strong>，只是這次型別換成了較長的函式型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span>  <span class="n">mutate</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">//</span>          <span class="err">└────────────</span> <span class="err">型別（函式型別）────────────┘</span>  <span class="err">└名字┘</span></span></span></code></pre></div><p><code>mutate</code> 是<strong>這個參數的名字</strong> — 方法內部靠它指涉傳進來的那個函式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>  <span class="c1">// ← 用名字 mutate「呼叫」傳進來的函式
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>容易混淆的是型別裡面那個 <code>current</code>：它和 <code>mutate</code> 不同層級 — <code>current</code> 只是函式型別內標記參數的名字，<strong>純文件性</strong>，寫成 <code>SettingsModel Function(SettingsModel)</code> 行為完全一樣，只是讓型別讀起來更清楚。換句話說，前半的函式型別規定「這個名字必須是什麼形狀的函式」，最後的 <code>mutate</code> 則是「這個函式參數叫什麼」。下一節先補 HOF 的基礎，第 4 節再回頭談「前半那串型別裸寫在簽章」造成的閱讀摩擦。</p>
<hr>
<h2 id="2-higher-order-function-是什麼最小定義">2. Higher-order function 是什麼（最小定義）</h2>
<p><strong>把函式當資料處理的函式</strong> — 接收函式當參數，或回傳函式，符合其一即是。前提是語言把函式視為一等公民（first-class），能像變數一樣傳遞。Dart、JS、Kotlin、Swift 皆成立。</p>
<p>常見的 <code>list.map((x) =&gt; x*2)</code>、<code>list.where((x) =&gt; x&gt;0)</code>、<code>onPressed: () =&gt; ...</code> 都屬此類。<code>update((s) =&gt; ...)</code> 是同一家族。</p>
<hr>
<h2 id="3-為什麼這個場景適合用-hof">3. 為什麼這個場景適合用 HOF</h2>
<p>這個場景有三個特徵，剛好對上 HOF 的強項 — HOF 適不適用，由這些特徵決定。</p>
<h3 id="31-流程固定變化點單一">3.1 流程固定、變化點單一</h3>
<p>9 個欄位的更新，<strong>流程 100% 相同</strong>（取值、去重、賦回、通知），<strong>唯一差異</strong>是中間那一步「<code>copyWith(哪個欄位: 值)</code>」。</p>
<p>當「共用流程」與「變化點」能這樣切乾淨時，HOF 正好對上這個結構：把固定流程寫死在 <code>update</code> 裡，把變化點抽成函式參數 <code>transform</code> 由呼叫端帶入。<code>map</code> 對「走訪迴圈（固定）+ 元素變換（變化）」做的是同一件事。</p>
<h3 id="32-模型不可變本來就是current--next">3.2 模型不可變，本來就是「current → next」</h3>
<p><code>SettingsModel</code> 是不可變物件（<code>@immutable</code> + 全 <code>final</code>）：要改 <code>fillColor</code>，得用 <code>copyWith</code> 產生新副本、再把整個物件替換回去。</p>
<p>也就是說，不可變模型下的更新，在語意上<strong>就是一個 <code>(current) =&gt; next</code> 的函式</strong> — 拿舊值算出新值。用函式參數表達這件事，是最貼合的形狀。</p>
<h3 id="33-變化點開放難以列舉">3.3 變化點開放、難以列舉</h3>
<p>「未來會改哪些欄位、怎麼組合」是開放的（可能同時改兩個欄位、可能有條件邏輯）。函式參數能表達任意轉換；若改用「enum 指定欄位 + switch」則被固定的列舉鎖死，每加一種改法都要動 <code>update</code> 內部。HOF 把「怎麼改」的決定權留在呼叫端，<code>update</code> 不需要知道。</p>
<p>反過來說，當「變化集合是封閉的、而且需要被序列化或跨層比對」時，enum + switch 反而較好 — 例如要把「使用者改了哪個欄位」存進 undo 堆疊、或透過網路傳給後端，列舉值是可序列化的資料，閉包不是。本案例的變化點純粹發生在呼叫端、不需要 persist，HOF 才站得住。所以「開放」算不算優點，要跟「變化是否需要被當資料搬運」一起看。</p>
<blockquote>
<p>判準：<strong>流程固定 + 變化點單一 + 變化開放</strong> 三者同時成立時，HOF 幾乎總是比「列舉 + 分支」或「複製多個方法」更省。</p></blockquote>
<p>對照反例放進具體場景更清楚。假設一個只有「深色模式開關」單一布林設定的 controller，更新邏輯就是 <code>value = !value</code>，既沒有共用流程、也沒有開放的變化點 — 這時把它包成收函式的 <code>update</code>，只是逼讀者解析一串函式型別去做一件 <code>toggleDarkMode()</code> 就講完的事，抽象成本大於收益。另一種反向情境是：9 個欄位看似共用流程，實際每個的更新路徑各不相同（有的要打 API、有的要寫檔、有的純記憶體），那麼「固定流程」的前提根本不成立，硬抽進 <code>update</code> 反而把三條不同的路徑塞進同一個殼裡。三條件少一條，具名方法通常更省 — 場景不對時硬用，才是過度設計。</p>
<hr>
<h2 id="4-原始寫法的優缺點裸函式型別">4. 原始寫法的優缺點（裸函式型別）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="什麼是函式型別裸寫在簽章">什麼是「函式型別裸寫在簽章」</h3>
<p>這是整個討論的起點，值得單獨講清楚。把術語拆三個詞：</p>
<ul>
<li><strong>函式型別</strong>：描述「一個函式長什麼樣」的型別，例如 <code>SettingsModel Function(SettingsModel current)</code> — 收一個 <code>SettingsModel</code>、回傳一個 <code>SettingsModel</code>。</li>
<li><strong>裸寫</strong>：把完整型別<strong>整串攤開寫出來</strong>，沒有先取名包裝（對比「裸數字 / magic number」直接寫 <code>120</code> 而非具名常數）。</li>
<li><strong>在簽章</strong>：寫在方法的參數列（signature）裡。</li>
</ul>
<p>合起來就是：<strong>把那串 <code>SettingsModel Function(SettingsModel current)</code> 原封不動塞進參數位，而不是先用 <code>typedef</code> 取個名字再引用。</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 裸寫：函式型別整串長在簽章裡
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="o">//</span>          <span class="err">└──────────</span> <span class="err">這一整串就是「裸寫的函式型別」──────────┘</span></span></span></code></pre></div><p>為什麼偏偏是「函式型別」會因為裸寫而卡住，一般型別卻不會？因為 <code>int</code>、<code>Color</code> 這類型別已經是短名稱，裸寫毫無負擔；而函式型別的完整語法 <code>X Function(Y)</code> 較長、巢狀時更難讀，<strong>讀者得當場在腦中解析「這是收什麼、回什麼的函式」</strong>。讀程式碼第一眼卡住的，正是這串裸寫的函式型別 — 它才是這篇要討論「要不要抽 typedef」的真正觸發點。下面的優缺點，都圍繞「裸寫 vs 取名」這個軸展開。</p>
<h3 id="優點">優點</h3>
<ul>
<li><strong>型別就地可見</strong>：函式的形狀（收什麼、回什麼）直接寫在簽章上，讀者不必跳到別處查定義。</li>
<li><strong>零額外宣告</strong>：不需要為了一個參數多定義一個型別別名。</li>
</ul>
<h3 id="缺點">缺點</h3>
<ul>
<li><strong>簽章冗長、語法門檻</strong>：<code>SettingsModel Function(SettingsModel current)</code> 對不熟函式型別語法的人構成解析負擔，一眼難消化。</li>
<li><strong>命名與語境矛盾</strong>：參數叫 <code>mutate</code>（變異／就地修改），但模型不可變、實際是「產生新副本」，名稱會誤導。</li>
<li><strong>缺使用錨點</strong>：簽章沒有範例，第一次用的人不知道該傳什麼形狀的 lambda。</li>
</ul>
<hr>
<h2 id="5-typedef-改寫後的優缺點">5. typedef 改寫後的優缺點</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 設定轉換規則：收當前設定、回傳改好的新設定（通常以 copyWith 實作）。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">typedef</span> <span class="n">SettingsMutator</span> <span class="o">=</span> <span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// 套用一條「目前設定 → 新設定」的轉換規則 …
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">/// 範例：`controller.update((s) =&gt; s.copyWith(fillColor: c));`
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsMutator</span> <span class="n">transform</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">transform</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="優點-1">優點</h3>
<ul>
<li><strong>簽章簡潔、概念命名</strong>：<code>SettingsMutator</code> 把函式型別升格成領域詞彙，認知從「解析 <code>X Function(Y)</code>」降到「讀一個名詞」。</li>
<li><strong>命名精準</strong>：<code>transform</code>（轉換）貼合不可變語境，不再暗示就地修改。</li>
<li><strong>有錨點</strong>：doc comment 的範例讓第一次使用者立即知道怎麼傳。</li>
<li><strong>錯誤訊息更易讀</strong>：型別對不上時，編譯器印的是 <code>SettingsMutator</code> 這個名字，而不是整串 <code>SettingsModel Function(SettingsModel)</code>；裸寫版的錯誤訊息會把完整型別攤開，較難一眼定位。</li>
<li><strong>可重用</strong>：同一個 <code>SettingsMutator</code> 型別若日後被多個 API 共用，定義集中一處。</li>
</ul>
<h3 id="缺點-1">缺點</h3>
<ul>
<li><strong>多一層 indirection</strong>：想知道 <code>transform</code> 的確切型別，得跳到 <code>typedef</code> 定義；只看 <code>update</code> 簽章看不到形狀。</li>
<li><strong>多一個命名負擔</strong>：<code>SettingsMutator</code> 本身要取得好；命名不當反而多一層要理解的東西。</li>
<li><strong>對單一用途略顯重</strong>：若這個函式型別只在一處使用，typedef 的「集中重用」優點用不上，只剩「命名」一項收益。</li>
</ul>
<hr>
<h2 id="6-並排比較">6. 並排比較</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>原始（裸函式型別）</th>
          <th>typedef 改寫後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>簽章可讀性</td>
          <td>冗長、需解析語法</td>
          <td>簡潔、讀一個名詞</td>
      </tr>
      <tr>
          <td>型別形狀可見性</td>
          <td>就地可見（優）</td>
          <td>需跳到 typedef 定義（劣）</td>
      </tr>
      <tr>
          <td>命名語意</td>
          <td><code>mutate</code> 與不可變矛盾</td>
          <td><code>transform</code> 貼合</td>
      </tr>
      <tr>
          <td>使用門檻</td>
          <td>無範例</td>
          <td>有範例錨點</td>
      </tr>
      <tr>
          <td>額外宣告成本</td>
          <td>無</td>
          <td>多一個 typedef 要命名/維護</td>
      </tr>
      <tr>
          <td>多處共用時</td>
          <td>各自裸寫、重複</td>
          <td>集中定義、重用</td>
      </tr>
      <tr>
          <td>pattern / 行為</td>
          <td>HOF</td>
          <td>HOF（不變）</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>兩者是同一個 pattern（HOF + ValueNotifier）的兩種表達</strong>。取捨重點在「型別就地可見」對上「簽章簡潔 + 概念命名」—— 當函式型別會被多處使用、或語法門檻造成實際閱讀摩擦時，typedef 划算；若只用一次且團隊熟悉函式型別語法，裸寫也完全合理。</p>
<p>改寫的驗證也印證它停在「表達層」：呼叫端傳 lambda 不依賴參數名 → 零修改；行為不變；全套測試原封不動通過。</p>
<hr>
<h2 id="7-收斂">7. 收斂</h2>
<ul>
<li>HOF 適合的場景特徵：<strong>流程固定 + 變化點單一 + 變化開放</strong>。三者齊備時，把變化點抽成函式參數最省；場景不符（欄位少、流程各異）則具名方法更直白。</li>
<li>不可變模型的更新本質就是 <code>(current) =&gt; next</code>，用函式參數表達是語意上最貼合的形狀。</li>
<li>兩種寫法的取捨：裸函式型別型別就地可見、零宣告；typedef 簽章簡潔、命名成概念、可重用，但多一層 indirection。</li>
<li>選擇依據：函式型別是否多處共用、語法是否造成實際閱讀摩擦。摩擦明顯就抽 typedef，否則裸寫無妨。</li>
</ul>
<blockquote>
<p><strong>延伸</strong>：本文「模型不可變」段是整個 HOF 適配的前提之一。<code>SettingsModel</code> 那種 <code>@immutable</code> + <code>copyWith</code> 結構怎麼產生、以及更好懂的替代路徑，見 <a href="/blog/work-log/freezed-%E7%9A%84%E4%B8%89%E5%B1%A4%E7%B5%90%E6%A7%8B%E8%A7%A3%E5%89%96with_%E4%BB%A5%E5%8F%8A%E6%9B%B4%E5%A5%BD%E6%87%82%E7%9A%84%E6%9B%BF%E4%BB%A3%E8%B7%AF%E5%BE%91/" data-link-title="Freezed 的三層結構解剖：with、_$、以及更好懂的替代路徑" data-link-desc="freezed `class X with _$X implements Y` 的分層結構解剖：`with` 與 `_$` 各自的角色、沒有 freezed 怎麼手做、中間投影物件 vs DTO 直接 implements 的維護取捨。">Freezed 的三層結構解剖</a>。</p></blockquote>
]]></content:encoded></item></channel></rss>