硅

硅上观道・我的个人技术博客

硅上观道的个人博客

函数式心法 (3):巧用柯里化和部分应用

<p>上篇文章我们谈了高阶函数在函数式语言中的妙用,文末留了一个引子,点出了「柯里化」和「部分应用」两个概念。如果你还没有看过,我非常建议你先看一下 <a class="link" href="https://www.changsun.work/post/250821-higher-order-functions/" target="_blank" rel="noopener" >上一篇文章</a>。</p> <p>这篇文章,我将深入解释这两个概念的细节,并说明两个概念在函数式语言中的价值。</p> <hr> <h2 id="1-什么是柯里化">1. 什么是柯里化 </h2><p>我们上篇文章最后给出了一个相当简略的 Haskell 解法,就是 <code>map (*2) [1, 2, 3, 4, 5]</code>。这个解法看起来很直观,可是对于不熟悉函数式写法的朋友,细想一下可能还是会觉得这里的 <code>*2</code> 写法颇有些怪异:从类型的角度,我们知道这里应该是一个函数;可是这个「乘二」为什么可以是一个函数呢?</p> <p>想了解为何这种写法成立,必须先解释什么是<strong>柯里化</strong>。简单来讲,柯里化就是把一个多参数函数,转化为了多个串联起来的<strong>单一参数函数</strong>。</p> <p>我们写一个最简单的加法函数举个例子:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">add</span> <span class="ow">::</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> </span></span><span class="line"><span class="cl"><span class="nf">add</span> <span class="n">x</span> <span class="n">y</span> <span class="ow">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> </span></span></code></pre></div><p>在传统非柯里化形式的函数定义中,其类型应包括一个含有两个整数的元组,返回一个整数。也就是<code>(Int, Int) -&gt; Int</code>。如果用非柯里化形式写这个函数,应当写为:<code>add' (x, y) = x + y</code>。这是在其他语言中常见的多参数函数写法。</p> <p>而在我们的例子中,这个函数实际的函数签名是:<code>Int -&gt; Int -&gt; Int</code>。这其实是经过柯里化处理后的标准函数类型。由于 Haskell 的函数签名是<strong>右结合</strong>的,柯里化后的函数签名,完整写法是是<code>Int -&gt; (Int -&gt; Int)</code>。</p> <p>也就是说,原先是两个参数一起输入,柯里化后转变为先输入一个参数,返回一个单一参数的函数;第二个参数输入给这个单一参数函数,最终得到计算结果。</p> <p>在 Haskell 中,柯里化后的类型形式尽管发生了变化,但是<strong>行为和原先是一致的</strong>。换言之,现在的<code>add x y = x + y</code>和非柯里化形式<code>add' (x, y) = x + y</code>行为是一致的,都是计算两数之和。</p> <p>那你可能要问了,既然柯里化后和非柯里化的函数行为完全一致,为什么我们需要这样一个东西呢?</p> <p>这就要提到我们的下一个概念:柯里化<strong>解锁的全新函数使用方式</strong>──「部分应用」。</p> <hr> <h2 id="2-柯里化带来的部分应用">2. 柯里化带来的部分应用 </h2><p>仔细阅读上文对柯里化的说明,我们会发现一个关键点:<strong>没有人规定函数必须一次提供所有参数</strong>。</p> <p>也就是说,我们完全可以只提供一部分参数,使用返回的中间函数。而这,就是<strong>部分应用</strong>的概念。</p> <p>仍然拿上文最基本的<code>add</code>函数举例:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">add</span> <span class="ow">::</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> </span></span><span class="line"><span class="cl"><span class="nf">add</span> <span class="n">x</span> <span class="n">y</span> <span class="ow">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">add3</span> <span class="ow">::</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> </span></span><span class="line"><span class="cl"><span class="nf">add3</span> <span class="ow">=</span> <span class="n">add</span> <span class="mi">3</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">add3</span> <span class="mi">10</span> <span class="c1">-- 13</span> </span></span><span class="line"><span class="cl"><span class="nf">add3</span> <span class="mi">20</span> <span class="c1">-- 23</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">add10</span> <span class="ow">::</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> </span></span><span class="line"><span class="cl"><span class="nf">add10</span> <span class="ow">=</span> <span class="n">add</span> <span class="mi">10</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">add10</span> <span class="mi">10</span> <span class="c1">-- 20</span> </span></span><span class="line"><span class="cl"><span class="nf">add10</span> <span class="mi">20</span> <span class="c1">-- 30</span> </span></span></code></pre></div><p>在这个例子中,<code>add3</code>和<code>add10</code>两个函数就是<code>add</code>函数部分应用的结果。我们可以发现,对于只传入部分参数的情况,<code>add</code>函数返回值的类型的确是一个函数,符合我们的预期。</p> <p>对于 Haskell 中的多参数函数,我们完全可以一次性<strong>只提供部分参数</strong>,得到的新函数可以<strong>等待其他参数</strong>需要传入的时候再做最后计算。</p> <p>像这样通过部分应用得到的新函数,我们想要多少就可以定义多少。</p> <hr> <h2 id="3-柯里化和部分应用的价值">3. 柯里化和部分应用的价值 </h2><p>这两个概念的核心价值,在于函数式语言对于代码逻辑的一种特殊的复用方式。我们上面举的例子比较简单,但对于更复杂的函数,使用部分应用的形式可以大大简化代码重复的现象。</p> <p>在 Haskell 中,所有多参数函数都可以被理解为一种高阶函数,因为它们都可以通过部分应用的范式返回另一个函数。</p> <p>用一个形象的比喻,就是多参数函数可以被理解成一个「函数工厂」,我们通过传入部分参数生成各式各样的新函数,这些函数被应用在了需要的地方。<strong>这极大增加的代码的抽象程度和使用的灵活性</strong>。</p> <p>回到我们开头的例子,我想现在大家就理解<code>map (*2) [1, 2, 3, 4, 5]</code>为什么是一种合法且推荐的写法了:</p> <p><code>*</code>是一个双参数函数,这里我们只传入了一个参数<code>2</code>,得到了一个新的单一参数函数;而<code>map</code>正好需要第一个传入的参数是单一参数函数,我们通过函数的组合,最终得到了正确的结果。</p> <p>这里,我们看到了柯里化和部分应用的另一个实用价值:通过灵活控制参数传入的时机,我们可以<strong>更自由地组合各个函数</strong>,用灵活的方式完成需求。</p> <hr> <p>下一篇文章,我打算谈一谈函数式流程控制的问题,说明代数数据类型(ADT)和模式匹配这一对完美的组合。</p> <p><strong>如果你对更多函数式语言特性感兴趣,欢迎关注我!</strong></p>

2025/8/22
阅读更多

函数式心法 (2):理解运用高阶函数

<p>上篇文章我们从全局宏观的角度,了解了 Haskell 语言的特性和学习方法,这篇文章我们来谈一个具体的函数式语言特性:高阶函数。</p> <p>我选择这个问题,一个重要的原因是许多其他范式的语言纷纷在新版本、新标准中引入了高阶函数特性。在<strong>更纯粹的函数式语境下理解这个概念设计的必要性</strong>,我觉得对于我们更好使用高阶函数是有帮助的。</p> <p>换言之,哪怕<strong>你以后再也不碰 Haskell,了解一下高阶函数绝对不亏</strong>。</p> <hr> <h2 id="1-为什么需要高阶函数">1. 为什么需要高阶函数? </h2><p>我们先来看一个简单的例子:假设我们有一个由整数构成的列表,现在想要得到一个新列表,新列表的每个元素是原列表的两倍。</p> <p>如果使用命令式的思路来解决这个问题,常见的做法是使用 <code>for</code> 循环。我们来看一个 <code>Python</code> 的示例代码:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">double_list</span><span class="p">(</span><span class="n">num_list</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span> </span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[]</span> </span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">num_list</span><span class="p">:</span> </span></span><span class="line"><span class="cl">        <span class="n">result</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">i</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">double_list</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]))</span> </span></span><span class="line"><span class="cl"><span class="c1"># [2, 4, 6, 8, 10]</span> </span></span></code></pre></div><p>在这个简单的需求下,命令式的写法其实也是很直观的。只需要在 <code>for</code> 循环中,每步向 <code>result</code> 列表添加计算好的新元素,就可以得到正确结果。</p> <p>如果我们希望在函数式语言下达到类似的效果,又应该怎么办呢?</p> <p>由于在纯函数式语言 (如 Haskell) 中,我们要避免使用可变状态,无法在函数式语言使用类似 <code>result</code> 这种可变的变量。换言之,函数式语言中我们不能用传统的、基于可变状态的 <code>for</code> 循环来解决这个问题。</p> <p>要想实现类似效果,一个自然的想法就是要把函数式语言中的“一等公民”,也就是函数,<strong>将其作用发挥到极致</strong>。</p> <p>我们可不可以通过函数的组合,来代替 <code>for</code> 循环呢?这时,我们就需要引入这篇文章的主角:高阶函数。</p> <hr> <h2 id="2-高阶函数-map">2. 高阶函数 <code>map</code> </h2><p>什么是<strong>高阶函数 (Higher-Order Function)</strong> 呢?我们一般认为,可以将其他函数作为一个函数的参数传入进去,或者返回值是一个函数的函数可以被称之为高阶函数。</p> <p>在上述这个案例中,Haskell 语言的常见做法是使用 <code>map</code> 这个高阶函数。</p> <p>比如,我们可以通过定义一个 <code>doubleNum</code> 函数,将这个函数传入 <code>map</code> 的方式来解决这个问题:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">doubleNum</span> <span class="ow">::</span> <span class="kt">Int</span> <span class="ow">-&gt;</span> <span class="kt">Int</span> </span></span><span class="line"><span class="cl"><span class="nf">doubleNum</span> <span class="n">x</span> <span class="ow">=</span> <span class="n">x</span> <span class="o">*</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">::</span> <span class="p">[</span><span class="kt">Int</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">=</span> <span class="n">map</span> <span class="n">doubleNum</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="c1">-- [2, 4, 6, 8, 10]</span> </span></span></code></pre></div><p>这里 <code>map</code> 的原理是什么呢?首先,我们定义了一个可以把任意整数翻倍的函数;然后,我们通过 <code>map</code> 高阶函数,把这个 <code>doubleNum</code> 函数应用到了列表中的每一个值;最终得到了预期的计算结果。</p> <p>在这里,<code>map</code> 函数接受一个函数和一个列表,返回一个新列表。新列表中的每个元素,都是原列表对应元素经过传入函数计算后的结果。</p> <p><img alt="Map 高阶函数示意图" class="gallery-image" data-flex-basis="339px" data-flex-grow="141" height="901" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/FuFxWyUIbCNX58_qwmYwKKyb55n2ZSgNo1Yf0cZzGBs/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA4L2hpZ2hlci1vcmRlci1mdW5jdGlvbnMucG5n" srcset="https://imgproxy.citrusreader.com/RKuO8s7y0WrTrCvBFBebmWtlrPf4baMXrbjNIlw7oTw/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9oaWdoZXItb3JkZXItZnVuY3Rpb25zXzMxMzI5MjE1MzgxNTc3OTkyMzBfaHVfNmZiZGQzM2M2NjRjZGRiYi5wbmc 800w, https://imgproxy.citrusreader.com/FuFxWyUIbCNX58_qwmYwKKyb55n2ZSgNo1Yf0cZzGBs/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA4L2hpZ2hlci1vcmRlci1mdW5jdGlvbnMucG5n 1275w" width="1275"></p> <p>实际上,这个思路和命令式是类似的,但是使用的工具不同。这里的 <code>map</code> 是一个纯粹的函数,唯一特殊的地方,就是接受第一个参数实际上也是一个函数。</p> <p>当然,如果你觉得这样写有点麻烦,我们实际上可以使用「匿名函数」这个特性来进一步简化代码:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">::</span> <span class="p">[</span><span class="kt">Int</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">=</span> <span class="n">map</span> <span class="p">(</span><span class="nf">\</span><span class="n">x</span> <span class="ow">-&gt;</span> <span class="n">x</span> <span class="o">*</span> <span class="mi">2</span><span class="p">)</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span> </span></span></code></pre></div><p>匿名函数允许我们在不给函数起名字的情况下,使用这个函数。这在一些函数特别简单的场景下很有用,比如这里把值翻倍这种简单的需求,用匿名函数可以简化代码。</p> <p>这里的 <code>\x -&gt; x * 2</code>,本质上和上文中起名的函数作用是一致的。</p> <hr> <h2 id="3-进一步讨论">3. 进一步讨论 </h2><p>通过上面的例子,我想大家对高阶函数的一些常见使用场景有了进一步的理解。</p> <p>实际上,尽管提到高阶函数,大家一般会想起函数式语言中特别常用的 <code>map</code>\<code>filter</code>\<code>foldl</code>\<code>foldr</code>,开发者完全可以根据自己的需求,<strong>自由拼装组合各种函数,灵活实现自己的需求</strong>。</p> <p>当需求变得更复杂的时候,我们会发现使用高阶函数的代码往往比传统 <code>for</code> 循环的代码更具表达力,而纯函数特性又会使高阶函数具备<strong>行为确定性、可测试性</strong>。</p> <p>当然,上文提到的 Haskell 例子就是最简略的写法了吗?其实不是。我们还可以这么写:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-haskell" data-lang="haskell"><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">::</span> <span class="p">[</span><span class="kt">Int</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="nf">result</span> <span class="ow">=</span> <span class="n">map</span> <span class="p">(</span><span class="o">*</span><span class="mi">2</span><span class="p">)</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span> </span></span></code></pre></div><p>至于为什么「<code>*2</code>」这也可以是一个函数,就不得不提到函数式语言的另外两个特性:<strong>柯里化</strong>和<strong>部分应用</strong>。这些概念,我将会在本专栏下一篇文章详细说明。</p> <p>当然,即便到这里,我想我们仍然不能说这种高阶函数的写法就一定比 <code>for</code> 循环更出色。</p> <p>但如果我们继续深入学习 Haskell 的知识,在<strong>掌握 <code>Functor</code> 概念后,让 <code>map</code> 推广到 <code>fmap</code></strong>,函数式写法的优雅性才会真正展现。当然,这些内容就要在专栏更靠后的文章才会讨论了。</p> <hr> <p>在文章最后,我还是想再强调一遍:<strong>我们不是只能在函数式语言中才可以使用高阶函数</strong>。</p> <p>比如,开头用 <code>Python</code> 写的代码片段,我们完全可以使用函数式风格写法实现:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">double_list</span><span class="p">(</span><span class="n">num_list</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span> </span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span> <span class="o">*</span> <span class="mi">2</span><span class="p">,</span> <span class="n">num_list</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">double_list</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]))</span> </span></span></code></pre></div><p>我们同样可以使用 <code>Python</code> 内置的 <code>map</code> 函数来完成这个任务。这里的 <code>lambda x: x * 2</code> 是和上文类似的匿名函数。</p> <p>尽管高阶函数写法在 <code>Python</code> 中并非强制的实现方式,我想掌握更多的解决问题工具,对于理解别人的代码、写出表达能力更强的代码都是有帮助的。</p> <p><strong>如果你喜欢这篇博客,欢迎关注我。共勉!</strong></p> <p>本系列的下一篇文章:<a class="link" href="https://www.changsun.work/post/250822-currying-partial-application/" target="_blank" rel="noopener" >函数式心法 (3):巧用柯里化和部分应用</a></p>

