跳至内容

Haskell/构建词汇

来自维基教科书,开放的书籍,开放的世界

本章将作为一段插曲,提供一些关于学习和使用 Haskell 的建议。我们将讨论获取函数词汇的重要性,以及本书和其他资源如何提供帮助。但是,首先我们需要了解函数组合。

函数组合

[编辑 | 编辑源代码]

函数组合意味着将一个函数应用于一个值,然后将另一个函数应用于结果。考虑这两个函数

示例:简单函数

f x = x + 3
square x = x ^ 2

我们可以用两种不同的方式组合它们,这取决于我们首先应用哪个函数

Prelude> square (f 1)
16
Prelude> square (f 2)
25
Prelude> f (square 1)
4
Prelude> f (square 2)
7

内部函数周围的括号是必要的;否则,解释器会认为你试图获取 square ff square 的值;这两种情况都会导致类型错误。

两个函数的组合会产生一个独立的函数。如果我们经常应用 f 然后平方(反之亦然),我们应该为结果组合生成一个新的变量名

示例:组合函数

squareOfF x = square (f x)

fOfSquare x = f (square x)

还有一种巧妙的方法来编写组合函数。它使用 (.),函数组合运算符,就像在两个函数之间加一个句点一样简单

示例:使用 (.) 组合函数

squareOfF x = (square . f) x

fOfSquare x = (f . square) x

请注意,函数仍然是从右到左应用的,因此 g(f(x)) == (g . f) x(.) 模仿数学运算符 ,它以相同的方式工作: .

顺便说一句,我们的函数定义实际上是数学方程式,所以我们可以取

squareOfF x = (square . f) x

并从两边消去 x,留下

squareOfF = square . f

我们将在后面学习更多关于这种没有显示参数的函数的情况。现在,请理解我们可以简单地用我们定义的变量名替换任何组合函数的情况。

需要词汇

[编辑 | 编辑源代码]

Haskell 使编写组合函数和定义变量变得简单,因此我们最终得到相对简单、优雅和表达力强的代码。当然,为了使用函数组合,我们首先需要有函数可以组合。虽然我们自己编写的函数将始终可用,但每个 GHC 安装都附带大量库(即打包的代码),这些库提供了用于许多常见任务的函数。因此,有效的 Haskell 程序员需要对基本库有一定程度的了解。至少,你应该知道如何在需要时在库中找到有用的函数。

仅凭我们将在 递归 章中介绍的 Haskell 语法,原则上我们就有足够的知识来编写几乎任何我们想要的列表操作程序。但是,仅使用这些基础知识来编写完整的程序将非常低效,因为我们最终将重新编写标准库的大部分内容。因此,我们今后的大部分学习将涉及学习和理解 Haskell 社区已经构建的这些宝贵的工具。

Prelude 和库

[编辑 | 编辑源代码]

以下是一些关于 Haskell 库的基本事实

首先,Prelude 是默认加载到每个 Haskell 程序中的核心库。这个无所不在的库提供了一组有用的函数、类和类型。我们在这些入门章节中介绍了 Prelude 类型和函数。

GHC 提供了一组庞大的核心库,具有各种工具,但只有 Prelude 会自动加载。其他库是模块,可供你的程序导入。稍后,我们将解释模块的工作原理。现在,只需知道你的源文件需要在顶部附近添加几行来导入任何所需的模块。例如,要使用 Data.List 模块中的 permutations 函数,请在你的 .hs 文件顶部添加 import Data.List 行。以下是一个完整的源文件示例

示例:在源文件中导入模块

import Data.List

testPermutations = permutations "abc"

对于快速的 GHCi 测试,只需在命令行中输入 :m +Data.List 来加载该模块。

Prelude> :m +Data.List
Prelude Data.List> :t permutations
permutations :: [a] -> [[a]]

一个例子

[编辑 | 编辑源代码]

在继续之前,让我们看一下(我们承认有点戏剧性的)熟悉 Prelude 中的一些基本函数可以为我们带来的示例。[1] 假设我们需要一个函数,它接受一个由空格分隔的单词组成的字符串,并返回该字符串,其中单词的顺序反转,例如 "Mary had a little lamb" 变成 "lamb little a had Mary"。我们可以使用我们已经介绍过的基本知识以及即将介绍的递归章节中的一些见解来解决这个问题。下面是一个混乱、复杂的解决方案。不要盯着它看太久!

示例:这里有巨龙

