Logo
Section 1 / 2

Haskell 初探

我们首次通过使用 Haskell 解决一个简单问题来初步了解函数式编程。同时,我们还将它与使用命令式语言进行对比。

导入

Haskell 初探

我们首次通过使用 Haskell 解决一个简单问题来初步了解函数式编程。同时,我们还将它与使用命令式语言进行对比。

什么是 Haskell?

Haskell 是一种编译型、静态类型、函数式编程语言。它以美国逻辑学家 Haskell Brooks Curry 命名。

  • 编译型 意味着程序不是由解释器直接运行的。相反,GHC(即 Glorious Haskell Compiler)会将我们的程序转换为计算机可以直接执行的机器码。
  • 静态类型 指的是程序中的每个变量都有一个固定的类型(例如:实数、字符字符串)。类型在编写时就已经确定,可以通过手动声明类型或由系统自动推断得出。
  • 函数式 描述的是编程范式。正如其名,函数式程序的核心是函数,尤其是所谓的纯函数——它们没有副作用。纯函数易于组合,这使得我们可以用简单的部分构建复杂的程序。

函数式 范式是声明式编程的一种形式。它使 Haskell 与其他大多数遵循命令式编程范式的语言区分开来。命令式编程关注的是向计算机提供一系列指令,以解决某个问题。

第一个例子

为了说明两者的区别,我们来看一个简单的问题,并分别用 Python 和 Haskell 来解决。假设我们银行账户里有 1000 美元,想要进行投资。银行提供一项年利率为 5% 的投资计划。那么经过 n 年后,我们会拥有多少钱?

命令式解法

这是一个用命令式风格编写的 Python 程序:

1def compound_interest(years): 2 current_money = 1000 3 for year in range(years): 4 current_money = current_money * 1.05 5 print('After {years} years, you have {money:.2f} dollars.'.format(years=years, money=current_money)) 6 return current_money 7 8compound_interest(10)

这个程序体现了命令式编程的一些典型特征:

  • 程序是一系列按顺序执行的语句。
  • 语句可以有副作用,即影响函数范围之外的内容。在这个例子中,print 语句将计算结果输出到用户屏幕上。
  • 明确存在控制流,比如使用循环遍历年份,以及使用return语句表示函数结束。
  • 声明了一个变量(current_money)。该变量是可变的,即可以被修改。在循环过程中,它的值被多次更新,最终返回。

如果你熟悉命令式编程,这些特性对你来说都是很自然的。事实上,你可能很难想象这个问题还能用其他方式解决。现在,让我们看看如何在 Haskell 中解决这个问题——这是一种没有语句、没有循环、也没有可变变量的语言。

重新思考问题

在看解决方案之前,让我们先从这种命令式的思路中退一步。声明式编程鼓励我们以更抽象的方式定义问题,而不依赖于具体指令的执行顺序。通常,通过递归方程来表达问题会很有帮助。

  • 初始时刻(“0 年后”),我们的投资金额是 1000 美元。
  • 一年后,投资金额变为 1.05 * 1000 = 1050 美元。
  • 两年后,投资金额变为 1.05 * 1050 = 1102.5 美元。我们可以将其重写为 1.05 * (1.05 * 1000),其中 (1.05 * 1000) 是一年后的公式。
  • 因此,如果我们投资 n 年,就需要将 n-1 年的结果乘以 1.05。

由此得到递归公式:

compoundInterest(n)={1000if n=01.05×compoundInterest(n−1)otherwise\text{compoundInterest}(n) = \begin{cases} 1000 & \text{if } n = 0 \\ 1.05 \times \text{compoundInterest}(n-1) & \text{otherwise} \end{cases}

函数式解法

我们的 Haskell 程序是这一递归数学定义的直接翻译:

1compoundInterest :: Int -> Double 2compoundInterest 0 = 1000 3compoundInterest n = 1.05 * (compoundInterest (n - 1)) 4 5main = printf "After 10 years, you have %.2f dollars." (compoundInterest 10)

我们逐行分析一下。第一行:

compoundInterest :: Int -> Double

是名为 compoundInterest 的函数的类型注解。与 Python 不同,Haskell 是静态类型的,每个函数或变量都有明确的类型。这里表示 compoundInterest 是一个接受整数(年数)作为参数并返回浮点数(Double 类型)的函数。

接下来的两行是函数定义,由两个等式组成,实现了我们对问题的递归描述。

compoundInterest 0 = 1000

这是处理基础情况:投资 0 年的情形。这里的 0 表示函数的整数参数。在 Haskell 中,我们通过这种方式表明这条规则仅在参数为 0 时适用。

compoundInterest n = 1.05 * (compoundInterest (n - 1))

这是处理投资 n 年的情况。它递归调用 compoundInterest 计算 n-1 年的结果,然后将结果乘以 1.05。使用变量名 n 而非具体数值(如 0),表示这条规则适用于任意整数参数。记住,当 n 为 0 时,第一条规则已经覆盖了这种情况,因此不会触发这条规则。

最后一行是特殊的 main 函数,用于打印结果。


与命令式程序相比:

  • 存在类型和静态类型注解。
  • 没有语句。函数通过表达式分情况定义。
  • 没有循环。我们使用递归调用来重复应用利率。
  • 没有可变变量。我们通过递归表达式,从 n-1 年的结果推导出 n 年的结果。
  • 函数内部没有副作用(将结果打印到屏幕的操作发生在计算函数之外)。

至此,我们完成了对 Haskell 编程思维的初步探索。适应声明式的问题解决方式需要一些时间,但掌握这种思维方式带来了诸多好处。在课程后续内容中,我们将深入探讨本节略过的主题,例如递归、类型系统,以及 Haskell 如何处理副作用。

为什么你应该学习 Haskell