2025/8/21
阅读更多

打造 NixOS 开发环境 (2):NixOS学习与配置入门指南

<p>上篇文章主要谈了 NixOS 的高可复现性、可维护性和原子化更新特性如何恰到好处地解决了我的需求。如果你还没有看过,请参考 <a class="link" href="https://www.changsun.work/post/250819-nixos-intro/" target="_blank" rel="noopener" >本系列上一篇文章</a>。</p> <p>从这篇文章开始,我们将开始提一些学习配置 NixOS 开发机时更具体的建议。</p> <hr> <h2 id="1-nixos-真的对新手很不友好吗">1. NixOS 真的对新手很不友好吗? </h2><p>首先,我想要回答一个问题:NixOS 真的对新手很不友好吗?</p> <p>实际上,我觉得起码<strong>从系统安装的角度来看,其实并不难</strong>,想要尝鲜体验一下的难度远低于大多数人想象。</p> <p>NixOS 官网提供了包含图形化界面的安装镜像,以及命令行安装的最小安装镜像。其中,图形化镜像的安装过程和那些公认新手友好的 Linux 发行版并无太大区别,且图形化安装界面会引导你选择你喜欢的桌面环境。</p> <p>也就是说,如果你拿不准主意不知道 NixOS 是否适合你,我的建议就是从官网上下载一个 ISO 镜像<strong>在虚拟机内安装试试,亲自尝试过才知道适不适合</strong>。不论后续配置困难与否,至少安装过程是很丝滑的,可以进入系统以后再慢慢体验和斟酌。</p> <p>事实上,对于下定决心要学习使用 NixOS 的朋友而言,我仍然建议在虚拟机内先尝试一下,并做一些基本的配置工作。不同于其他发行版,我们在虚拟机内做的所有配置调整,未来都是可以迁移到物理机上的,不必担心有重复劳动的问题。</p> <p>也就是说,我们完全可以<strong>想在虚拟机内尝试多久就尝试多久</strong>,直到觉得系统达到相对比较顺的时候再考虑真正在物理机上安装。</p> <p>系统安装的流程很直观,如有任何问题,或者你是 Linux 资深用户并想尝试无图形界面镜像的最小化安装,<a class="link" href="https://nixos.org/manual/nixos/stable/#ch-installation" target="_blank" rel="noopener" >请查阅官方手册的对应部分</a>。</p> <hr> <h2 id="2-nixos-在当下的正确打开方式">2. NixOS 在当下的正确打开方式 </h2><p>在这篇文章写成的时间点而言 (2025 年 8 月),即便 Nix Flakes 仍然是测试阶段,我建议一定要从开始就使用 Flakes 和 home-manager 来管理自己的配置。这两个工具分别解决了不同方向的问题,配合起来使用才可以实现真正高可复现性的 NixOS 环境。</p> <p>Flakes 作为新一代 Nix 工具链的重要一环,即便现在名义上还没有完全稳定,也已经具备了极佳的可用性和开发者体验。向 NixOS 配置中引入 Flakes,才可以真正实现精确到 commit 级的软件源。</p> <p>这主要是因为,引入 Flakes 后系统会自动生成并维护一个 <code>flake.lock</code> 文件,这个 lockfile 本质上和 JavaScript 项目中的 <code>packages.lock</code>,Rust 项目中的 <code>cargo.lock</code> 的功能是一致的。这个由系统自动维护的文件会详细记录软件源 (如 nixpkgs) 的 commit 值并锁定,这在根本上避免了重新构建时可能出现的版本漂移的问题。</p> <p>也就是说,<strong>只有引入了 Flakes,才真正实现了系统层面近乎完美的可复现性</strong>。</p> <p>而 home-manager,解决的是用户层软件配置可复现性差的问题。按照原来的配置模式,所有软件均为系统层全局安装,而配置文件和传统 Linux 系统一样放在用户 home 目录下软件指定的文件夹中,这就意味着需要用户自己想办法处理软件配置文件的备份和版本管理问题。</p> <p>引入 home-manager 后,一方面,一部分软件可以为具体用户安装而不是全局安装,对于有多用户管理需求的使用场景要友好很多,可以实现严格的用户间开发环境隔离。</p> <p>另一方面,由于 home-manager 源软件在打包的时候往往提供了一些 API,用于把原先 YAML、TOML、JSON 或者其他配置文件类型,转化为 Nix 语言配置 (当然用户也可以选择使用原版配置文件),并在新配置 rebuild 过程中自动在 home 目录下对应位置创建指向 <code>/nix/store</code> 目录的软链接。这使得用户层一些软件的配置信息也可以方便地使用 git 进行版本管理。</p> <p>简单来说,home-manager 让我们对于终端模拟器、shell 等很多<strong>用户层配置也可以轻松进行版本管理和跨设备配置精确复刻</strong>。配合 Flakes,<strong>从系统层到用户层的几乎全部细节,都被一套配置文件定义了</strong>,用户可以轻松实现备份、版本管理、迁移等工作。</p> <p>关于 Flakes 和 home-manager 的具体使用和配置,我非常推荐知友 <a class="link" href="https://www.zhihu.com/people/yuansuye" target="_blank" rel="noopener" >@於清樂</a> 写的 <a class="link" href="https://nixos-and-flakes.thiscute.world/zh/preface" target="_blank" rel="noopener" ><em>NixOS &amp; Flakes Book</em></a>。全文使用清晰流畅的中文,用深入浅出的方式介绍了许多稍显复杂的概念。</p> <p>我自己基本就是看这套教程入门的,哪怕只是把基础部分看完,对于 NixOS 终端用户而言就完全够用了,在知识上就具备了定制化 NixOS 开发环境的条件。</p> <hr> <h2 id="3-遇到问题怎么办">3. 遇到问题怎么办? </h2><p>当然,考虑到 NixOS 的复杂性,我想我们在使用过程中不可避免会遇到一些问题。</p> <p>NixOS 被人诟病的一点就是文档混乱分散,质量比 Arch wiki 确实存在差距。但是,当你掌握了一定的检索技巧后,我觉得很多问题实际上还是能找到解决办法的 ── 文档<strong>只是分散,并非完全不存在</strong>。</p> <p>首先,对于想要找“保姆级教程”的朋友,追求文章的可读性和逻辑性,首先推荐的就是上文提到的<em>NixOS &amp; Flakes Book</em>,此外使用英语创作的 <a class="link" href="https://nixos.org/guides/nix-pills/00-preface.html" target="_blank" rel="noopener" ><em>Nix Pills</em></a>。</p> <p>对于 Nix 语言本身,建议先从 <a class="link" href="https://nix.dev/tutorials/nix-language" target="_blank" rel="noopener" >官方 Nix 语言入门教学</a> 看起,先学一下基本的数据结构,有这些基本知识,做简单的系统配置完全够用。</p> <p>至于后续其他深入的 Nix 语言特性,比如柯里化、高阶函数等函数式语言特性,可以等需要的时候慢慢研究,或者移步我目前正在撰文的另一个专栏:<a class="link" href="https://zhuanlan.zhihu.com/column/c_1930265593199698635" target="_blank" rel="noopener" >《函数式心法》</a>。</p> <p>对于具体软件的配置问题,首先还是建议先查 NixOS Wiki。但是,需要注意的是,Wiki 的各个页面良莠不齐,有的页面事无巨细基本配置抄了就能用;有的页面语焉不详,令人不知所云。所以,我们在使用的时候应当好好甄别。</p> <p>而且 NixOS Wiki 实际上还有两个版本,这是 <a class="link" href="https://wiki.nixos.org/wiki/NixOS_Wiki" target="_blank" rel="noopener" >官方版本</a>,这是 <a class="link" href="https://nixos.wiki/" target="_blank" rel="noopener" >用户维护的非官方版本</a>。</p> <p>对于 NixOS Wiki 没有覆盖的软件配置,通常下一步是查阅 <a class="link" href="https://wiki.archlinux.org/title/Main_page" target="_blank" rel="noopener" >Arch Wiki</a> 和 <a class="link" href="https://www.mankier.com/" target="_blank" rel="noopener" >mankier</a> 看有没有信息供参考。</p> <p>当然,查到非 NixOS Wiki 上的配置指引,想要翻译成 Nix 配置还是需要再转化一下的。这时候就可以查官方的 <a class="link" href="https://search.nixos.org/packages" target="_blank" rel="noopener" >NixOS Search</a> 和 <a class="link" href="https://mynixos.com/" target="_blank" rel="noopener" >MyNixOS</a>。</p> <p>到这一步,绝大多数问题都基本可以解决。官方提供的 NixOS Search 是一个兜底性工具,毕竟所有的软件包、系统全局配置选项、home-manager 配置选项以及 Flakes 配置选项等信息都可以查到,并且一般伴有比较详细的说明和示例。</p> <p>如果你仍然有疑问,也欢迎到知乎上提问并邀请我,我非常乐意回答大家的问题。</p> <hr> <p>这篇文章主要谈了 NixOS 安装、Flakes 和 home-manager 的必要性以及遇到问题时的正确解决思路三点。后续几篇文章,我将深入谈一些 NixOS 使用时的技巧,并开始展示如何定制适合自己工作流的桌面环境 (Niri 窗口管理器) 和其他开发工具。</p>

