From a711d160168837a0bdb1e5c5bb42f9e1718b2942 Mon Sep 17 00:00:00 2001
From: trhao <1927978923@qq.com>
Date: Thu, 4 Jul 2024 12:52:09 +0800
Subject: [PATCH] =?UTF-8?q?fix=20zh-cn/wording:=20=E5=91=BD=E4=BB=A4?=
=?UTF-8?q?=E8=A1=8C=E5=BC=95=E6=95=B0=20->=E5=91=BD=E4=BB=A4=E8=A1=8C?=
=?UTF-8?q?=E5=8F=82=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
zh-cn/01-introduction.txt | 2 +-
zh-cn/09-input-and-output.txt | 24 ++---
zh-cn/ch01/introduction.md | 2 +-
zh-cn/ch09/input-and-output.md | 24 ++---
zh-cn/chapters.html | 88 +++++++++----------
zh-cn/input-and-output.html | 156 ++++++++++++++++-----------------
zh-cn/introduction.html | 68 +++++++-------
7 files changed, 182 insertions(+), 182 deletions(-)
diff --git a/zh-cn/01-introduction.txt b/zh-cn/01-introduction.txt
index 3d310aa..248a396 100644
--- a/zh-cn/01-introduction.txt
+++ b/zh-cn/01-introduction.txt
@@ -19,7 +19,7 @@
[$../img/fx.png]
-Haskell 是一门纯函数式编程语言 (purely functional programming language)。在命令式语言中执行操作需要给电脑安排一组命令,随着命令的执行,状态就会随之发生改变。例如你指派变量 a 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 (control flow),你就可以重复执行操作。然而在纯函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”,如“阶乘是指从1到某个数的乘积”,"一个串列中数字的和"是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 (variable) 一旦被指定,就不可以更改了,你已经说了 a 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。x=5 代表 x 就是 5,不是说 x 在 5 这个状态。) 所以说,在纯函数式编程语言中的函数能做的唯一事情就是利用引数计算结果,不会产生所谓的"副作用 (side effect)" (译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“引用透明 (Referential Transparency)” (译注:这就跟数学上函数的使用一样)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
+Haskell 是一门纯函数式编程语言 (purely functional programming language)。在命令式语言中执行操作需要给电脑安排一组命令,随着命令的执行,状态就会随之发生改变。例如你指派变量 a 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 (control flow),你就可以重复执行操作。然而在纯函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”,如“阶乘是指从1到某个数的乘积”,"一个串列中数字的和"是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 (variable) 一旦被指定,就不可以更改了,你已经说了 a 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。x=5 代表 x 就是 5,不是说 x 在 5 这个状态。) 所以说,在纯函数式编程语言中的函数能做的唯一事情就是利用参数计算结果,不会产生所谓的"副作用 (side effect)" (译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“引用透明 (Referential Transparency)” (译注:这就跟数学上函数的使用一样)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
[^../img/lazy.png]
diff --git a/zh-cn/09-input-and-output.txt b/zh-cn/09-input-and-output.txt
index 4022675..150c905 100644
--- a/zh-cn/09-input-and-output.txt
+++ b/zh-cn/09-input-and-output.txt
@@ -831,19 +831,19 @@ Which one do you want to delete?
Take salad out of the oven
}}
-==命令行引数
+==命令行参数
[$../img/arguments.png]
-如果你想要写一个在终端里运行的程序,处理命令行引数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行引数。
+如果你想要写一个在终端里运行的程序,处理命令行参数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行参数。
在之前的章节中,我们写了一个能将 to-do item 加进或移除 to-do list 的一个程序。但我们的写法有两个问题。第一个是我们把放 to-do list 的文件名称给写死了。我们擅自决定用户不会有很多个 to-do lists,就把文件命名为 todo.txt。
一种解决的方法是每次都询问用户他们想将他们的 to-do list 放进哪个文件。我们在用户要删除的时候也采用这种方式。这是一种可以运作的方式,但不太能被接受,因为他需要用户运行程序,等待程序询问才能回答。这被称为交互式的程序,但讨厌的地方在当你想要自动化执行程序的时候,好比说写成 script,这会让你的 script 写起来比较困难。
-这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行引数告诉程序他们想要什么。
+这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行参数告诉程序他们想要什么。
-在 System.Environment 模块当中有两个很酷的 I/O actions,一个是 getArgs,他的 type 是 getArgs :: IO [String],他是一个拿取命令行引数的 I/O action,并把结果放在包含的一个串列中。getProgName 的型态是 getProgName :: IO String,他则是一个 I/O action 包含了程序的名称。
+在 System.Environment 模块当中有两个很酷的 I/O actions,一个是 getArgs,他的 type 是 getArgs :: IO [String],他是一个拿取命令行参数的 I/O action,并把结果放在包含的一个串列中。getProgName 的型态是 getProgName :: IO String,他则是一个 I/O action 包含了程序的名称。
我们来看一个展现他们功能的程序。
{{
@@ -859,7 +859,7 @@ import Data.List
putStrLn progName
}}
-我们将 getArgs 跟 progName 分别绑定到 args 跟 progName。我们印出 The arguments are: 以及在 args 中的每个引数。最后,我们印出程序的名字。我们把程序编译成 arg-test。
+我们将 getArgs 跟 progName 分别绑定到 args 跟 progName。我们印出 The arguments are: 以及在 args 中的每个参数。最后,我们印出程序的名字。我们把程序编译成 arg-test。
{{
$ ./arg-test first second w00t "multi word arg"
@@ -872,7 +872,7 @@ The program name is:
arg-test
}}
-知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行引数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
+知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行参数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
我们叫这程序 todo,他会作三件事:
@@ -885,7 +885,7 @@ arg-test
我们的程序要像这样运作:假如我们要加入 Find the magic sword of power,则我们会打 todo add todo.txt "Find the magic sword of power"。要查看事项我们则会打 todo view todo.txt,如果要移除事项二则会打 todo remove todo.txt 2
-我们先作一个分发的 association list。他会把命令行引数当作 key,而对应的处理函数当作 value。这些函数的型态都是 [String] -> IO ()。他们会接受命令行引数的串列并回传对应的查看,加入以及删除的 I/O action。
+我们先作一个分发的 association list。他会把命令行参数当作 key,而对应的处理函数当作 value。这些函数的型态都是 [String] -> IO ()。他们会接受命令行参数的串列并回传对应的查看,加入以及删除的 I/O action。
{{
import System.Environment
@@ -909,11 +909,11 @@ main = do
action args
}}
-首先,我们取出引数并把他们绑定到 (command:args)。如果你还记得 pattern matching,这么做会把第一个引数绑定到 command,把其他的绑定到 args。如果我们像这样执行程序 todo add todo.txt "Spank the monkey",command 会变成 "add",而 args 会变成 ["todo.txt", "Spank the monkey"]。
+首先,我们取出参数并把他们绑定到 (command:args)。如果你还记得 pattern matching,这么做会把第一个参数绑定到 command,把其他的绑定到 args。如果我们像这样执行程序 todo add todo.txt "Spank the monkey",command 会变成 "add",而 args 会变成 ["todo.txt", "Spank the monkey"]。
在下一行,我们在一个分派的串列中寻到我们的指令是哪个。由于 "add" 指向 add,我们的结果便是 Just add。我们再度使用了 pattern matching 来把我们的函数从 Maybe 中取出。但如果我们想要的指令不在分派的串列中呢?那样 lookup 就会回传 Nothing,但我们这边并不特别处理失败的情况,所以 pattern matching 会失败然后我们的程序就会当掉。
-最后,我们用剩下的引数调用 action 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 main 的 do block 中,他最后会被执行。如果我们的 action 函数是 add,他就会被喂 args 然后回传一个加入 Spank the monkey 到 todo.txt 中的 I/O action。
+最后,我们用剩下的参数调用 action 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 main 的 do block 中,他最后会被执行。如果我们的 action 函数是 add,他就会被喂 args 然后回传一个加入 Spank the monkey 到 todo.txt 中的 I/O action。
我们剩下要做的就是实作 add,view 跟 remove,我们从 add 开始:
@@ -1004,7 +1004,7 @@ remove [fileName, numberString] = do
[^../img/salad.png]
-总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行引数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行引数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
+总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行参数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行参数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
在其他编程语言,我们可能会用一个大的 switch case 来实作,但使用高阶函数让我们可以要 dispatch list 给我们要的函数,并要那些函数给我们适当的 I/O action。
@@ -1403,7 +1403,7 @@ copyFile source dest = do
B.writeFile dest contents
}}
-我们写了自己的函数,他接受两个 FilePath(记住 FilePath 不过是 String 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 main 函数中,我们做的只是拿到命令行引数然后调用那个函数来拿到一个 I/O action。
+我们写了自己的函数,他接受两个 FilePath(记住 FilePath 不过是 String 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 main 函数中,我们做的只是拿到命令行参数然后调用那个函数来拿到一个 I/O action。
{{
$ runhaskell bytestringcopy.hs something.txt ../../something.txt
@@ -1438,7 +1438,7 @@ ghci> head []
先前我们谈过为什么在 I/O 部份的程序要越少越好。程序的逻辑部份尽量都放在 pure 的部份,因为 pure 的特性就是他们的结果只会根据函数的参数不同而改变。当思考 pure function 的时候,你只需要考虑他回传什么,因为除此之外他不会有任何副作用。这会让事情简单许多。尽管 I/O 的部份是难以避免的(像是打开文件之类),但最好是把 I/O 部份降到最低。Pure functions 缺省是 lazy,那代表我们不知道他什么时候会被 evaluate,不过我们也不该知道。然而,一旦 pure functions 需要丢出 Exception,他们何时被 evaluate 就很重要了。那是因为我们只有在 I/O 的部份才能接到 Exception。这很糟糕,因为我们说过希望 I/O 的部份越少越好。但如果我们不接 Exception,我们的程序就会当掉。这问题有解决办法吗?答案是不要在 pure code 里面使用 Exception。利用 Haskell 的型态系统,尽量使用 Either 或 Maybe 之类的型态来表示可能失败的计算。
-这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 main 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行引数所指定文件名称,并计算里面有多少行的程序。
+这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 main 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行参数所指定文件名称,并计算里面有多少行的程序。
{{
import System.Environment
diff --git a/zh-cn/ch01/introduction.md b/zh-cn/ch01/introduction.md
index 42ffbb9..7aabed3 100644
--- a/zh-cn/ch01/introduction.md
+++ b/zh-cn/ch01/introduction.md
@@ -18,7 +18,7 @@

-Haskell 与其他语言不同,是一门_纯粹函数式编程语言 \(purely functional programming language\)_。在一般常见的命令式语言中,要执行操作的话是给电脑一组命令,而状态会随着命令的执行而改变。例如你指派变量 `a` 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 \(control flow\),你就可以重复执行操作。然而在纯粹函数式编程语言中,你不是像命令式语言那样命令电脑「要做什么」,而是通过用函数来描述出问题「是什么」,如「_阶乘是指从1到某个数的乘积」,「一个串列中数字的和」是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 \(variable\) 一旦被指定,就不可以更改了,你已经说了 `a` 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。`x=5` 代表 `x` 就是 5,不是说 `x` 在 5 这个状态。\) 所以说,在纯粹函数式编程语言中的函数能做的唯一事情就是利用引数计算结果,不会产生所谓的"副作用 \(side effect\)" \(译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect\)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“_引用透明 \(Referential Transparency\)\_” \(译注:这就跟数学上函数的使用一样\)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
+Haskell 与其他语言不同,是一门_纯粹函数式编程语言 \(purely functional programming language\)_。在一般常见的命令式语言中,要执行操作的话是给电脑一组命令,而状态会随着命令的执行而改变。例如你指派变量 `a` 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 \(control flow\),你就可以重复执行操作。然而在纯粹函数式编程语言中,你不是像命令式语言那样命令电脑「要做什么」,而是通过用函数来描述出问题「是什么」,如「_阶乘是指从1到某个数的乘积」,「一个串列中数字的和」是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 \(variable\) 一旦被指定,就不可以更改了,你已经说了 `a` 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。`x=5` 代表 `x` 就是 5,不是说 `x` 在 5 这个状态。\) 所以说,在纯粹函数式编程语言中的函数能做的唯一事情就是利用参数计算结果,不会产生所谓的"副作用 \(side effect\)" \(译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect\)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“_引用透明 \(Referential Transparency\)\_” \(译注:这就跟数学上函数的使用一样\)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。

diff --git a/zh-cn/ch09/input-and-output.md b/zh-cn/ch09/input-and-output.md
index 25853b0..9d455ca 100644
--- a/zh-cn/ch09/input-and-output.md
+++ b/zh-cn/ch09/input-and-output.md
@@ -868,19 +868,19 @@ $ cat todo.txt
Take salad out of the oven
```
-## 命令行引数
+## 命令行参数

-如果你想要写一个在终端里运行的程序,处理命令行引数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行引数。
+如果你想要写一个在终端里运行的程序,处理命令行参数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行参数。
在之前的章节中,我们写了一个能将 to-do item 加进或移除 to-do list 的一个程序。但我们的写法有两个问题。第一个是我们把放 to-do list 的文件名称给写死了。我们擅自决定用户不会有很多个 to-do lists,就把文件命名为 todo.txt。
一种解决的方法是每次都询问用户他们想将他们的 to-do list 放进哪个文件。我们在用户要删除的时候也采用这种方式。这是一种可以运作的方式,但不太能被接受,因为他需要用户运行程序,等待程序询问才能回答。这被称为交互式的程序,但讨厌的地方在当你想要自动化执行程序的时候,好比说写成 script,这会让你的 script 写起来比较困难。
-这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行引数告诉程序他们想要什么。
+这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行参数告诉程序他们想要什么。
-在 `System.Environment` 模块当中有两个很酷的 I/O actions,一个是 **getArgs**,他的 type 是 `getArgs :: IO [String]`,他是一个拿取命令行引数的 I/O action,并把结果放在包含的一个串列中。**getProgName** 的型态是 `getProgName :: IO String`,他则是一个 I/O action 包含了程序的名称。
+在 `System.Environment` 模块当中有两个很酷的 I/O actions,一个是 **getArgs**,他的 type 是 `getArgs :: IO [String]`,他是一个拿取命令行参数的 I/O action,并把结果放在包含的一个串列中。**getProgName** 的型态是 `getProgName :: IO String`,他则是一个 I/O action 包含了程序的名称。
我们来看一个展现他们功能的程序。
@@ -897,7 +897,7 @@ main = do
putStrLn progName
```
-我们将 `getArgs` 跟 `progName` 分别绑定到 `args` 跟 `progName`。我们打印出 `The arguments are:` 以及在 `args` 中的每个引数。最后,我们打印出程序的名字。我们把程序编译成 `arg-test`。
+我们将 `getArgs` 跟 `progName` 分别绑定到 `args` 跟 `progName`。我们打印出 `The arguments are:` 以及在 `args` 中的每个参数。最后,我们打印出程序的名字。我们把程序编译成 `arg-test`。
```haskell
$ ./arg-test first second w00t "multi word arg"
@@ -910,7 +910,7 @@ The program name is:
arg-test
```
-知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行引数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
+知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行参数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
我们叫这程序 todo,他会作三件事:
@@ -924,7 +924,7 @@ arg-test
我们的程序要像这样运作:假如我们要加入 `Find the magic sword of power`,则我们会打 `todo add todo.txt "Find the magic sword of power"`。要查看事项我们则会打 `todo view todo.txt`,如果要移除事项二则会打 `todo remove todo.txt 2`
-我们先作一个分发的 association list。他会把命令行引数当作 key,而对应的处理函数当作 value。这些函数的型态都是 `[String] -> IO ()`。他们会接受命令行引数的串列并回传对应的查看,加入以及删除的 I/O action。
+我们先作一个分发的 association list。他会把命令行参数当作 key,而对应的处理函数当作 value。这些函数的型态都是 `[String] -> IO ()`。他们会接受命令行参数的串列并回传对应的查看,加入以及删除的 I/O action。
```haskell
import System.Environment
@@ -948,11 +948,11 @@ main = do
action args
```
-首先,我们取出引数并把他们绑定到 `(command:args)`。如果你还记得 pattern matching,这么做会把第一个引数绑定到 `command`,把其他的绑定到 `args`。如果我们像这样执行程序 `todo add todo.txt "Spank the monkey"`,`command` 会变成 `"add"`,而 `args` 会变成 `["todo.txt", "Spank the monkey"]`。
+首先,我们取出参数并把他们绑定到 `(command:args)`。如果你还记得 pattern matching,这么做会把第一个参数绑定到 `command`,把其他的绑定到 `args`。如果我们像这样执行程序 `todo add todo.txt "Spank the monkey"`,`command` 会变成 `"add"`,而 `args` 会变成 `["todo.txt", "Spank the monkey"]`。
在下一行,我们在一个分派的串列中寻到我们的指令是哪个。由于 `"add"` 指向 `add`,我们的结果便是 `Just add`。我们再度使用了 pattern matching 来把我们的函数从 `Maybe` 中取出。但如果我们想要的指令不在分派的串列中呢?那样 lookup 就会回传 `Nothing`,但我们这边并不特别处理失败的情况,所以 pattern matching 会失败然后我们的程序就会当掉。
-最后,我们用剩下的引数调用 `action` 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 `main` 的 do block 中,他最后会被执行。如果我们的 `action` 函数是 `add`,他就会被喂 `args` 然后回传一个加入 `Spank the monkey` 到 todo.txt 中的 I/O action。
+最后,我们用剩下的参数调用 `action` 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 `main` 的 do block 中,他最后会被执行。如果我们的 `action` 函数是 `add`,他就会被喂 `args` 然后回传一个加入 `Spank the monkey` 到 todo.txt 中的 I/O action。
我们剩下要做的就是实作 `add`,`view` 跟 `remove`,我们从 `add` 开始:
@@ -1044,7 +1044,7 @@ remove [fileName, numberString] = do

-总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行引数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行引数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
+总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行参数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行参数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
在其他编程语言,我们可能会用一个大的 switch case 来实作,但使用高端函数让我们可以要 dispatch list 给我们要的函数,并要那些函数给我们适当的 I/O action。
@@ -1444,7 +1444,7 @@ copyFile source dest = do
B.writeFile dest contents
```
-我们写了自己的函数,他接受两个 `FilePath`(记住 `FilePath` 不过是 `String` 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 `main` 函数中,我们做的只是拿到命令行引数然后调用那个函数来拿到一个 I/O action。
+我们写了自己的函数,他接受两个 `FilePath`(记住 `FilePath` 不过是 `String` 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 `main` 函数中,我们做的只是拿到命令行参数然后调用那个函数来拿到一个 I/O action。
```haskell
$ runhaskell bytestringcopy.hs something.txt ../../something.txt
@@ -1479,7 +1479,7 @@ pure code 能丢出 Exception,但 Exception 只能在 I/O section 中被接到
先前我们谈过为什么在 I/O 部份的程序要越少越好。程序的逻辑部份尽量都放在 pure 的部份,因为 pure 的特性就是他们的结果只会根据函数的参数不同而改变。当思考 pure function 的时候,你只需要考虑他回传什么,因为除此之外他不会有任何副作用。这会让事情简单许多。尽管 I/O 的部份是难以避免的(像是打开文件之类),但最好是把 I/O 部份降到最低。Pure functions 缺省是 lazy,那代表我们不知道他什么时候会被 evaluate,不过我们也不该知道。然而,一旦 pure functions 需要丢出 Exception,他们何时被 evaluate 就很重要了。那是因为我们只有在 I/O 的部份才能接到 Exception。这很糟糕,因为我们说过希望 I/O 的部份越少越好。但如果我们不接 Exception,我们的程序就会当掉。这问题有解决办法吗?答案是不要在 pure code 里面使用 Exception。利用 Haskell 的型态系统,尽量使用 `Either` 或 `Maybe` 之类的型态来表示可能失败的计算。
-这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 `main` 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行引数所指定文件名称,并计算里面有多少行的程序。
+这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 `main` 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行参数所指定文件名称,并计算里面有多少行的程序。
```haskell
import System.Environment
diff --git a/zh-cn/chapters.html b/zh-cn/chapters.html
index 4809763..d774be7 100644
--- a/zh-cn/chapters.html
+++ b/zh-cn/chapters.html
@@ -1,36 +1,36 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Haskell趣學指南 (Learn You a Haskell for Great Good! 繁體中文版)
-
-
-
-
-
-
-
Haskell趣學指南
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Haskell趣學指南 (Learn You a Haskell for Great Good! 繁體中文版)
+
+
+
+
+
+
+
Haskell趣學指南
- 简介
- 输入与输出
-
-
本作品受創用 CC BY-NC-SA 3.0 保護
-
-
-
-
-
-
-
+
+
本作品受創用 CC BY-NC-SA 3.0 保護
+
+
+
+
+
+
+
diff --git a/zh-cn/input-and-output.html b/zh-cn/input-and-output.html
index 59997b0..a3b3300 100644
--- a/zh-cn/input-and-output.html
+++ b/zh-cn/input-and-output.html
@@ -1,57 +1,57 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 输入与输出
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
输入与输出
+
+
+
+
+
+
输入与输出

@@ -640,18 +640,18 @@
0
$ cat todo.txt
-Take salad out of the oven
命令行引数
+Take salad out of the oven
命令行参数

-
如果你想要写一个在终端里运行的程序,处理命令行引数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行引数。
+
如果你想要写一个在终端里运行的程序,处理命令行参数是不可或缺的。幸运的是,利用 Haskell 的 Standard Libary 能让我们有效地处理命令行参数。
在之前的章节中,我们写了一个能将 to-do item 加进或移除 to-do list 的一个程序。但我们的写法有两个问题。第一个是我们把放 to-do list 的文件名称给写死了。我们擅自决定用户不会有很多个 to-do lists,就把文件命名为 todo.txt。
一种解决的方法是每次都询问用户他们想将他们的 to-do list 放进哪个文件。我们在用户要删除的时候也采用这种方式。这是一种可以运作的方式,但不太能被接受,因为他需要用户运行程序,等待程序询问才能回答。这被称为交互式的程序,但讨厌的地方在当你想要自动化执行程序的时候,好比说写成 script,这会让你的 script 写起来比较困难。
-
这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行引数告诉程序他们想要什么。
+
这也是为什么有时候让用户在执行的时候就告诉程序他们要什么会比较好,而不是让程序去问用户要什么。比较好的方式是让用户透过命令行参数告诉程序他们想要什么。
-
在 System.Environment 模块当中有两个很酷的 I/O actions,一个是 getArgs,他的 type 是 getArgs :: IO [String],他是一个拿取命令行引数的 I/O action,并把结果放在包含的一个串列中。getProgName 的型态是 getProgName :: IO String,他则是一个 I/O action 包含了程序的名称。
+
在 System.Environment 模块当中有两个很酷的 I/O actions,一个是 getArgs,他的 type 是 getArgs :: IO [String],他是一个拿取命令行参数的 I/O action,并把结果放在包含的一个串列中。getProgName 的型态是 getProgName :: IO String,他则是一个 I/O action 包含了程序的名称。
我们来看一个展现他们功能的程序。
import System.Environment
@@ -664,7 +664,7 @@
mapM putStrLn args
putStrLn "The program name is:"
putStrLn progName
-
我们将 getArgs 跟 progName 分别绑定到 args 跟 progName。我们打印出 The arguments are: 以及在 args 中的每个引数。最后,我们打印出程序的名字。我们把程序编译成 arg-test。
+
我们将 getArgs 跟 progName 分别绑定到 args 跟 progName。我们打印出 The arguments are: 以及在 args 中的每个参数。最后,我们打印出程序的名字。我们把程序编译成 arg-test。
$ ./arg-test first second w00t "multi word arg"
The arguments are:
first
@@ -673,7 +673,7 @@
multi word arg
The program name is:
arg-test
-
知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行引数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
+
知道了这些函数现在你能写几个很酷的命令行程序。在之前的章节,我们写了一个程序来加入待作事项,也写了另一个程序删除事项。现在我们要把两个程序合起来,他会根据命令行参数来决定该做的事情。我们也会让程序可以处理不同的文件,而不是只有 todo.txt
我们叫这程序 todo,他会作三件事:
-
@@ -692,7 +692,7 @@
我们的程序要像这样运作:假如我们要加入 Find the magic sword of power,则我们会打 todo add todo.txt "Find the magic sword of power"。要查看事项我们则会打 todo view todo.txt,如果要移除事项二则会打 todo remove todo.txt 2
-我们先作一个分发的 association list。他会把命令行引数当作 key,而对应的处理函数当作 value。这些函数的型态都是 [String] -> IO ()。他们会接受命令行引数的串列并回传对应的查看,加入以及删除的 I/O action。
+我们先作一个分发的 association list。他会把命令行参数当作 key,而对应的处理函数当作 value。这些函数的型态都是 [String] -> IO ()。他们会接受命令行参数的串列并回传对应的查看,加入以及删除的 I/O action。
import System.Environment
import System.Directory
import System.IO
@@ -708,11 +708,11 @@
(command:args) <- getArgs
let (Just action) = lookup command dispatch
action args
-首先,我们取出引数并把他们绑定到 (command:args)。如果你还记得 pattern matching,这么做会把第一个引数绑定到 command,把其他的绑定到 args。如果我们像这样执行程序 todo add todo.txt "Spank the monkey",command 会变成 "add",而 args 会变成 ["todo.txt", "Spank the monkey"]。
+首先,我们取出参数并把他们绑定到 (command:args)。如果你还记得 pattern matching,这么做会把第一个参数绑定到 command,把其他的绑定到 args。如果我们像这样执行程序 todo add todo.txt "Spank the monkey",command 会变成 "add",而 args 会变成 ["todo.txt", "Spank the monkey"]。
在下一行,我们在一个分派的串列中寻到我们的指令是哪个。由于 "add" 指向 add,我们的结果便是 Just add。我们再度使用了 pattern matching 来把我们的函数从 Maybe 中取出。但如果我们想要的指令不在分派的串列中呢?那样 lookup 就会回传 Nothing,但我们这边并不特别处理失败的情况,所以 pattern matching 会失败然后我们的程序就会当掉。
-最后,我们用剩下的引数调用 action 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 main 的 do block 中,他最后会被执行。如果我们的 action 函数是 add,他就会被喂 args 然后回传一个加入 Spank the monkey 到 todo.txt 中的 I/O action。
+最后,我们用剩下的参数调用 action 这个函数。他会还传一个加入 item,显示所有 items 或者删除 item 的 I/O action。由于这个 I/O action 是在 main 的 do block 中,他最后会被执行。如果我们的 action 函数是 add,他就会被喂 args 然后回传一个加入 Spank the monkey 到 todo.txt 中的 I/O action。
我们剩下要做的就是实作 add,view 跟 remove,我们从 add 开始:
add :: [String] -> IO ()
@@ -786,7 +786,7 @@
hClose tempHandle
removeFile fileName
renameFile tempName fileName
-总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行引数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行引数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
+总结我们的程序:我们做了一个 dispatch association,将指令对应到一些会接受命令行参数并回传 I/O action 的函数。我们知道用户下了什么命令,并根据那个命令从 dispatch list 取出对影的函数。我们用剩下的命令行参数调用哪些函数而得到一些作相对应事情的 I/O action。然后便执行那些 I/O action。
在其他编程语言,我们可能会用一个大的 switch case 来实作,但使用高阶函数让我们可以要 dispatch list 给我们要的函数,并要那些函数给我们适当的 I/O action。
@@ -1067,7 +1067,7 @@
copyFile source dest = do
contents <- B.readFile source
B.writeFile dest contents
-我们写了自己的函数,他接受两个 FilePath(记住 FilePath 不过是 String 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 main 函数中,我们做的只是拿到命令行引数然后调用那个函数来拿到一个 I/O action。
+我们写了自己的函数,他接受两个 FilePath(记住 FilePath 不过是 String 的同义词。)并回传一个 I/O action,他会用 bytestring 拷贝第一个文件至另一个。在 main 函数中,我们做的只是拿到命令行参数然后调用那个函数来拿到一个 I/O action。
$ runhaskell bytestringcopy.hs something.txt ../../something.txt
就算我们不用 bytestring 来写,程序最后也会长得像这样。差别在于我们会用 B.readFile 跟 B.writeFile 而不是 readFile 跟 writeFile。有很大的可能性,就是你只要 import 文件并在函数前加上 qualified 模块名,就可以把一个用正常 String 的程序改成用 ByteString。也有可能你是要反过来做,但那也不难。
@@ -1090,7 +1090,7 @@
先前我们谈过为什么在 I/O 部份的程序要越少越好。程序的逻辑部份尽量都放在 pure 的部份,因为 pure 的特性就是他们的结果只会根据函数的参数不同而改变。当思考 pure function 的时候,你只需要考虑他回传什么,因为除此之外他不会有任何副作用。这会让事情简单许多。尽管 I/O 的部份是难以避免的(像是打开文件之类),但最好是把 I/O 部份降到最低。Pure functions 缺省是 lazy,那代表我们不知道他什么时候会被 evaluate,不过我们也不该知道。然而,一旦 pure functions 需要丢出 Exception,他们何时被 evaluate 就很重要了。那是因为我们只有在 I/O 的部份才能接到 Exception。这很糟糕,因为我们说过希望 I/O 的部份越少越好。但如果我们不接 Exception,我们的程序就会当掉。这问题有解决办法吗?答案是不要在 pure code 里面使用 Exception。利用 Haskell 的型态系统,尽量使用 Either 或 Maybe 之类的型态来表示可能失败的计算。
-这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 main 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行引数所指定文件名称,并计算里面有多少行的程序。
+这也是为什么我们要来看看怎么使用 I/O Excetion。I/O Exception 是当我们在 main 里面跟外界沟通失败而丢出的 Exception。例如我们尝试打开一个文件,结果发现他已经被删掉或是其他状况。来看看一个尝试打开命令行参数所指定文件名称,并计算里面有多少行的程序。
import System.Environment
import System.IO
@@ -1226,15 +1226,15 @@
这边 toTry 使用 handler1 当作 handler,而 thenTryThis 用了 handler2。launchRockets 并不是 catch 的参数,所以如果有任何一个 exception 被丢出都会让我们的程序当掉,除非 launchRockets 使用 catch 来处理 exception。当然 toTry,thenTryThis 跟 launchRockets 都是 I/O actions,而且被 do syntax 绑在一起。这很像其他语言中的 try-catch blocks,你可以把一小段程序用 try-catch 包住,你可以自己调整该包多少进去。
现在你知道如何处理 I/O exception 了。我们并没有提到如何从 pure code 中丢出 exception,这是因为正如我们先前提到的,Haskell 提供了更好的办法来处理错误。就算是在可能会失败的 I/O action 中,我也倾向用 IO (Either a b),代表他们是 I/O action,但当他们被执行,他们结果的型态是 Either a b,意思是不是 Left a 就是 Right b。
-
-
-
-
-
Send feedback
-
-
+
+
+
+
+ Send feedback
+
+
diff --git a/zh-cn/introduction.html b/zh-cn/introduction.html
index 1df00d1..92f2f30 100644
--- a/zh-cn/introduction.html
+++ b/zh-cn/introduction.html
@@ -52,40 +52,40 @@
目录
从零开始
- 简介
-
-关于这份教学
-
-
-欢迎来到 Haskell 趣学指南!会想看这篇文章表示你对学习 Haskell 有很大的兴趣。你来对地方了,来让我简单介绍一下这个教学。
-
-撰写这份教学,一方面是让我自己对 Haskell 更熟练,另一方面是希望能够分享我的学习经验,帮助初学者更快进入状况。网络上已经有无数 Haskell 的教学文档,在我学习的过程中,我并不限于只参考一份来源。我常常阅读不同的教学文章,他们每个都从不同的角度出发。参考这些资源让我能将知识化整为零。这份教学是希望提供更多的机会能让你找到你想要得到的解答。
-
-这份教学主要针对已经有使用命令式编程语言 (imperative programming languages) 写程序经验 (C, C++, Java, Python …) 、却未曾接触过函数式编程语言 (functional programming languages) (Haskell, ML, OCaml …) 的读者。就算没有写程序经验也没关系,会想学 Haskell 的人我相信都是很聪明的。
-
-若在学习中遇到什么地方不懂的,Freenode IRC 上的 #Haskell 频道是提问的绝佳去处。那里的人都很友善,有耐心且能体谅初学者。
-
-(译注: Stackoverflow 上的 #haskell tag 也有很多 Haskell 神人们耐心地回答问题,提供给不习惯用 IRC 的人的另一个选择。)
-
-我经历了不少挫折才学会 Haskell,在初学的时候它看起来是如此奇怪的语言。但有一天我突然开窍了,之后的学习便如鱼得水。我想要表达的是:尽管 Haskell 乍看下如此地诡异,但假如你对编程十分有兴趣,他非常值得你学习。学习 Haskell 让你想起你第一次写程序的感觉。非常有趣,而且强迫你 Think different。
-什么是 Haskell?
-
-
-Haskell 是一门纯函数式编程语言 (purely functional programming language)。在命令式语言中执行操作需要给电脑安排一组命令,随着命令的执行,状态就会随之发生改变。例如你指派变量 a 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 (control flow),你就可以重复执行操作。然而在纯函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”,如“阶乘是指从1到某个数的乘积”,"一个串列中数字的和"是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 (variable) 一旦被指定,就不可以更改了,你已经说了 a 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。x=5 代表 x 就是 5,不是说 x 在 5 这个状态。) 所以说,在纯函数式编程语言中的函数能做的唯一事情就是利用引数计算结果,不会产生所谓的"副作用 (side effect)" (译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“引用透明 (Referential Transparency)” (译注:这就跟数学上函数的使用一样)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
-
-Haskell 是惰性 (lazy) 的。也就是说若非特殊指明,函数在真正需要结果以前不会被求值。再加上引用透明,你就可以把程序仅看作是数据的一系列变形。如此一来就有了很多有趣的特性,如无限长度的数据结构。假设你有一个 List: xs = [1,2,3,4,5,6,7,8],还有一个函数 doubleMe,它可以将一个 List 中的所有元素都乘以二,返回一个新的 List。若是在命令式语言中,把一个 List 乘以 8,执行 doubleMe(doubleMe(doubleMe(xs))),得遍历三遍 xs 才会得到结果。而在惰性语言中,调用 doubleMe 时并不会立即求值,它会说“嗯嗯,待会儿再做!”。不过一旦要看结果,第一个 doubleMe 就会对第二个说“给我结果,快!”第二个 doubleMe 就会把同样的话传给第三个 doubleMe,第三个 doubleMe 只能将 1 乘以 2 得 2 后交给第二个,第二个再乘以 2 得 4 交给第一个,最终得到第一个元素 8。也就是说,这一切只需要遍历一次 list 即可,而且仅在你真正需要结果时才会执行。惰性语言中的计算只是一组初始数据和变换公式。
-
-Haskell 是静态类型 (statically typed) 的。当你编译程序时,编译器需要明确哪个是数字,哪个是字串。这就意味着很大一部分错误都可以在编译时被发现,若试图将一个数字和字串相加,编译器就会报错。Haskell 拥有一套强大的类型系统,支持自动类型推导 (type inference)。这一来你就不需要在每段代码上都标明它的类型,像计算 a=5+4,你就不需另告诉编译器“ a 是一个数值”,它可以自己推导出来。类型推导可以让你的程序更加简练。假设有个函数是将两个数值相加,你不需要声明其类型,这个函数可以对一切可以相加的值进行计算。
-
-Haskell 采纳了很多高端编程语言的概念,因而它的代码优雅且简练。与同层次的命令式语言相比,Haskell 的代码往往会更短,更短就意味着更容易理解,bug 也就更少。
-
-Haskell 这语言是一群非常聪明的人设计的 (他们每个人都有 PhD 学位)。最初的工作始于 1987 年,一群学者聚在一起想设计一个屌到爆的编程语言。到了 2003 年,他们公开了 Haskell Report,这份报告描述了 Haskell 语言的一个稳定版本。(译注:这份报告是 Haskell 98 标准的修订版,Haskell 98 是在 1999 年公开的,是目前 Haskell 各个编译器实现缺省支持的标准。在 2010 年又公开了另一份 Haskell 2010 标准,详情可见穆信成老师所撰写的简介。
-你需要些什么来使用 Haskell 呢?
-
-
-一句话版本的答案:一个编辑器和一个编译器。我们不会对编辑器多加着墨,你可以用你喜欢的编辑器。至于编译器,在这份教学中我们会使用目前最流行的版本:GHC。而安装 GHC 最方便的方法就是去下载 Haskell Platform,他包含了许多现成 Runtime Library 让你方便写程序。(译注:Ubuntu 的用户有现成的套件可以使用,可以直接 apt-get install Haskell-platform 来安装。但套件的版本有可能比较旧。)
-
-GHC 可以解释执行 Haskell Script (通常是以 .hs 作为结尾),也可以编译。它还有个交互模式,你可以在里面调用 Script 里定义的函数,即时得到结果。 对于学习而言,这比每次修改都编译执行要方便的多。想进入交互模式,只要打开控制台输入 ghci 即可。假设你在 myfunctions.hs 里定义了一些函数,在 ghci 中输入 :l myfunctions.hs,ghci 便会加载 myfunctions.hs。之后你便可以调用你定义的函数。一旦修改了这个 .hs 文件的内容,再次执行 :l myfunctions.hs 或者相同作用的 :r ,都可以重新加载该文件。我自己通常就是在 .hs 文件中定义几个函数,再到 ghci 加载,调用看看,再修改再重新加载。这也正是我们往后的基本流程。
+ 简介
+
+关于这份教学
+
+
+欢迎来到 Haskell 趣学指南!会想看这篇文章表示你对学习 Haskell 有很大的兴趣。你来对地方了,来让我简单介绍一下这个教学。
+
+撰写这份教学,一方面是让我自己对 Haskell 更熟练,另一方面是希望能够分享我的学习经验,帮助初学者更快进入状况。网络上已经有无数 Haskell 的教学文档,在我学习的过程中,我并不限于只参考一份来源。我常常阅读不同的教学文章,他们每个都从不同的角度出发。参考这些资源让我能将知识化整为零。这份教学是希望提供更多的机会能让你找到你想要得到的解答。
+
+这份教学主要针对已经有使用命令式编程语言 (imperative programming languages) 写程序经验 (C, C++, Java, Python …) 、却未曾接触过函数式编程语言 (functional programming languages) (Haskell, ML, OCaml …) 的读者。就算没有写程序经验也没关系,会想学 Haskell 的人我相信都是很聪明的。
+
+若在学习中遇到什么地方不懂的,Freenode IRC 上的 #Haskell 频道是提问的绝佳去处。那里的人都很友善,有耐心且能体谅初学者。
+
+(译注: Stackoverflow 上的 #haskell tag 也有很多 Haskell 神人们耐心地回答问题,提供给不习惯用 IRC 的人的另一个选择。)
+
+我经历了不少挫折才学会 Haskell,在初学的时候它看起来是如此奇怪的语言。但有一天我突然开窍了,之后的学习便如鱼得水。我想要表达的是:尽管 Haskell 乍看下如此地诡异,但假如你对编程十分有兴趣,他非常值得你学习。学习 Haskell 让你想起你第一次写程序的感觉。非常有趣,而且强迫你 Think different。
+什么是 Haskell?
+
+
+Haskell 是一门纯函数式编程语言 (purely functional programming language)。在命令式语言中执行操作需要给电脑安排一组命令,随着命令的执行,状态就会随之发生改变。例如你指派变量 a 的值为 5,而随后做了其它一些事情之后 a 就可能变成的其它值。有控制流程 (control flow),你就可以重复执行操作。然而在纯函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”,如“阶乘是指从1到某个数的乘积”,"一个串列中数字的和"是指把第一个数字跟剩余数字的和相加。你用宣告函数是什么的形式来写程序。另外,变量 (variable) 一旦被指定,就不可以更改了,你已经说了 a 就是 5,就不能再另说 a 是别的什么数。(译注:其实用 variable 来表达造成字义的 overloading,会让人联想到 imperative languages 中 variable 是代表状态,但在 functional languages 中 variable 是相近于数学中使用的 variable。x=5 代表 x 就是 5,不是说 x 在 5 这个状态。) 所以说,在纯函数式编程语言中的函数能做的唯一事情就是利用参数计算结果,不会产生所谓的"副作用 (side effect)" (译注:也就是改变非函数内部的状态,像是 imperative languages 里面动到 global variable 就是 side effect)。一开始会觉得这限制很大,不过这也是他的优点所在:若以同样的参数调用同一个函数两次,得到的结果一定是相同。这被称作“引用透明 (Referential Transparency)” (译注:这就跟数学上函数的使用一样)。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
+
+Haskell 是惰性 (lazy) 的。也就是说若非特殊指明,函数在真正需要结果以前不会被求值。再加上引用透明,你就可以把程序仅看作是数据的一系列变形。如此一来就有了很多有趣的特性,如无限长度的数据结构。假设你有一个 List: xs = [1,2,3,4,5,6,7,8],还有一个函数 doubleMe,它可以将一个 List 中的所有元素都乘以二,返回一个新的 List。若是在命令式语言中,把一个 List 乘以 8,执行 doubleMe(doubleMe(doubleMe(xs))),得遍历三遍 xs 才会得到结果。而在惰性语言中,调用 doubleMe 时并不会立即求值,它会说“嗯嗯,待会儿再做!”。不过一旦要看结果,第一个 doubleMe 就会对第二个说“给我结果,快!”第二个 doubleMe 就会把同样的话传给第三个 doubleMe,第三个 doubleMe 只能将 1 乘以 2 得 2 后交给第二个,第二个再乘以 2 得 4 交给第一个,最终得到第一个元素 8。也就是说,这一切只需要遍历一次 list 即可,而且仅在你真正需要结果时才会执行。惰性语言中的计算只是一组初始数据和变换公式。
+
+Haskell 是静态类型 (statically typed) 的。当你编译程序时,编译器需要明确哪个是数字,哪个是字串。这就意味着很大一部分错误都可以在编译时被发现,若试图将一个数字和字串相加,编译器就会报错。Haskell 拥有一套强大的类型系统,支持自动类型推导 (type inference)。这一来你就不需要在每段代码上都标明它的类型,像计算 a=5+4,你就不需另告诉编译器“ a 是一个数值”,它可以自己推导出来。类型推导可以让你的程序更加简练。假设有个函数是将两个数值相加,你不需要声明其类型,这个函数可以对一切可以相加的值进行计算。
+
+Haskell 采纳了很多高端编程语言的概念,因而它的代码优雅且简练。与同层次的命令式语言相比,Haskell 的代码往往会更短,更短就意味着更容易理解,bug 也就更少。
+
+Haskell 这语言是一群非常聪明的人设计的 (他们每个人都有 PhD 学位)。最初的工作始于 1987 年,一群学者聚在一起想设计一个屌到爆的编程语言。到了 2003 年,他们公开了 Haskell Report,这份报告描述了 Haskell 语言的一个稳定版本。(译注:这份报告是 Haskell 98 标准的修订版,Haskell 98 是在 1999 年公开的,是目前 Haskell 各个编译器实现缺省支持的标准。在 2010 年又公开了另一份 Haskell 2010 标准,详情可见穆信成老师所撰写的简介。
+你需要些什么来使用 Haskell 呢?
+
+
+一句话版本的答案:一个编辑器和一个编译器。我们不会对编辑器多加着墨,你可以用你喜欢的编辑器。至于编译器,在这份教学中我们会使用目前最流行的版本:GHC。而安装 GHC 最方便的方法就是去下载 Haskell Platform,他包含了许多现成 Runtime Library 让你方便写程序。(译注:Ubuntu 的用户有现成的套件可以使用,可以直接 apt-get install Haskell-platform 来安装。但套件的版本有可能比较旧。)
+
+GHC 可以解释执行 Haskell Script (通常是以 .hs 作为结尾),也可以编译。它还有个交互模式,你可以在里面调用 Script 里定义的函数,即时得到结果。 对于学习而言,这比每次修改都编译执行要方便的多。想进入交互模式,只要打开控制台输入 ghci 即可。假设你在 myfunctions.hs 里定义了一些函数,在 ghci 中输入 :l myfunctions.hs,ghci 便会加载 myfunctions.hs。之后你便可以调用你定义的函数。一旦修改了这个 .hs 文件的内容,再次执行 :l myfunctions.hs 或者相同作用的 :r ,都可以重新加载该文件。我自己通常就是在 .hs 文件中定义几个函数,再到 ghci 加载,调用看看,再修改再重新加载。这也正是我们往后的基本流程。