跳转到内容

用 48 小时编写你自己的 Scheme/第一步

来自 Wikibooks,开放世界中的开放书籍
用 48 小时编写你自己的 Scheme
第一步 解析 → 

首先,你需要安装 GHC。在 GNU/Linux 上,它通常是预安装的,或者可以通过包管理器(例如 aptyumpacman,具体取决于你的发行版)获得。它也可以从 http://www.haskell.org/ghc/ 下载。二进制包可能是最简单的,除非你真的知道自己在做什么。它应该像任何其他软件包一样下载和安装。本教程是在 GNU/Linux 上开发的,但只要你了解如何使用命令行,所有内容也应该可以在 Windows 上运行,或者在 Macintosh 上从终端中运行。

对于 UNIX(或 Windows Emacs)用户,有一个非常好的 Emacs 模式,包括语法高亮和自动缩进。Windows 用户可以使用记事本或任何其他文本编辑器:Haskell 语法相当适合使用记事本,但你必须注意缩进。 Eclipse 用户可能想尝试使用 eclipsefp 插件。

现在,是时候编写你的第一个 Haskell 程序了。这个程序将从命令行读取一个名称,然后打印一个问候语。创建一个以 ".hs" 结尾的文件,并输入以下文本。确保缩进正确,否则可能无法编译。

 module Main where
 import System.Environment
 
 main :: IO ()
 main = do
     args <- getArgs
     putStrLn ("Hello, " ++ args !! 0)

让我们来看一下这段代码。前两行指定我们将创建一个名为 Main 的模块,并导入 System 模块。每个 Haskell 程序都从一个名为 main 的操作开始,该操作位于名为 Main 的模块中。该模块可以导入其他模块,但必须存在才能使编译器生成可执行文件。Haskell 区分大小写:模块名称始终大写,定义始终小写。

main :: IO () 是一个类型声明:它说明 main 的类型是 IO (),这是一个带有单位类型 () 值的 IO 操作。单位类型只允许一个值,也表示为 (),因此不包含任何信息。Haskell 中的类型声明是可选的:编译器会自动推断它们,并且只会在它们与你指定的类型不同时才会报错。在本教程中,我会明确指定所有声明的类型,以提高清晰度。如果你在家跟着学习,你可能想省略它们,因为在构建程序时,需要更改的内容更少。

IO 类型是 Monad 类(一种类型类)的实例。Monad 是一个概念。说一个值是 Monad 类中的一种类型,就是说

  1. 此值附带(某种类型的)额外信息;
  2. 大多数函数不需要关心这些信息。

在本例中,

  1. "额外信息" 是要使用随附的值执行的 IO 操作;
  2. 而基本值(附带信息)是空值,表示为 ()

IO [String]IO () 都属于同一个 IO Monad 类型,但它们具有不同的基本类型。它们作用于(并传递)不同类型的值,[String]()

"附带(隐藏)信息的" 值称为 "Monadic 值"。

"Monadic 值" 通常被称为 "操作",因为考虑使用 IO Monad 的最简单方法是考虑影响外部世界的一系列操作。这些操作序列可能传递基本值,并且每个操作都能够作用于这些值。

Haskell 是一种函数式语言:你不会像其他语言那样提供一系列指令给计算机执行,而是提供一组定义,告诉它如何执行它可能需要的每个函数。这些定义使用各种操作和函数的组合。编译器会找出将所有内容组合在一起的执行路径。

要编写这些定义中的一个,你将其设置为一个等式。左侧定义一个名称,以及可选的 模式(将在稍后 解释),这些模式将绑定变量。右侧定义其他定义的一些组合,告诉计算机在遇到该名称时该做什么。这些等式就像代数中的普通等式一样:你始终可以在程序的文本中将右侧替换为左侧,它将计算出相同的值。这种被称为 "引用透明度" 的特性,使得对 Haskell 程序的推理比其他语言容易得多。