2025/8/20
阅读更多

打造 NixOS 开发工作流 (1):为什么选择 NixOS

<p>NixOS,一个用户评价两极分化相当严重的 Linux 发行版。</p> <p>最近一个星期,我终于下定决心尝试在旧笔记本上安装使用 NixOS,打算将开发环境彻底迁移到这个声明式配置的操作系统上。事实上,从我的个人经验来看,配置使用 NixOS 的难度比我原先预想的要小,最终达到的效果十分令我满意,我终于开始理解为何知乎、Reddit 上不少资深 Linux 用户最后选择 NixOS“养老”了。<strong>NixOS 的声明式配置和原子化特性,完美解决了我多设备维护和更新的痛点。</strong></p> <p>这篇文章将作为后续一整个系列的开篇,详细讲述我选择尝试 NixOS 的目的和核心考量,以及从宏观角度谈谈这个操作系统适合的用户画像。</p> <hr> <h2 id="1-问题描述">1. 问题描述 </h2><p>虽然说我觉得自己其实是有一定的 Linux 使用经验的,之前体验过多个不同的发行版,但从个人开发环境的角度说,除非有特殊的项目需要,我大部分的开发环境都在主力 Windows 笔记本上。</p> <p>实际上,绝大多数时候 Windows 环境配置还是很容易的,对于资源比较充裕的语言和开发工具,支持 Windows 哪怕麻烦也值得尝试去做,绝大多数情况下往往不存在什么问题。但当我开始研究一些相对小众的语言,如 Haskell,或者涉及到底层 C 编译器的问题时,Windows 的解决方案要么不够简单优雅,要么完全不可能。<strong>手边有 Linux 开发环境就显得尤为重要了</strong>。</p> <p>当然,你可能说这完全可以通过虚拟机和 WSL 配合完成。一方面,使用这些解决方案对日常携带的笔记本电脑有着不低的性能要求;另一方面,对一些特殊的开发需求,比如 GUI 软件开发,想要理顺开发环境同样是不轻松的。</p> <p>考虑到我自己手边有一些相对老旧硬件,我当时非常自然的想法是把这些多年前淘汰下来的设备利用起来,把一些非 CPU 密集型任务迁移到这些设备上,同时通过物理机使用 Linux,倒逼自己提升 Linux 的实际使用能力。</p> <p>事实上,我大一的时候就是这么做的,在家和学校各有一台配合使用的 Linux 设备,并将相当一部分开发环境从 Windows 主力机上迁移了出去。</p> <p>但后来放假的时候又遇到了两个问题:</p> <p>其一,在一台设备上配置好的环境,<strong>换到另一地的另一个设备</strong>上工作时,自然发现<strong>完全没有同步配置</strong>。很多东西要么自己维护一个自动化配置脚本,要么就必须自己手动再踩坑配置一遍,完全是时间的浪费;</p> <p>其二,在一地的时候另一地的设备处于不使用状态。对于个人开发机而言,选取滚动发行版并尽量追新软件是一个自然的选择,但滚动发行版的一个重要使用准则就是要紧跟发行版更新,<strong>长期不更新的设备突然更新可能会出现各种依赖解析错误</strong>。</p> <p>我之前碰到过长期不更新 OpenSUSE Tumbleweed 再次更新后的 Python 依赖版本冲突问题,虽不致命,但很多时候被破坏的环境也要花不少时间修复,这在我看来同样是时间的浪费。对我一些使用 Arch Linux 的朋友而言,长期不更新的设备进行更新,往往体验会更“刺激”一些。</p> <p>于是我开始思考:有没有一种解决方案,能够实现开发环境的<strong>一劳永逸</strong>呢?至少,它应当在极大程度上降低切换设备的维护成本,并尽可能把开发环境的痛点问题<strong>提前暴露解决</strong>。</p> <p>于是,我注意到了 NixOS。</p> <hr> <h2 id="2-为什么选择-nixos">2. 为什么选择 NixOS </h2><p>事实上,NixOS 通过其高可复现性的特征,帮助我达成了“配置跟着人走”的目的,可以灵活决定更新的方式和频率。</p> <p>我们来看看对于上文提到的两个问题,NixOS 的解决方式:</p> <p>关于“配置跟着人走”的需求,NixOS 让我实现了通过前期<strong>打磨一套系统层、用户层和具体项目的配置文件</strong>,把整个操作系统中的<strong>各组件和开发环境清晰地、声明式地描述出来</strong>,这套配置文件是可以通过 git 版本管理跟着人走的。这意味着,换一台设备,只需安装系统时对硬件相关配置稍作调整,就可以实现环境的完美复刻。</p> <p>关于滚动更新和长期停机更新的矛盾问题,NixOS 提供的解决方案同样很令人满意。即使用 Unstable 源,在 A 设备上能正常构建的配置,B 设备即便一段时间未更新,升级时也基本不会出现依赖冲突问题。即便真的有问题,NixOS 的<strong>原子化升级</strong>特性也可以兜底,确保了系统不至于陷入“升级了一半”这种尴尬境地,开发者有充分的时间慢慢排查配置错误。</p> <p>总而言之,对于我自己特定的需求而言,NixOS 是一个远优于其他方案的解决方案。</p> <hr> <h2 id="3-nixos-适合所有人吗">3. NixOS 适合所有人吗? </h2><p>NixOS 是一个适合我的选择,但这个发行版适合所有人吗?尽管我非常喜欢这个发行版,我仍然不觉得这是一个适合所有人的解决方案。原因有以下几点:</p> <ul> <li>NixOS 并不简单。这显然不能完全称之为一个新手友好的发行版,我个人建议是至少要有一种别的发行版的使用经验,熟练使用命令行排查问题,对 Systemd 有着基本的了解,这种情况下迁移到 NixOS 的过程才会相对顺滑;</li> <li>Nix 是一门函数式语言。这门语言其实语法很简单,官方文档看两个小时基本就能学个大概,但是毕竟需要一点函数式思维。如果你接触过 Clojure、Haskell 等函数式语言,上手 Nix 会相当简单。</li> <li>NixOS 无法遵守 FHS 标准。为了达到 NixOS 无与伦比的可复现性,你会发现许多 Linux 系统目录内都是向 Nix store 内的只读软链接,而不是像传统发行版那样的真实文件。这个问题并非无解,绝大多数时候主要是开发者在打包一些软件(尤其是闭源软件)需要头疼,对终端用户是无感的。但如果你有特殊需求必须依赖 FHS 标准,NixOS 很可能不是一个方便的选择,需要你深入理解 Nix 打包软件的方法。</li> </ul> <p>当然,如果你是一位 Linux 资深用户,我认为学习使用 NixOS 是<strong>完全值得</strong>的。</p> <p>我一直觉得,一些需要付出一定的初始学习成本,但是<strong>后期可以获得高回报</strong>的技能、工具都是<strong>值得学习</strong>的,也就是<strong>坚持长期主义</strong>。之前学习 Rust 语言,强迫自己适应内存安全的代码规范是如此;练习双拼输入法,提高博客写作效率也是如此;而 <strong>NixOS,我觉得是在操作系统层面提供的一个类似的“长期主义”路径</strong>。</p> <hr> <p>这篇文章将作为一个系列文章的开篇,我将在后续文章中继续分享我是如何打造基于 NixOS + Niri 窗口管理器的桌面环境,使用 Flake 和 home-manager 分别管理系统层和用户层配置,开发环境配置,以及探讨一下在部署时 Nix 和 Docker 容器化技术关系等问题。</p> <p>如果你同样对多设备开发环境感到困扰,或者单纯对 NixOS 的设计哲学感兴趣,欢迎关注我,也欢迎与我随时交流!</p> <p>本系列下一篇文章:<a class="link" href="https://www.changsun.work/post/250820-nixos-install-and-search-basics/" target="_blank" rel="noopener" >打造 NixOS 开发环境 (2):NixOS 学习与配置入门指南</a></p>

2025/8/19
阅读更多

Haskell 心法 (1):初识 - 为何学、学什么和怎么学

