函数式心法 (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">-></span> <span class="kt">Int</span> <span class="ow">-></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) -> Int</code>。如果用非柯里化形式写这个函数,应当写为:<code>add' (x, y) = x + y</code>。这是在其他语言中常见的多参数函数写法。</p> <p>而在我们的例子中,这个函数实际的函数签名是:<code>Int -> Int -> Int</code>。这其实是经过柯里化处理后的标准函数类型。由于 Haskell 的函数签名是<strong>右结合</strong>的,柯里化后的函数签名,完整写法是是<code>Int -> (Int -> 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">-></span> <span class="kt">Int</span> <span class="ow">-></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">-></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">-></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>