用 48 小时编写你自己的 Scheme/第一步
首先,你需要安装 GHC。在 GNU/Linux 上,它通常是预安装的,或者可以通过包管理器(例如 apt
或 yum
或 pacman
,具体取决于你的发行版)获得。它也可以从 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 类中的一种类型,就是说
- 此值附带(某种类型的)额外信息;
- 大多数函数不需要关心这些信息。
在本例中,
- "额外信息" 是要使用随附的值执行的 IO 操作;
- 而基本值(附带信息)是空值,表示为
()
。
IO [String]
和 IO ()
都属于同一个 IO
Monad 类型,但它们具有不同的基本类型。它们作用于(并传递)不同类型的值,[String]
和 ()
。
"附带(隐藏)信息的" 值称为 "Monadic 值"。
"Monadic 值" 通常被称为 "操作",因为考虑使用 IO Monad 的最简单方法是考虑影响外部世界的一系列操作。这些操作序列可能传递基本值,并且每个操作都能够作用于这些值。
Haskell 是一种函数式语言:你不会像其他语言那样提供一系列指令给计算机执行,而是提供一组定义,告诉它如何执行它可能需要的每个函数。这些定义使用各种操作和函数的组合。编译器会找出将所有内容组合在一起的执行路径。
要编写这些定义中的一个,你将其设置为一个等式。左侧定义一个名称,以及可选的 模式(将在稍后 解释),这些模式将绑定变量。右侧定义其他定义的一些组合,告诉计算机在遇到该名称时该做什么。这些等式就像代数中的普通等式一样:你始终可以在程序的文本中将右侧替换为左侧,它将计算出相同的值。这种被称为 "引用透明度" 的特性,使得对 Haskell 程序的推理比其他语言容易得多。
我们如何定义 main
操作?我们知道它必须是一个 IO ()
操作,我们希望它读取命令行参数并打印一些输出,最终产生 ()
或无价值。
创建 IO 操作有两种方法(直接创建或通过调用执行它们的函数)
- 使用
return
函数将普通值提升到 IO Monad 中。 - 组合两个现有的 IO 操作。
由于我们想做两件事,我们将采用第二种方法。内置操作 getArgs 读取命令行参数并将它们作为字符串列表传递。内置函数 putStrLn 接受一个字符串并创建一个将该字符串写入控制台的操作。
要组合这些操作,我们使用 do 块。do 块由一系列行组成,所有这些行都与 do 之后第一个非空格字符对齐。每行可以采用两种形式之一
name <- action1
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 源文件的名称。
练习 |
---|