<p>Haskell 是一门风格极其独特的语言。网络上我们往往能够看到各式各样对其函数式特性的评价,我相信不少人对其的印象就是一门“函数式纯度高、学术风格浓厚、学习曲线陡峭”的语言。</p> <p>最近机缘巧合下我尝试了一下这个语言。从我个人的感觉而言,这门语言其实并<strong>没有想象中那么难</strong>,很多特性的引入不仅在我看来很自然,对于代码<strong>抽象程度和表达能力</strong>的提升也是肉眼可见的。</p> <p>这篇文章我将以一个有其他函数式语言经验的 Haskell 初学者视角,谈谈我的看法。</p> <p>如果你还没有决定是否要学这门有趣的语言,或者刚刚开始你的 Haskell 旅程,我想看完本文以及计划中的系列文章后,你会有一些全新的想法。</p> <hr> <h2 id="1-为什么要学-haskell">1. 为什么要学 Haskell? </h2><p>对我而言,理由其实很简单:之前接触了不少语言,有些是混合范式语言中具备不少函数式风格,有些虽然是函数式语言,但由于各种设计上的考量最后还是引入了一些没那么函数式的特性。随着对函数式风格理解的逐渐深入,最终接触最纯粹的函数式语言 Haskell 几乎是必然的。</p> <p>当然,除了对我自己,我始终觉得对于任何一个开发人员而言,学习掌握函数式风格语言独特的抽象方式都是有价值的。</p> <h3 id="11-有助于学习其他新语言">1.1 有助于学习其他新语言 </h3><p>首先,学习掌握函数式语言,特别是 Haskell,对于<strong>深入理解其他语言</strong>在新版本中引入的特性是有很大帮助的。</p> <p>比如,掌握了模式匹配、匿名函数等概念,在写 Python 的时候就会发现,新版本引入的模式匹配和匿名函数语法糖其实还是很有用的,合理使用可以提高代码的表达能力和可读性。</p> <p>再比如,理解 Haskell 的高阶函数以及应用场景,会发现可以在 JavaScript 等很多语言用高阶函数简化层层嵌套的循环结构。</p> <p>许多主流语言在近十年间积极吸收函数式语言的发展成果,这使得学习函数式语言,<strong>客观上也达到了学习其他语言创新成果的目的</strong>。而且,由于 Haskell 的纯粹性和体系性,这些函数式风格的<strong>威力和精妙之处</strong>,往往在 Haskell 编码实践中才能得到最深刻的体会。</p> <p>总而言之,即便你以极其功利的目的,学习 Haskell 也不亏。从来没有浪费的学习。</p> <h3 id="12-有助于获得全新的抽象思维">1.2 有助于获得全新的抽象思维 </h3><p>其次,由于函数式风格和传统面向对象的差异很大,使用的抽象思维方式也大相径庭。学习 Haskell 可以让我们在面临同样的工程需求时用截然不同的抽象视角来完成工作。</p> <p>面向对象需要工程师思考类之间的职责划分、继承层次和对象交互;而函数式编程则依赖代数数据结构的设计、不同函数的组合以及副作用的严格隔离等。</p> <p>在一些工程中的重要问题上,Haskell 的函数式特性也带来了和其他语言间的巨大差距。例如,全局不可变性和无副作用函数彻底规避了共享状态竞争,设计并发程序兼具简洁性和工业级效率;基于代数数据类型和 Monad 抽象的 <code>Maybe/Either</code> 等类型,将错误处理编码进类型系统,通过严格的编译期检查覆盖所有分支,避免了许多运行时崩溃。</p> <p>显然,二者所需的<strong>工程思维和架构方法是完全不同的</strong>,对于同一个现实问题,掌握函数式编程的人就可以使用一种风格迥异、许多时候表达能力更强的方式将问题转化成逻辑和代码语言。</p> <h3 id="13-坚若磐石的稳定性">1.3 坚若磐石的稳定性 </h3><p>进一步来说,我觉得即便你不喜欢上述所有提到的特性,有一点是必须说明的:Haskell 通过严格的类型系统使得所有能编译运行的代码有相当的质量保障。</p> <p>Haskell 的严格编译期检查,有一些人可能不太喜欢,会觉得编译期检查影响了编写代码的速度。</p> <p>实际上,这些检查的逻辑在于把很多运行时测试才可能发现的问题<strong>提前到编译期一起解决掉</strong>,确保可编译代码的质量,<strong>极大提升了软件上线后的稳定性和可维护性</strong>。</p> <p>实际上,如果我们充分对比动态语言软件和 Haskell 在测试、维护、重构与拓展等多阶段的情况,我们很可能发现后者的严格检查虽然在编译期需要更多时间处理,但从全局来看<strong>反而是提高了效率</strong>的。</p> <p>当然,Haskell 社区内流传的传说“编译通过即正确”固然是夸张的说法,毕竟编译期无法验证软件业务逻辑是否正确,该有单元测试、集成测试一样都不能少。然而,编译通过意味着代码排除了所有编译期涵盖的错误类别(如类型不匹配、分支覆盖不完备、副作用处理不当等)。这种<strong>编译期给开发者带来的安全感和对核心正确性的信心</strong>,是其他语言无法比拟的。</p> <p>从这个角度而言,我个人觉得唯一可以和 Haskell 实现类似“把错误提早到编译期”理念的语言就是 Rust。不过 Rust 为了实现底层内存操作,引入了所有权、生命周期等概念。如果你觉得某个项目不追求手动控制内存带来的极致性能,可以接受由 GC 来回收内存垃圾带来的便利性和开销,那么 Haskell 是在<strong>更高抽象层次上</strong>践行把问题提前解决的极佳选择。</p> <h3 id="14-极高的-ai-驱动编程契合度">1.4 极高的 AI 驱动编程契合度 </h3><p>顺着上一点往下说,我一直觉得强调严格编译期检查的语言,特别适合 AI 辅助编程,乃至完全由 AI 智能体驱动的开发。</p> <p>原因很简单:编译期错误信息通常是<strong>确定性的、局部的</strong>,并且<strong>直接指向代码中违反语言规则的具体位置</strong>。大语言模型可以相对容易地通过上下文和详细报错信息,尝试推断问题根源并修复错误。</p> <p>对于 Haskell 和 Rust 而言,如果代码有问题,可以直接向大语言模型提供详尽的编译器报错信息,让 AI 根据报错和上下文自主排查错误根源,排除类型错误。</p> <p>反之,如果是 Python 的代码,可能有些时候看似正确的代码其实是有问题的,只有进入运行时才能够被发现。运行时排查问题,除非这个软件本身就有完善的日志系统可以清晰向 AI 提供相关信息和问题复现条件,很多时候解决问题还得靠人类开发者。</p> <p>当然,这不是说 Haskell 生成的代码就一定比 Python 好。实际上,由于 Python 训练语料更多,一般而言 Python 代码的生成质量是不错的。</p> <p>但即便 Python 的代码是正确的,我在使用前往往需要大量测试才敢用;对于 Haskell,AI 如果能生成可编译的代码并且人工复核业务逻辑无误,那么代码的基础健壮性就有了一定的保证,显著降低 AI 生成代码引入隐蔽运行时错误的可能性。</p> <hr> <h2 id="2-核心概念一览表-中英对照">2. 核心概念一览表 (中英对照) </h2><p>学习任何具备一定体系性的知识,首先都需要把“要学什么”的路径图规划清楚。</p> <p>Haskell 作为一种语法设计高度统一的语言,把握住正确的学习方向和路径就尤为重要。而把学习方向的第一步,就是要对核心概念有一个基本的印象。</p> <p>由于 Haskell 中文社区很多大佬的文章往往会混用一些中文表达的概念和英文表达的概念,我觉得有必要提供一个简单的中英文概念对照。很多时候让你一头雾水的某个概念,可能换一个语言环境解释就清楚了。</p> <p>这里,我提供一个中英文对照的概念表,以及一个非常简略的介绍。我认为初学阶段需要掌握的概念包括:</p> <table> <thead> <tr> <th>中文概念</th> <th>英文概念</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>函数定义</td> <td>Function Definition</td> <td>使用 <code>=</code> 定义函数、参数、返回值</td> </tr> <tr> <td>基本类型</td> <td>Basic Types</td> <td><code>Int</code>, <code>Char</code>, <code>Bool</code>, <code>String/List/Tuple</code> 等</td> </tr> <tr> <td>模式匹配</td> <td>Pattern Matching</td> <td>可以代替大量嵌套 <code>if...else...</code> 结构,舒适定义代码的控制流</td> </tr> <tr> <td>匿名函数</td> <td>Anonymous Function</td> <td><code>\x-&gt;x + 1</code>,不需要命名就可以使用的函数</td> </tr> <tr> <td>高阶函数</td> <td>Higher-Order Function</td> <td>接受函数作为参数,或者返回函数的函数,如 <code>map/filter/foldl</code></td> </tr> <tr> <td>柯里化</td> <td>Currying</td> <td>所有函数本质上只有一个参数,<code>(a,b)-&gt;c</code> 等价于 <code>a-&gt;b-&gt;c</code></td> </tr> <tr> <td>部分应用</td> <td>Partial Application</td> <td>只提供函数部分参数,与柯里化高度相关</td> </tr> <tr> <td>代数数据类型</td> <td>Algebraic Data Type (ADT)</td> <td><code>data</code> 关键字定义类型,可以是枚举、结构体或它们的组合</td> </tr> <tr> <td>Maybe 类型</td> <td>Maybe Type</td> <td><code>Maybe a = Just a | Nothing</code></td> </tr> <tr> <td>类型类</td> <td>Typeclass</td> <td>类似接口,定义一组函数,类型通过实现函数来声明支持该行为</td> </tr> <tr> <td>惰性求值</td> <td>Lazy Evaluation</td> <td>Haskell 一大语法特点</td> </tr> <tr> <td>函子</td> <td>Functor</td> <td>可被映射的数据容器或上下文</td> </tr> <tr> <td>应用函子</td> <td>Applicative</td> <td>一种承载了函数的容器,可被应用于 Funtor</td> </tr> <tr> <td>单子</td> <td>Monad</td> <td>代表顺序计算链,用于处理副作用、可选值、错误等上下文中的计算</td> </tr> <tr> <td>do 语法</td> <td>do Notation</td> <td>好用的编写 Monad 计算的语法糖</td> </tr> </tbody> </table> <p>这个列表大体上遵循了从上到下由易到难,如果你还没有开始学 Haskell,我觉得完全可以按图索骥一步一步掌握语言的核心思想。</p> <p>掌握这个列表中的概念,是学习 Haskell 语言和阅读社区代码的<strong>关键基础</strong>。当然,实际项目中还需要理解更多细节和工具链的使用,但把握住这些概念就具备了<strong>构建复杂软件的基本能力</strong>。</p> <hr> <h2 id="3-haskell-怎么学">3. Haskell 怎么学 </h2><p>我觉得 Haskell 学习过程中最重要的一点,就是<strong>多找教程交叉比对</strong>。</p> <p>我之前<a class="link" href="https://www.changsun.work/post/250515-resource-aquiring/" target="_blank" rel="noopener" >专门写过一个回答论证“你学不明白不见得是你的问题,可能是没找对教程的问题”</a>,我觉得理解这一点在学习 Haskell 中十分重要。</p> <p>Haskell 语言很多高级抽象工具,像是 Monad,想要单凭一篇文章讲明白其实是很困难的,但如果你读了二三十篇技术博客谈这一个概念,即便仍然不能完全理解,起码<strong>多篇不同侧重点的文章建立起对一个概念的立体理解</strong>。</p> <p>总而言之,找到适合自己理解的教程是相当重要的,而唯一能确保这一点的就是先浏览大量材料,再从中挑选契合的文章重点精研。</p> <p>此外,我还想说一点就是<strong>实践优先,在做中学</strong>。</p> <p>Haskell 本身的语言设计可能有不少学术成分,网络上一些人也有论及 Haskell 必谈抽象代数、范畴论的习惯。</p> <p>我不是说理解背后的深层次数学思想无用,只是如果你对这些数学工具了解不多,大可不必被吓到。实际上,Haskell 选择这些语言特性更多是出于实践因素的考量,只有不断在项目中实践,才能够真正理解深层次的设计理念。</p> <p>须知,<strong>不必深入理解数学思想,也可以高效运用 Haskell 解决问题</strong>。理解底层数学原理<strong>有助于揣摩设计动机,但绝不是使用 Haskell 的门槛</strong>。</p> <p>最后,如果你对这个语言感兴趣,欢迎关注我,我将在未来的一段时间持续讲解编写 Haskell 的心得体会和实践经验。</p> <p>本系列下一篇文章:<a class="link" href="https://www.changsun.work/post/250821-higher-order-functions/" target="_blank" rel="noopener" >函数式心法 (2):理解运用高阶函数</a></p>

2025/7/19
阅读更多

Clojure 笔记 (2):基本语法结构和 REPL 驱动编程