monsterRevWords :: String -> String
monsterRevWords input = rejoinUnreversed (divideReversed input)
    where
    divideReversed s = go1 [] s
        where
        go1 divided [] = divided
        go1 [] (c:cs)
            | testSpace c = go1 [] cs
            | otherwise   = go1 [[]] (c:cs)
        go1 (w:ws) [c]
            | testSpace c = (w:ws)
            | otherwise   = ((c:w):ws)
        go1 (w:ws) (c:c':cs)
            | testSpace c =
                if testSpace c'
                    then go1 (w:ws) (c':cs)
                    else go1 ([c']:w:ws) cs
            | otherwise = go1 ((c:w):ws) (c':cs)
    testSpace c = c == ' '
    rejoinUnreversed [] = []
    rejoinUnreversed [w] = reverseList w
    rejoinUnreversed strings = go2 (' ' : reverseList newFirstWord) (otherWords)
        where
        (newFirstWord : otherWords) = reverseList strings
        go2 rejoined ([]:[]) = rejoined
        go2 rejoined ([]:(w':ws')) = go2 (rejoined) ((' ':w'):ws')
        go2 rejoined ((c:cs):ws) = go2 (c:rejoined) (cs:ws)
    reverseList [] = []
    reverseList w = go3 [] w
        where
        go3 rev [] = rev
        go3 rev (c:cs) = go3 (c:rev) cs

这个东西存在太多问题;所以让我们只考虑其中三个

  • 要查看 monsterRevWords 是否按预期执行,你可以相信我们的话,对各种可能的输入进行全面测试,或者尝试理解它并获得可怕的头痛(请不要这样做)。
  • 此外,如果我们编写一个如此丑陋的函数,并且以后不得不修复错误或稍微修改它,[2] 我们将面临一段可怕的时光。
  • 最后,我们至少有一个容易发现的潜在问题:如果你再仔细看看定义,在中间部分有一个 testSpace 辅助函数,它检查一个字符是否是空格。然而,该测试仅包括常见的空格字符(即 ' '),而没有其他空白字符(制表符、换行符等)。[3]

如果我们使用以下 Prelude 函数,我们可以做得比上面的垃圾代码好得多

  • words,它可靠地将字符串分解为以空格分隔的单词,返回一个字符串列表;
  • reverse,它反转列表(顺便说一下,这就是上面 reverseList 所做的);以及
  • unwords,它与 words 相反;

然后函数组合意味着我们的问题立即得到解决。

示例:revWords 以 Haskell 风格完成

revWords :: String -> String
revWords input = (unwords . reverse . words) input

这很短、简单、易读,而且(因为 Prelude 是可靠的)没有错误。[4] 所以,任何时候你正在编写的程序开始看起来像 monsterRevWords,请环顾四周,伸手去拿你的工具箱——库。

本书对库的使用

[编辑 | 编辑源代码]

在上述严厉警告之后,你可能期望我们继续深入研究标准库。然而,初学者学习路线的目的是以概念性、可读性和合理简洁的方式涵盖 Haskell 的功能。系统地研究库不会帮助我们,但我们将根据我们涵盖的每个概念,适当地介绍库中的函数。

  • 在初级 Haskell 部分,一些练习(主要是关于列表处理的练习)涉及编写 Prelude 函数的等效定义。对于你完成的每个练习,你的工具库中将添加一个新的函数。
  • 我们偶尔会介绍更多库函数;可能是在例子中,或者只是顺便提一下。每当我们这样做时,花点时间测试该函数并进行一些实验。记住将我们在类型基础中提到的关于类型的习惯性好奇心扩展到函数本身。
  • 虽然前几章关系紧密,但本书后面的部分更加独立。 实践中的 Haskell 包含关于分层库的章节,并且它们的大部分内容可以在完成初级 Haskell 后不久就理解。
  • 随着我们进入初学者学习路线的后面部分,我们将讨论的概念(特别是单子)将自然而然地引导我们探索核心库的重要部分。

其他资源

[edit | edit source]
  • 首先,所有模块都具有基本文档。你现在可能还没有准备好直接阅读它,但我们会做到。你可以在线阅读Prelude 规范以及与 GHC 捆绑在一起的库的文档,它具有不错的导航功能,只需单击一下即可访问源代码。
  • Hoogle 是一个通过文档搜索的绝佳方法,对初学者友好。它涵盖了大量的库。你可以按函数名(比如,length)或按类型(“从列表到 Int 的函数”,你将写成 [a]->Int)进行搜索。这种第二种类型的搜索可以帮助你找到非常具体的函数,并避免重复造轮子。
  • 除了 GHC 中包含的库之外,还有一个庞大的库生态系统,可以通过Hackage 获得,并且可以使用名为cabal 的工具进行安装。 Hackage 网站提供了其库的文档。我们不会在初学者学习路线中尝试使用核心库之外的库,但你当然应该在开始自己的项目后使用 Hackage。
  • 在适当的情况下,我们将提供指向其他有用学习资源的指针,尤其是在我们转向中级和高级主题时。

笔记

  1. 此处的示例受 HaskellWiki 中的简单 Unix 工具 演示的启发。
  2. 合著者注:“后面再说?我半个小时前写的,现在还没完全搞清楚……”
  3. 检查字符是否为空格的可靠方法是使用 isSpace 函数,该函数位于 Data.Char 模块中。
  4. 如果你想知道,Prelude 或 Data.List 中的许多其他函数可以帮助使 monsterRevWords 更简洁一些——举几个例子:(++)concatgroupByintersperse——但是没有一种使用方式可以与上面的单行代码相比。
华夏公益教科书