原文。
https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/First_Steps
首先,你需要安装一个ghc。在Linux上,它常常被预先安装好了或者能够通过apt-get或者yum命令来轻松搞定。你也可以从官网直接下载它。不过除非你确信你想从源码去编译它,否则下载一个二进制包就可以了。像安装其他的软件包一样下载和安装它既可。这个教程是在Linux下完成的,但是只要你会使用相关的命令行操作,它在Windows或是Mac环境下也一样能工作。
对UNIX或者Windows Emacs用户来说,这里有一个很棒的Emacs mode,包括了语法高亮和自动缩进的功能。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()
,是一个返回Unit类型()
的IO操作。一个Unit类型仅会包含一个值,而()
,它表示什么也没有。类型声明在Haskell里是可选的:编译器能够自动的识别它们,当你的声明和编译器自动识别发生冲突的时候它则会报错。在这个教程中,为了更清晰的说明,所有的类型都是显式声明的。而你在家里跟着做的时候,你可能更愿意选择忽略,因为在编写这个程序的时候其实并不太需要去在意它们。
IO类型是Monad类型类的一个实例,Monad是一种抽象的概念,如果满足以下这两个条件,那我们就会说这样的值是Monad:
- 这个值包含了一些特定类型的附加信息;
- 大多数函数不需要去关心这些附加的信息。
在这个例子里,
- 这个附加的信息就是将被执行的IO操作;
- 而这个附加信息的值是不存在的,表示成
()
。
IO[String]
和IO()
都同样属于IO Monad类型,但它们有着不同的基本类型。它们作用于(或是传递)不同类型的值,[String]
和()
。
那些包含了附加信息的值则被称作“Monadic”。
Monadic值常被称作“操作” ,因为最容易的思考IO Monad用途的方法就是把它当做一系列可能会影响外界世界的动作。这一系列动作会传递一些基础的数值,然后在这个过程中每个动作都会对这些值进行影响。
Haskell是一个函数式的语言:与给出计算机一系列指令从而让它执行不同,你需要给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就会在接下来的一系列操作里和这个返回的字符串列表通过绑定操作符>>=
绑定在一起。第二种情况仅仅执行这个action2,并通过>>
操作符同下一行连结在一起。绑定操作符在处理不同Monad的情况下有不同的语义:在IO Monad中,它会连续执行所有的操作,然后对外部世界产生这些操作带来的副作用。由于这个绑定符号的语义依赖你具体使用的Monad类型,所以你并不能在同一个do代码块里把不同类型的Monad类型的操作糅杂在一起---在这里只有IO Monad是可用的(在同一个管道中)。
当然,这些操作可能自己会调用其他函数或是复杂的表达式,然后继续传递它们的计算结果(通过调用return或是其他最终调用了return的函数)。
在这个例子里,我们首先取出参数列表中的第一个元素(args !! 0)
,然后把它拼接到字符串"Hello,"的后面("Hello," ++
),最后把结果传给putStrLn。
就这样,一个包含了之前所说的读取和打印操作的新的操作就这样创建完毕并存到了main这个返回值为IO()
的标识符中。这样Haskell系统就能够识别并运行它了。
Haskell中,字符串即是字符的列表形式,所以你可以对它使用任何的
列表函数或是操作符。以下是一个完整的标准操作符列表和它们对应的优先级:
接下来编译和运行这个程序:
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源代码的路径。