<p>上篇文章我们聊了 Clojure 语言的基本特性和语言生态,这篇文章我想讲讲 Clojure 的语法,以及最有特色的 REPL 驱动编程。</p> <p>欢迎查看<a class="link" href="https://www.changsun.work/post/250518-rust-cli-3/" target="_blank" rel="noopener" >这个系列的上一篇文章</a>,也欢迎关注我的<a class="link" href="https://www.changsun.work/projects/2505-rust-cli/" target="_blank" rel="noopener" >全栈开发笔记系列专栏,我将更新更多精彩内容。</a></p> <hr> <p>Clojure 一系列神奇的特性,比如非常好用的 REPL 驱动开发、基于宏的元编程等。</p> <p>究竟是什么带来了这一系列可以极大提高开发者体验的特性呢?我希望在这篇文章初探一下。</p> <h2 id="1-clojure-的基本语法结构">1. Clojure 的基本语法结构 </h2><p>如果你看过 Lisp 语系的语言,一定会对其满屏的<code>()</code>小括号印象深刻。</p> <p>不同于 C 系的语言普遍使用<code>{}</code>来划分代码段,Lisp 系语言使用的是层层叠叠的小括号。</p> <p>我们来看一个 Clojure 官网提供的示意图:</p> <p><img alt="Clojure的S表达式 (Source: clojure.org)" class="gallery-image" data-flex-basis="475px" data-flex-grow="198" height="202" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/wcVqE0paYF9ElWYHtgMX-4Tf4b3LTrLPtkVcp4fLHKw/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsai1zLWV4cHJlc3Npb25zLnBuZw" width="400"></p> <p>这个示意图上使用了一个非常基本的 Clojure 表达式:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">(</span><span class="nb">+ </span><span class="mi">3</span> <span class="mi">4</span><span class="p">)</span> </span></span></code></pre></div><p>这和其他语言的<code>3 + 4</code>的含义是一样的,最后计算出的结果也是 7。</p> <p>用 Clojure 的语法概念来分析,<code>+</code>加号一个符号,这个符号代表的是加法函数。后续的数字<code>3</code>和<code>4</code>,是传入函数的参数。</p> <p>从另一个角度说,这整个表达式不仅仅表示了一个函数。如果我们不考虑这行代码在执行时的作用,只看字面形态,这个<code>()</code>由小括号括起来的结构实际上也可以代表 Clojure 原生数据结构:列表(List)。</p> <p>也就是说,每个 Clojure 的表达式实际上也是一个 List,我们可以去评估这个表达式的执行结果,也可以像操作一个列表一样来操作这个表达式。</p> <p>这个特性,实际上对于 Clojure 实现方便的宏编程有着重要的作用。当然,要讲到宏,还需要再写 2~3 篇文章,欢迎大家关注我。</p> <hr> <h2 id="2-独特的代码评估过程">2. 独特的代码评估过程 </h2><p>如果你接触过 Java,一定熟悉 Java 的代码评估流程。我们来看一个同样援引自 Clojure 官网的流程图:</p> <p><img alt="Java源代码评估流程图 (Source: clojure.org)" class="gallery-image" data-flex-basis="539px" data-flex-grow="224" height="178" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/lgx0AURsB1lY2CxdOSxh5IslqgDdqVJeaJL7QBNfOyY/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2phdmEtZXZhbHVhdGlvbi1wcm9jZXNzLnBuZw" width="400"></p> <p>Java 的源代码被编译器<code>javac</code>编译成可以在 JVM 上运行的字节码。</p> <p>那么,同样作为运行在 JVM 上的语言,Clojure 的策略是什么样的呢?</p> <p><img alt="Clojure独特的评估流程 (Source: clojure.org)" class="gallery-image" data-flex-basis="428px" data-flex-grow="178" height="224" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/lHWoMz4k13Vii4jMQBE_4nP90l1kwkHnJ7JdMJn3ZSs/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsai1uZXctZXZhbHVhdGlvbi1wcm9jZXNzLnBuZw" width="400"></p> <p>源代码在被编译成字节码前首先经历了一步,就是被<code>Reader</code>读取成了 Clojure 数据结构 ── 也就是上文提到的 List。</p> <p>这种设计就意味着:</p> <ol> <li>Clojure 代码的最小执行单元不再是源代码文件,而是一个由<code>()</code>括起来的表达式;</li> <li>由于源代码文件实际上也被读取成一个个表达式,那么其实完全可以由开发者交互性输入一个个需要的表达式然后执行,这就为 REPL 驱动开发提供了可能;</li> <li>由于把源代码转化成数据结构的<code>Reader</code>和编译为字节码的<code>Compiler</code>两个步骤被严格分开,用代码生成代码的思路(也就是宏)成为可能。宏在编译阶段(Compiler)的早期进行展开,而这个编译阶段的输入是 Reader 产生的数据结构。宏展开发生在将数据结构编译成字节码之前;</li> </ol> <p>下面,我们具体重点讨论第二点,也就是 REPL 驱动开发。</p> <hr> <h2 id="3-repl-驱动开发">3. REPL 驱动开发 </h2><p>所谓 REPL,指的是“Read-Eval-Print-Loop”,也即读取 ── 评估 ── 打印循环。</p> <p>如果你用过 Python 的 Jupyter Notebook 或者是 Wolfram Mathematica,我相信你一定不会对 REPL 感到陌生。每次需要修改代码时,无需从头到尾执行一遍代码文件,而是可以只执行一个较小的代码片段,立刻看到这个代码片段的执行结果。</p> <p>事实上,Clojure 的 REPL 要比 Python、Wolfram 语言的 REPL 要好用得多。究其原因,还是因为 Lisp 语系独特的语法结构。</p> <p>由于任何一个由<code>()</code>小括号括起来的表达式都可以执行,我们可以尝试按照任意顺序、执行任意嵌套层级的表达式。</p> <p>举一个简单的例子:</p> <p>这是我配置好的 VSCode + Calva 插件的环境,这是我从一个代码文件中间随便挑出的代码片段:</p> <p><img alt="一段简单的代码示例" class="gallery-image" data-flex-basis="417px" data-flex-grow="174" height="305" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/EV_5y3Yw41M0JwnxWYZoOoj43L0RJnthCl4SufmXWRw/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L3JlcGwtZGVtby1jb2RlLnBuZw" width="531"></p> <p>我可以随性所欲地执行代码片段看看结果:</p> <p><img alt="使用Calva评估每行代码" class="gallery-image" data-flex-basis="489px" data-flex-grow="204" height="316" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/J6GzZ-85buXfNMHuhfi_OhKJdHDJlfPgsx7MMbkRrV4/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L3JlcGwtY29kZS1ldmFsdWF0ZWQtMS5wbmc" width="645"></p> <p>这种即时反馈和探索能力,是不是极大地提升了开发体验?</p> <hr> <p>总结来说,Clojure 基于 S 表达式(S-expression)的语言特性,使其天然具备了实现强大开发工具(如 REPL)和语言扩展能力(如宏)的基础,带来了许多独特且高效的开发体验。</p> <p>今天这篇文章着重讲了 REPL 驱动开发的基本概念。下面几篇文章,我将谈谈 Clojure 的基本数据结构,流程控制,以及函数式风格的高阶函数写法等问题。</p> <p><strong>关注我,不错过更多有趣的技术博客!</strong></p>

2025/6/22
阅读更多

Clojure 笔记 (1):语言生态介绍和环境配置