我们如何定义 main 操作?我们知道它必须是一个 IO () 操作,我们希望它读取命令行参数并打印一些输出,最终产生 () 或无价值。

创建 IO 操作有两种方法(直接创建或通过调用执行它们的函数)

  1. 使用 return 函数将普通值提升到 IO Monad 中。
  2. 组合两个现有的 IO 操作。

由于我们想做两件事,我们将采用第二种方法。内置操作 getArgs 读取命令行参数并将它们作为字符串列表传递。内置函数 putStrLn 接受一个字符串并创建一个将该字符串写入控制台的操作。

要组合这些操作,我们使用 do 块。do 块由一系列行组成,所有这些行都与 do 之后第一个非空格字符对齐。每行可以采用两种形式之一

  1. name <- action1
  2. action2

第一种形式将 action1 的结果绑定到 name,以便在接下来的操作中可用。例如,如果 action1 的类型是 IO [String](一个返回字符串列表的 IO 操作,如 getArgs),那么 name 将在所有后续的 actions 中绑定到通过使用 "绑定" 操作符 >>= 传递的字符串列表。第二种形式只执行 action2,通过 >> 操作符将其与下一行(如果有)进行排序。绑定操作符在每个 Monad 中具有不同的语义:在 IO Monad 的情况下,它会顺序执行操作,执行 actions 导致的任何外部副作用。由于这种组合的语义取决于所使用的特定 Monad,因此你不能在同一个 do 块中混合不同 Monad 类型的操作 - 只能使用 IO Monad(它们都在同一个 "管道" 中)。

当然,这些 actions 本身可能调用函数或复杂的表达式,并传递它们的结果(通过调用 return 函数或其他最终执行此操作的函数)。在本例中,我们首先获取参数列表中的第一个元素(索引为 0,args !! 0),将其附加到字符串 "Hello, " 的末尾("Hello, " ++),最后将其传递给 putStrLn,它会创建一个新的 IO 操作,参与 do 块排序。

这样创建的新操作(如上所述,是操作的组合序列)存储在类型为 IO () 的标识符 main 中。Haskell 系统注意到这个定义,并执行其中的操作。

字符串是 Haskell 中的字符列表,因此你可以在它们上使用任何列表函数和运算符。标准运算符及其优先级的完整表如下

运算符 优先级 结合性 描述
. 9 函数组合
!! 列表索引
^, ^^, ** 8 求幂(整数、分数和浮点数)
*, / 7 乘法、除法
+, - 6 加法、减法
: 5 cons(列表构建)
++ 列表连接
`elem`, `notElem` 4 列表成员资格
==, /=, <, <=, >=, > 相等、不等和其他关系运算符
&& 3 逻辑与
|| 2 逻辑或
>>, >>= 1 忽略返回值的 Monadic 绑定,将值传递给下一个函数的 Monadic 绑定
=<< 反向 Monadic 绑定(与上述相同,但参数颠倒)
$ 0 中缀函数应用(f $ x 等同于 f x,但右结合而不是左结合)

要编译并运行该程序,请尝试以下操作

debian:/home/jdtang/haskell_tutorial/code# ghc -o hello_you --make listing2.hs
debian:/home/jdtang/haskell_tutorial/code# ./hello_you Jonathan
Hello, Jonathan

-o 选项指定要创建的可执行文件的名称,然后你只需指定 Haskell 源文件的名称。

练习
  1. 更改程序,使其从命令行读取 两个 参数,并使用这两个参数打印一条消息
  2. 更改程序,使其对这两个参数执行简单的算术运算并打印结果。你可以使用 read 将字符串转换为数字,使用 show 将数字转换回字符串。尝试不同的运算。
  3. getLine 是一个从控制台读取一行并将其作为字符串返回的 IO 操作。更改程序,使其提示输入姓名,读取姓名,然后打印该姓名,而不是命令行值


用 48 小时编写你自己的 Scheme
第一步 解析 → 
华夏公益教科书