<p>这篇文章我们来说说 Clojure 这个语言,Clojure 在 web 领域的生态情况,以及如何配置自己的本地环境使其能够顺畅编写 Clojure 代码。</p> <h2 id="clojure-语言介绍">Clojure 语言介绍 </h2><p>我之前了解到这个语言,主要是一次偶然的机会对 Lisp 系的语言产生了兴趣。</p> <p>作为一种设计远远超前于当时时代的语言,Lisp 语言生不逢时,未能获得与其优秀设计理念相匹配的广泛普及和应用。</p> <p><img alt="Lisp语言的百科介绍" class="gallery-image" data-flex-basis="508px" data-flex-grow="211" height="889" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/3Oyc0CC1iWBkzcaK4YiP-8PxD1orjKKQwr_7Z_ZIIKk/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2xpc3AucG5n" srcset="https://imgproxy.citrusreader.com/38Qo7psq28DtsdJv8AXJGPgsLw9KXk4YgA0FlBiKlbU/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9saXNwXzExMDMwMjM4OTIzMzYxNDc3MDc1X2h1XzI0MGRmOWExNTM4Y2EyNGUucG5n 800w, https://imgproxy.citrusreader.com/uOUTdDN4SzraqBurnGRzM1BfrRJJNMCE7gZUhmJaJQ0/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9saXNwXzExMDMwMjM4OTIzMzYxNDc3MDc1X2h1Xzc0OWE0ZTI0NDUyMzllZjYucG5n 1600w, https://imgproxy.citrusreader.com/3Oyc0CC1iWBkzcaK4YiP-8PxD1orjKKQwr_7Z_ZIIKk/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2xpc3AucG5n 1882w" width="1882"></p> <p>然而,即便如此,从 Lisp 中诞生的一系列优秀的设计理念并没有被埋没,反而在后续大量的其他语言中被继承,被发扬光大。</p> <p>举例而言,像是函数式编程的范式、基于宏的元编程思想等等,这些思想在其他各种语言中或多或少地得到了继承和融合。</p> <p>当然,还有一些更贴近原本 Lisp 的语言。这些语言一般都是在 Lisp 语言基础语法上稍加修改,我们一般称之为 Lisp 方言。</p> <p>Clojure 就是这么一种 Lisp 方言。这个语言依托于 JVM 生态,可以将 Lisp 风格的代码编译成字节码在 JVM 上运行。这样设计,就可以让 Clojure 可以比较方便地调用 Java 生态下的库和函数,方便开发者开发复杂的项目。</p> <p>从结果上来看,Clojure 这种“背靠大树好乘凉”的策略是成功的。在目前所有 Lisp 方言中,可以说目前 Clojure 的生态是相当不错的,社区也很活跃。</p> <p><img alt="Clojure官网介绍" class="gallery-image" data-flex-basis="337px" data-flex-grow="140" height="905" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/PmBAcYd9FCWSMbiJ-gmhmW3oYJ_zGQmu-6HbTHk0U7M/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsb2p1cmUtb2ZmaWNpYWwtc2l0ZS5wbmc" srcset="https://imgproxy.citrusreader.com/T7dX09393q2o0c9_OVL-7IT-DJlYYyYmJ-WrT9DvVlc/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9jbG9qdXJlLW9mZmljaWFsLXNpdGVfMTI5MzE0MDI4NzExMjgwMzEyMzhfaHVfYjE2MWE2YzI5MTEyNGM3NS5wbmc 800w, https://imgproxy.citrusreader.com/PmBAcYd9FCWSMbiJ-gmhmW3oYJ_zGQmu-6HbTHk0U7M/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsb2p1cmUtb2ZmaWNpYWwtc2l0ZS5wbmc 1274w" width="1274"></p> <hr> <h2 id="clojure-的-web-生态">Clojure 的 web 生态 </h2><p>既然背靠 JVM,一个绕不开的话题就是 web 后端服务。这是我们这个专栏后续会主要关注的方面。</p> <p>实际上,从我目前的学习体验来看,Clojure 是非常适合用于 web 后端开发的。</p> <p>Clojure 本身的函数式特性使得网络请求处理的生命周期清晰明确;宏和元编程赋予了 Clojure 无与伦比的表达能力,可以用很少的代码就完成复杂的业务需求;基于 REPL 的开发模式大大提高了开发效率;由于背靠 JVM,即便出现了某个冷门库缺失的情况,也可以轻松调用 Java 生态的库来解决。</p> <p>不同于在 Java 内具备统治性地位的 Spring 全家桶系列,Clojure 语言没有一个大而全的后端框架,而是有一系列松散的库的合集。</p> <p>Clojure 生态似乎比较排斥“框架”的概念,很多人都觉得框架太重了,而“库”这个体量刚刚好。</p> <p>实际上,Clojure 的后端生态由围绕在 Ring 这个基本库构建的一系列互相配合的库构成。既然被称为库,绝大部分库的核心代码行数往往就在 1k~10k 这个水平,每个库各司其职,需要开发者掌握的接口数量一般都很少。</p> <p><img alt="Clojure web生态核心:Ring库" class="gallery-image" data-flex-basis="497px" data-flex-grow="207" height="817" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/ApqHuuH3dh3sDMmsgoqwFZswSuiYnwGU5V23OOggs1o/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L3JpbmctZ2l0aHViLXNpdGUucG5n" srcset="https://imgproxy.citrusreader.com/KXQoltYQF4ZrbWqrgZktlsWVKoX6d8iusVcdOrXPDFo/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9yaW5nLWdpdGh1Yi1zaXRlXzcwMzgzNjkzOTE3ODk5MTQ4OTFfaHVfOWNjZTg4ZDA3MTMxNmQ4Zi5wbmc 800w, https://imgproxy.citrusreader.com/YETAWHZTn544gJfLftZBRzmUgKhWt525plb7iTsQBy0/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9yaW5nLWdpdGh1Yi1zaXRlXzcwMzgzNjkzOTE3ODk5MTQ4OTFfaHVfMWQ3MWUzZmE2ZGQ0ZTJmZC5wbmc 1600w, https://imgproxy.citrusreader.com/ApqHuuH3dh3sDMmsgoqwFZswSuiYnwGU5V23OOggs1o/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L3JpbmctZ2l0aHViLXNpdGUucG5n 1695w" width="1695"></p> <p>不少库甚至不需要一个专门的文档网站,只需要读完一个不长的 README 文件就掌握了全部核心技巧了。</p> <p>所以,尽管 Clojure 的 web 生态看上去要学很多东西,但实际上只需要掌握几个核心库就可以启动项目了,其他库可以根据需要渐进式添加。</p> <p>在本专栏后续的文章中,我们会首先简要讲解 Clojure 语言的基本特性,然后深入介绍围绕 Ring 的各个 Clojure 后端库。</p> <hr> <h2 id="关于环境配置">关于环境配置 </h2><p>作为专栏第一篇写 Clojure 的文章,我自然要稍微提一下 Clojure 的环境配置问题。</p> <p>Clojure 目前主流的项目管理方案有两个:一个是比较老的 leiningen 工具,另一个是更新一点的 Clojure CLI + deps.edn。</p> <p>如果你之前从来没有接触过 Clojure,我强烈建议从入门时就使用更新一些的 Clojure CLI 作为主要练习的工具,这是目前社区的主流。</p> <p>安装 Clojure CLI 的步骤很简单,参照<a class="link" href="https://clojure.org/guides/getting_started" target="_blank" rel="noopener" >官网上说的操作流程</a>即可。</p> <p>对于 Windows(非 wsl)用户而言,官网上建议的是用一个由社区维护的 msi 安装包,但我个人倒是更建议使用如下方式配置好 scoop,这样就可以使用 scoop 进行方便的包管理和更新了。</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">scoop bucket add java </span></span><span class="line"><span class="cl">scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure </span></span><span class="line"><span class="cl">scoop install temurin-lts-jdk clj-deps leiningen </span></span></code></pre></div><p>这里我们安装了开源的 JDK 实现 temurin JDK 的长期维护版。此外,cljs-deps 涵盖了 Clojure CLI 以及一些其他可能需要的周边工具,以及旧项目可能需要的 leiningen。</p> <p>安装完后,执行<code>clj</code>命令,看到如下结果:</p> <p><img alt="Clojure CLI成功安装" class="gallery-image" data-flex-basis="470px" data-flex-grow="195" height="412" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/Gb2_gQ1vBNFfNCagDOvjPwlApqHhTYsDIHJDZxvUd78/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsai1jb21tYW5kLWxpbmUucG5n" srcset="https://imgproxy.citrusreader.com/qoukAkzsASvyTbfpXZDQmYbWeuVdYo2HuIlml7Dtf0c/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9jbGotY29tbWFuZC1saW5lXzE5NzQ3NDA5OTQzMTg5MTIwMzdfaHVfYmI5MGVlZTNjMWQ5MzkyLnBuZw 800w, https://imgproxy.citrusreader.com/Gb2_gQ1vBNFfNCagDOvjPwlApqHhTYsDIHJDZxvUd78/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2Nsai1jb21tYW5kLWxpbmUucG5n 807w" width="807"></p> <p>到这里,就算环境配置成功了。</p> <p>专栏的下一篇文章,我打算接着聊如何配置 VSCode,使其能够完美实现 REPL 驱动的开发模式,显著提高开发效率;并尝试开始讲 Clojure 的基本语法。</p> <p><img alt="点击阅读下一篇文章" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/DNd3j1mBwDm8xNel2TjqU3wUXKLx47RAq9UtM4OyXeY/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9wb3N0LzI1MDYyMi1jbG9qdXJlLTItcmVwbC8"></p>

2025/6/19
阅读更多

前端笔记 (1):ES 标准和 ES6+ 变量定义方法

<p>从这篇文章开始,我将新开一个全栈开发笔记的专栏,专门记录我从基础开始学习前后端开发的收获和心得。</p> <p>欢迎收藏本站或者关注<a class="link" href="https://www.changsun.work/projects/2506-full-stack/" target="_blank" rel="noopener" >我的专栏</a>。</p> <hr> <h1 id="ecmascript-标准">ECMAScript 标准 </h1><p>我们在学习前端技术的时候常听说要打好<code>JavaScript</code>基础,那么什么是<code>JavaScript</code>基础呢?</p> <p>在我看来,其中很重要的一个环节就是要学习<strong>符合最新 ECMAScript(也就是我们常说的 ES)标准</strong>的<code>JavaScript</code>语言。</p> <p>过去,我们总能看到网友用各种段子、梗图调侃<code>JavaScript</code>语言。实际上,这个语言确实在设计之初有着不少缺陷,给人的感觉就是不严谨。</p> <p><img alt="JavaScript梗图1" class="gallery-image" data-flex-basis="214px" data-flex-grow="89" height="894" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/R_q9i7I9v4iJ7jPIdZcIWefxjcO3qIuAxH1D9ORi418/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2pzLW1lbWUxLmpwZw" width="800"></p> <p><img alt="JavaScript梗图2" class="gallery-image" data-flex-basis="266px" data-flex-grow="110" height="718" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/5SLJ-J_6s6zpNQeVZ2kKuplHSwMskO6F6QFWQknkEdc/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L2pzLW1lbWUyLnBuZw" width="796"></p> <p>但随着<code>JavaScript</code>和浏览器运行环境彻底绑定,成为前端事实标准后,有不少行业大佬在尝试引入新的语言风格和标准,来让这个语言变得更好。</p> <p>此外,如果想要<strong>学习 TypeScript</strong>这个在<code>JavaScript</code>基础上更进一步、引入更严谨的类型系统的语言,则必须要学习和接受这些<code>JavaScript</code>的新标准,<strong>主流前端框架</strong>同样要求我们对这些内容有着较高的熟悉度。由此看来,学习 ES6+的<code>JavaScript</code>语法是非常有必要的。</p> <p><code>JavaScript</code>语言的新标准是由国际标准组织 ECMA International 推动的,所以这些标准又称<strong>ECMAScript 标准,简称 ES</strong>。</p> <p>这些标准在制定、形成共识后,会由主流的 Chrome、Firefox 等浏览器推动兼容,<code>Node.js</code>等主流运行时也跟进新特性支持,从而实现从语言设计到运行环境落地的过程。</p> <p><img alt="ES标准演化时间线" class="gallery-image" data-flex-basis="480px" data-flex-grow="200" height="500" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/_91Et5IXEfpzPjj58tpdXHzDgxRy5FQUhwpP15heLyU/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L0phdmFTY3JpcHQtVmVyc2lvbnMucG5n" srcset="https://imgproxy.citrusreader.com/71Zs7zezEI61xkKbTM0Dfe3fBkvc3RMihIILDuSqmrY/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9KYXZhU2NyaXB0LVZlcnNpb25zXzU5ODc5OTQ3NTM0MjQyNTMzMzVfaHVfYzI4NTgyMzNhMTJiYzdhZi5wbmc 800w, https://imgproxy.citrusreader.com/_91Et5IXEfpzPjj58tpdXHzDgxRy5FQUhwpP15heLyU/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA2L0phdmFTY3JpcHQtVmVyc2lvbnMucG5n 1000w" width="1000"></p> <p>其实 ES6 及以后的新标准内容是相当丰富的,我们一篇文章也很面面俱到。这篇文章,我将从最基本的变量声明内容讲起,一步步深入到更复杂的内容。</p> <hr> <h1 id="constlet的引入"><code>const/let</code>的引入 </h1><p>过去<code>JavaScript</code>定义变量的唯一关键字就是<code>var</code>,而从 ES6 开始,我们一般建议使用<code>const</code>和<code>let</code>关键字来定义变量。</p> <p>顾名思义,<code>const</code>和<code>let</code>的区别主要在可变和不可变上,其中<code>let</code>为可变引用,可以重复赋值;<code>const</code>声明的是<strong>不可重新赋值的绑定</strong>。</p> <p>需要注意的是,这里<code>const</code>的不可变性主要是指这个引用(或者再直白一点,内存地址)不能改变,但是如果被引用的对象或数组发生变化,这是合法的。</p> <p>换言之,就是<code>const</code>对于原始类型是值不可变,对于对象、数组这样的复合类型就是地址不变。</p> <p>举个例子:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">t</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">];</span> </span></span><span class="line"><span class="cl"><span class="nx">t</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">t</span><span class="p">);</span> <span class="c1">// 合法,打印出[1, -1, 3, 5] </span></span></span><span class="line"><span class="cl"><span class="nx">t</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">5</span><span class="p">];</span> <span class="c1">// 不合法,不可重复赋值 </span></span></span></code></pre></div><p>这段代码是完全合法的,因为尽管<code>t</code>引用的数组新加入了一个数字 5,但是引用本身(内存地址)是没有改变的。</p> <p>那么,你可能会问,这么做的好处是什么呢?</p> <p>最显而易见的好处就是把可变和不可变引用分开了,代码更清晰,但是实际上还有更深层次的原因。</p> <p>一个非常重要的原因就是原先<code>var</code>在设计变量作用域时存在缺陷。</p> <p><code>var</code>采用的是函数作用域,也就是说一个变量一经定义,在整个函数中都是可访问的。这种情况下可能会导致变量重复声明带来的数值覆盖,或者其他的意外行为。</p> <p>反之,<code>let</code>和<code>const</code>采用的是相对更严谨的块作用域定义。也就是说,二者的作用域是明确限定在一对大括号<code>{}</code>里的。</p> <p>我们来看一个例子:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">printing</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>这个函数打印出来的结果,除了<code>for</code>循环打印出的 0~9,for 循环外的<code>console.log(i)</code>会打印出 10。这一点,其实是不够理想的。</p> <p>但是如果我们换用<code>let</code>来定义这个<code>i</code>变量:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">printing</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span> <span class="c1">// ReferenceError: i is not defined </span></span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>此时,<code>for</code>循环外的<code>console.log(i)</code>就会报出<code>ReferenceError</code>,因为<code>let</code>定义的变量是块级作用域,离开了 for 循环的块这个变量就不可以再访问了。</p> <hr> <h1 id="下一步写作计划">下一步写作计划 </h1><p>相信通过这简短的介绍,大家对 ES6+中最基本的变量定义问题有了初步的了解。</p> <p>在后续几篇文章中,我将进一步展开讲解最新 ES 标准中的函数定义、解构赋值、异步编程等问题,然后逐步延伸到<code>Solid.js</code>前端框架以及基于<code>Rust</code>的后端框架,尽全力将全栈开发的知识用通俗易懂的语言讲清楚、讲明白。</p> <p><strong>欢迎关注我,我将给你带来更多精彩文章!</strong></p>

2025/6/3
阅读更多

当我们发现毕生追求的知识在AI面前毫无价值时,如何重建存在的意义?

<p>我们不妨换个角度思考一下:<strong>难道我们存在的意义,在于自己知道多少知识吗?</strong></p> <p>如果真的如此,那岂不是知识越多,人生意义越大?而一个知识有限、探索欲有限的人,人生就近乎毫无价值了?</p> <p>果真如此吗?</p> <p><img alt="AI和人究竟是什么关系?" class="gallery-image" data-flex-basis="427px" data-flex-grow="178" height="719" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/gVbiUk_mq1gFukXC1Ode-Zy1qJuUqETed0H2MUD6i8U/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2FydGlmaWNpYWwtaW50ZWxsaWdlbmNlLTc3MDY5NjNfMTI4MC5qcGc" srcset="https://imgproxy.citrusreader.com/6HDtfTyiJk7D5ZF73o-7XHYRnaIyqAaBmSYFdEWXeX4/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9hcnRpZmljaWFsLWludGVsbGlnZW5jZS03NzA2OTYzXzEyODBfNzA4ODE0NzQzODk3ODcwOTQ1NV9odV80NjY4MDVlZjk3MDkyYmUuanBn 800w, https://imgproxy.citrusreader.com/gVbiUk_mq1gFukXC1Ode-Zy1qJuUqETed0H2MUD6i8U/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2FydGlmaWNpYWwtaW50ZWxsaWdlbmNlLTc3MDY5NjNfMTI4MC5qcGc 1280w" width="1280"></p> <hr> <h2 id="知识本身不是人生的意义和价值">知识本身不是人生的意义和价值 </h2><p>其实我们仔细思考一下便会发现,这个问题其实没有那么复杂,AI 的崛起也远不至于让大家那么悲观。</p> <p>人生的意义和价值<strong>从来不简单取决于个体掌握知识的多少</strong>。</p> <p>的确,了解知识的广度再广一些,深度再深一些,很多时候会让我们的人生变得更从容一些。</p> <p>但是从来没有人规定,我们人生的意义、价值,所感受到的幸福感,对社会的贡献等等,一定与知识的多少呈绝对的正相关。</p> <p>换言之,即便一个人对学习知识没有那么热衷,可能终其一生没有在很大程度上推动人类知识和思想的进步,但是这个人的人生同样可以是有价值的,可以是对社会有利的。</p> <p>那么,你可能就要问了,人生的真正意义和价值在于什么呢?</p> <hr> <h2 id="关键在于实践">关键在于实践 </h2><p>当然,我刚刚提的问题显然没有标准的正确答案。</p> <p>对我来说,人生的真正意义和价值,<strong>核心在于实践</strong>,在于切实改变我们所处的世界。</p> <p>了解更多的知识固然很好,但是很多时候仅了解知识是不足以扩展我们人生内涵的。</p> <p>要想真正让知识在我们的人生中体现出足够的价值,势必要把知识运用于实践,要在现实生活中运用这些知识,让自己和所处环境变得更好。</p> <p>举个例子,比如我现在正在回答的这个问题,其实自己想明白了这个问题当然也不错,但是真正能给我带来价值感的是把这篇文章发表出来,哪怕能帮助看到这篇文章的一两个人,也足够让我满足了。</p> <p>从这个角度,其实 AI 的出现当然<strong>不会让我丧失存在的意义</strong>。</p> <p>为什么呢?</p> <p>AI 固然知道很多东西,掌握的知识和信息不可不谓全面,但是就目前的技术水平而言,AI 距离独立的实践能力还有着相当的距离。</p> <p>实际上,AI 的大部分知识其实就来源于互联网信息资源。这些资源过去就是存在的,有人会觉得人类文明所积累起的浩瀚知识存量使我们的人生变得毫无意义了嘛?</p> <p>在 AI 这个多领域基础知识的集大成者面前保持敬畏之心,我觉得这没错;但如果因此感到自我价值被否定,那似乎就不太符合逻辑了。</p> <p>我再举个例子:</p> <p>AI 或许了解的环境保护知识远远多于常人,对风沙治理的理解入木三分,可是真正让我们感受到可以称得上“伟大”的,是一代又一代人种起的三北防护林。</p> <p>植树造林的知识固然很好,可是要说意义价值层面的问题,只有把知识落实到实践中去才能体现,只有三北防护林这样的宏伟工程<strong>落到实处</strong>时才会让人倍感震撼。</p> <p><img alt="三北防护林工程示意图" class="gallery-image" data-flex-basis="312px" data-flex-grow="130" height="492" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/MO0YvXDO2eRmEt7cTiUTETjrbGROp6BfsUlpy-AHMQE/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L3Nhbi1iZWktZm9yZXN0LmpwZw" width="640"></p> <hr> <h2 id="ai-可以是实践的助力">AI 可以是实践的助力 </h2><p>既然实践是价值和意义的根源,那么其实这一轮 AI 浪潮对我们人生的影响反而明确了:<strong>AI 可以帮助我们实践</strong>,显著加速我们通过实践实现人生意义和价值的进程。</p> <p>也就是说,经过一定的论证,我们得到了一个和问题相反的结论,也即 AI 对我们人生价值和意义的影响是<strong>积极而非消极</strong>的。</p> <p>其原因在于,AI 可以在知识积累层面提供巨大的帮助。只要有恰当的和 AI 配合工作的办法,原先缓慢的知识积累过程可以显著加速,在实践中迭代理论的过程同样可以显著加速。</p> <p>有了 AI 的帮助,许多搜集信息、积累背景知识等拖慢实践周期的步骤都可以加快节奏。随着 AI 推理能力的提升,一些复杂的矛盾综合体也可以交由 AI 进行分析,AI 甚至可以参与到我们人生的重大决策中,提供意见供我们参考。</p> <p>这些助力,或许就可以让一些后悔和遗憾不再发生,让我们更有信心面临实践中遇到的各种困难。</p> <p><img alt="AI是我们实践途中的助力" class="gallery-image" data-flex-basis="160px" data-flex-grow="66" height="1917" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/H2BFPhibUlVznI9rzhQS-ek3YwX5lh_BCPxp8KfItkM/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2h1bWFuLWNvbXB1dGVyLmpwZw" srcset="https://imgproxy.citrusreader.com/wiKiesVolJecmph0lhlqMqf5pFE6daCbjdJkiZydM98/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9odW1hbi1jb21wdXRlcl84NzM5ODc5NzUyNTc4MzY5OTNfaHVfMWRlMzFjMWRhOTQ1MzNjMi5qcGc 800w, https://imgproxy.citrusreader.com/H2BFPhibUlVznI9rzhQS-ek3YwX5lh_BCPxp8KfItkM/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2h1bWFuLWNvbXB1dGVyLmpwZw 1280w" width="1280"></p> <hr> <h1 id="总结">总结 </h1><p>最后总结一下,其实问题的核心还是在于实践。</p> <p>大家不要被 AI 的知识量吓住了。AI 知道的东西再多,最后还是要依赖<strong>我们每一个人</strong>把这些知识和理论转化成切实改变我们的生活的实践。</p> <p>意义和价值源自实践,而实践的根本,<strong>永远在于人本身</strong>。AI,终究是我们手中的工具,而非实践的主体。</p>

2025/6/2
阅读更多

5 月 28 日 DeepSeek R1 模型完成小版本试升级并开源,具体有哪些提升?

<p>这篇文章回答了这个知乎问题:<a class="link" href="https://www.zhihu.com/question/1911132833226916938/answer/1911529497615901312" target="_blank" rel="noopener" >5 月 28 日 DeepSeek R1 模型完成小版本试升级并开源,具体有哪些提升?使用体验如何?</a></p> <hr> <p>总结了一下我目前直接观察到的特点,包括:</p> <ol> <li><strong>鼓励提问者,说话更有“人味儿”</strong></li> <li><strong>开始使用 ta 来代表提问者</strong></li> <li><strong>输出更为复杂的流程图</strong></li> </ol> <hr> <h2 id="1-鼓励提问者更有人味儿">1. 鼓励提问者,更有“人味儿” </h2><p>之前的 DeepSeek-R1 给人的感觉更像是一个知识渊博但是比较严肃的专家,回答我们的问题时不苟言笑,一般都是在分析完用户需求后直截了当地提供解答。</p> <p>这次更新完之后,我最直接的一个感受就是 R1 的回答开始鼓励提问者了,特别是你在表达了面临的一些困难时。</p> <p>举个例子,如果我们尝试提提问:我最近在学编程,但总是写 bug,有点沮丧。你能给我这个初学者一些建议吗?</p> <p>让我们看看 R1 提供的回答:</p> <p><img alt="新版R1打招呼" class="gallery-image" data-flex-basis="432px" data-flex-grow="180" height="591" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/NHFvrJzbe-lNmx_ILSnHLJ8jMPpb6FNzpW9vYD272fA/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2JldHRlcl9ncmVldGluZ3MucG5n" srcset="https://imgproxy.citrusreader.com/a1_tEqoGvLy_kkujn-_bI1sdwmC3fgz_3_RpjJLKXmc/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9iZXR0ZXJfZ3JlZXRpbmdzXzc4MjI4NDY2NzUwMjYyOTU4NzFfaHVfMTcxY2YyZjMwNzk4OTBiYS5wbmc 800w, https://imgproxy.citrusreader.com/NHFvrJzbe-lNmx_ILSnHLJ8jMPpb6FNzpW9vYD272fA/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2JldHRlcl9ncmVldGluZ3MucG5n 1066w" width="1066"></p> <p>非常标准的先表现同理心,安慰一下,再提供解决方案这样的回答思路。</p> <p>翻到结尾:</p> <p><img alt="同样充满同理心的结尾" class="gallery-image" data-flex-basis="749px" data-flex-grow="312" height="337" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/LE_XUFfi9iXFORpgfcqa1rF1b9vHeH5XEpSReW4wfOo/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2JldHRlcl9lbmRpbmdzLnBuZw" srcset="https://imgproxy.citrusreader.com/sGkWbGGoYYnwW4Fj1ytELrQzE3AUKKrKiSGc54Pmb1w/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9iZXR0ZXJfZW5kaW5nc18yNTY0NzgwMTgwOTIzNDgzNTYwX2h1XzUzNmYwOGM5ZWYzYTg1YmEucG5n 800w, https://imgproxy.citrusreader.com/LE_XUFfi9iXFORpgfcqa1rF1b9vHeH5XEpSReW4wfOo/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2JldHRlcl9lbmRpbmdzLnBuZw 1052w" width="1052"></p> <p>总的上来说,比起原来目的明确的回答,R1 这个版本的回答开始<strong>越来越像真正的对话了</strong>。</p> <p>当然,这个方面可能对实际产品的使用表现而言没有什么实质性的提升,只是说给提问者带来了更多的情绪价值。</p> <p>但无论如何,这的确在一定程度上提高了使用体验。</p> <hr> <h2 id="2-更多使用ta来代表提问者">2. 更多使用“ta”来代表提问者 </h2><p>如果你仔细看了上面一个问题在推理阶段的截图,你可能会发现 R1 开始大量使用更中性的称呼“ta”来代表提问者。</p> <p>为了更好看请这个现象,我们再提一个问题:假设我有个开发者朋友遇到了技术难题,你会如何帮助我的朋友?</p> <p>这里我专门用了“开发者朋友”这个称呼,就是为了不在提问中规定明确的性别。我们看看 R1 的推理:</p> <p><img alt="揭穿我有个朋友" class="gallery-image" data-flex-basis="655px" data-flex-grow="273" height="397" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/BTuH6jmdBsSMRqcbKvuNwkbXcKIwlUDB5Rk0dIAElmM/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2ZyaWVuZC5wbmc" srcset="https://imgproxy.citrusreader.com/uMR1D_dXQ-ZcQTYoAsR9eGFoqt40O4yjYvbJGkqDhO8/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9mcmllbmRfMTI1MDk0MzE1NzY1MDM2MTAzMzRfaHVfNjE0YTI4MDlkOWVhMjJhOC5wbmc 800w, https://imgproxy.citrusreader.com/BTuH6jmdBsSMRqcbKvuNwkbXcKIwlUDB5Rk0dIAElmM/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2ZyaWVuZC5wbmc 1084w" width="1084"></p> <p>好嘛,上来先揭穿“我有个朋友”大概率就是我。</p> <p>再看看正式回答部分:</p> <p><img alt="使用TA作为人称代词" class="gallery-image" data-flex-basis="528px" data-flex-grow="220" height="504" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/HEUgC2z87PyaUk14FCHKzR-qq-WLGMfQVt4_ueFRdsw/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L3VzaW5nX3RhX3Byb25vdW4ucG5n" srcset="https://imgproxy.citrusreader.com/6nk8iC5-SjEjQzPJmvCO_po2mhLHKmWTmwh1TVVWs8U/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay91c2luZ190YV9wcm9ub3VuXzQ0NDA0MDA0MzA1OTM4Njc0MjBfaHVfZjFmNWI1NWJiYzgwYzJhMS5wbmc 800w, https://imgproxy.citrusreader.com/HEUgC2z87PyaUk14FCHKzR-qq-WLGMfQVt4_ueFRdsw/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L3VzaW5nX3RhX3Byb25vdW4ucG5n 1110w" width="1110"></p> <p>我们会发现,这一轮迭代后的 R1 开始<strong>大量使用 TA</strong>(有些时候是小写的 ta)作为主要的人称代词,而不是原来经常使用的“用户”。</p> <p>同样的,这一点并不会使得模型的硬核表现有显著提高,但是可以看到开发团队在细节用户体验上的努力。</p> <hr> <h2 id="3-更复杂的流程图">3. 更复杂的流程图 </h2><p>这一点我觉得是对模型的用户体验有更直接提升的一个方面,那就是 DeepSeek 开始生成更复杂、逻辑更严密的流程图了。</p> <p>过去虽然也偶尔看到 DeepSeek 通过 mermaid 生成流程图,但总得上来说频率比较低,且流程图结构相对简单。</p> <p>换言之,以往 R1 生成的流程图给我的感觉主要就是一个添头,基本上就是起到回答的装饰性作用,大部分信息点还是在正式文字回答中。</p> <p>流程图本身<strong>很少提供额外的信息点</strong>。</p> <p>但是,这轮升级后 R1 模型开始可以生成一些相对复杂得多的流程图,而且这些流程图看完其实是有一些额外的收获的。</p> <p>再举个例子,我们直接让 R1 帮我们梳理一个复杂业务的流程:绘制一个“宠物寄养平台”的用户注册、下单、支付、服务确认的全流程,包含异常分支(如退款)。</p> <p>我们看看 DeepSeek 生成的流程图:</p> <p><img alt="第一张流程图" class="gallery-image" data-flex-basis="165px" data-flex-grow="69" height="4290" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/-Q0Uzsqt46V8wOFsaZ2I2o1cIQNkwR1npxGdwMnYqtE/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDEucG5n" srcset="https://imgproxy.citrusreader.com/nx72y0sjvkshmPhkzXkIDsMAJOCKul6lGnGVC2UrNvk/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAxXzc2MTg5NTg4MDU1ODc3MDY2Nl9odV9kZTA5N2NhOWVjNjk1MzZjLnBuZw 800w, https://imgproxy.citrusreader.com/pisfynXkwEeQIXyfd51YdDuk810xMfKroTpaygTEscY/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAxXzc2MTg5NTg4MDU1ODc3MDY2Nl9odV85ODQyOTg5YjVkOTg4ZmIyLnBuZw 1600w, https://imgproxy.citrusreader.com/viAFE4DRetYox7V6sePd1dAIGc9D9ALQZqJTQvqz23c/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAxXzc2MTg5NTg4MDU1ODc3MDY2Nl9odV85YWZhNTU1ZGEwNDRhMjQ4LnBuZw 2400w, https://imgproxy.citrusreader.com/-Q0Uzsqt46V8wOFsaZ2I2o1cIQNkwR1npxGdwMnYqtE/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDEucG5n 2964w" width="2964"></p> <p><img alt="第二张流程图" class="gallery-image" data-flex-basis="1011px" data-flex-grow="421" height="678" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/A_dphXgexCunqs7QtW_OWrYv6HmsiW2eWSiw8e7844M/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDIucG5n" srcset="https://imgproxy.citrusreader.com/eEpZZCks0dZa-WbnfmynEMC6IugB1MpCGhyQ3uX7GKE/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAyXzI5NzE0NDU0MTg4NTk0MTI2NjFfaHVfNmU0MmU4ZDk2YWY3NDA5NS5wbmc 800w, https://imgproxy.citrusreader.com/tK2ijaDz6-XWHSWv8I-kojDpXUDkr9Io8EqZrvClcOI/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAyXzI5NzE0NDU0MTg4NTk0MTI2NjFfaHVfODNkMmM5MzBhMzRkMzE4LnBuZw 1600w, https://imgproxy.citrusreader.com/E18E8uR0TEDEQM9kHu0bP4qPE-RHrhOCVLA987WCPSI/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzAyXzI5NzE0NDU0MTg4NTk0MTI2NjFfaHVfOTFmMzQ1YjVjZTk4MWJhMC5wbmc 2400w, https://imgproxy.citrusreader.com/A_dphXgexCunqs7QtW_OWrYv6HmsiW2eWSiw8e7844M/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDIucG5n 2858w" width="2858"></p> <p><img alt="第三张流程图" class="gallery-image" data-flex-basis="1322px" data-flex-grow="550" height="522" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/Rwwd8VuVTMhtfz9KXwlD044mlskHpNAO34dKYs_f-WY/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDQucG5n" srcset="https://imgproxy.citrusreader.com/roVxN-pIzm6RChKOH35mGeRlYBfEgLEcvk_4MZga9GQ/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA0Xzg1MDkyNjQwNTMxMDA5ODUwNDdfaHVfZjU3NjZhNjFlMzU4MWI1OC5wbmc 800w, https://imgproxy.citrusreader.com/9C5aRs71c3ZNth8gdHw5lOJgOuItKjs2DKDL46EEg6E/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA0Xzg1MDkyNjQwNTMxMDA5ODUwNDdfaHVfNmVkM2ViNzhiOGFjZmVkMy5wbmc 1600w, https://imgproxy.citrusreader.com/8y9QARoQTlNgJz5AlArvc1E2T093WS9g_wlWvEYCixk/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA0Xzg1MDkyNjQwNTMxMDA5ODUwNDdfaHVfZDc5NjQ3MmZlMjliZTZiMi5wbmc 2400w, https://imgproxy.citrusreader.com/Rwwd8VuVTMhtfz9KXwlD044mlskHpNAO34dKYs_f-WY/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDQucG5n 2876w" width="2876"></p> <p>R1 总共为我生成了 4 张图,可以看到每张图仔细思考一下逻辑都挺合理的。尤其是第一张图,<strong>逻辑清晰、配色合理</strong>,的确非常有帮助。</p> <p>或者,我们也可以让 DeepSeek 帮我们通过 mermaid 生成<strong>甘特图</strong>来描述项目计划,这项功能同样是原先就存在但是现在看来更强大了:</p> <p><img alt="新R1生成的甘特图" class="gallery-image" data-flex-basis="1262px" data-flex-grow="526" height="876" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://imgproxy.citrusreader.com/DHNrQcXnothkYp9oaqGK7YLl2gBpfKshdzN10dKVamI/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDUucG5n" srcset="https://imgproxy.citrusreader.com/2DiCt4KEg4IiRHcG7SjT1a43oWLrcODXvYXrDzfKvig/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA1Xzg2NTc3MjA0NTE4NjkyMTY4MTZfaHVfYjI5NmNlMTE1M2NjZTU1NS5wbmc 800w, https://imgproxy.citrusreader.com/1hyo2XU6bCs4fITvW5xUpcy-55GZ_UGedAvcUzYb1O8/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA1Xzg2NTc3MjA0NTE4NjkyMTY4MTZfaHVfODc3MTJkMDlmNDkyOTdkOS5wbmc 1600w, https://imgproxy.citrusreader.com/dmnRWuyavTwcyWTImJS-jttSyqC9mW9MNctLbr1V4YE/raw:1/aHR0cHM6Ly93d3cuY2hhbmdzdW4ud29yay9kZWVwc2Vla19tZXJtYWlkXzA1Xzg2NTc3MjA0NTE4NjkyMTY4MTZfaHVfYzI2NDEwMmRiNTk2NTQyNi5wbmc 2400w, https://imgproxy.citrusreader.com/DHNrQcXnothkYp9oaqGK7YLl2gBpfKshdzN10dKVamI/raw:1/aHR0cHM6Ly9pbWcuY2hhbmdzdW4ud29yay8yMDI1LzA1L2RlZXBzZWVrX21lcm1haWRfMDUucG5n 4608w" width="4608"></p> <p>注意哦,这些都是我直接从生成回答中直接下载的图片,<strong>未作任何修改</strong>。怎么样,是不是现在这个版本的生成效果还是挺不错的?</p> <hr> <p>总的来讲,我觉得这一轮更新的确在很多细节问题上做了不少优化。更进一步的代码和写作能力还需要在不断日常使用中进行体验,回头有机会我再写写更深入的使用感受。</p>

2025/5/29
阅读更多

推荐订阅

Cloudflare 官方博客中文版,涵盖安全、AI 和开发者相关内容

美团技术团队最近更新内容。