函数式编程
HASKELL起步入门教程
Haskell作为一门纯函数语言,它有以下特点:
- Haskell强表达能力可以提高软件的生产力
- 小语言核心提供了很大的灵活性
- 代码可以很简洁,提高开发速度
- 从编译和解释两种语言中都能得到两全其美的好处
- Haskell使代码更容易理解和维护
- 可以深入复杂的库和理解代码是做 什么
- Haskell可以提高系统的鲁棒性
- 强类型捕获在编译时许多的错误
- 函数代码允许更好的测试方法
- 没有变化的语义可以使非并发代码并行化
- 对数据竞争的并发编程抽象
开始使用Haskell
安装 Haskell平台 ,包括 GHC 编译器。 IDE编辑工具可使用Atom,安装其Haskell插件,安装方式是在命令行输入 apm install ide-haskell。
创建一个hello.hs文件,内容如下:
main = putStrLn "Hello, world!"
编译该文件:
$ ghc --make hello
[1 of 1] Compiling Main ( hello.hs, hello.o )
Linking hello ...
$ ./hello
Hello, world!
或者以解释器方式:GHCI interpreter 如下:
$ ghci hello.hs
GHCi, version 7.0.3: http://www.haskell.org/ghc/ :? for help
...
Ok, modules loaded: Main.
*Main> main
Hello, world!
函数定义
下面是我们定义一个函数addone,输入参数是整数,输出也是整数:
addOne :: Integer -> Integer
addOne n = n + 1
第一行是我们定义一个函数名称为addOne,这个函数有一个Integer参数,返回一个Integer。第二行表示对于我们函数的任何输入,我们将把这个输入表示为n,那么我们将返回n+1。
注意,这里 = 是数学中的意义,在我们程序中任何地方有addOne n,我们能够使用n+1来替代它,得到的都是精确同样的结果,这其实是引用透明的案例,因为我们的函数对于任何给定输入总是返回同样的值。
回到REOL,键入:reload,那么敲入:
addOne 10
-- = 11
调用这个函数的方式是函数名称后跟着参数,之间都是有空格分开,没有任何逗号和括号,
函数与模式匹配
让我们定义另外一个函数,它是对于输入一个姓氏能够输出其姓名:
lastName :: String -> String
lastName "anthony" = "gillis"
lastName "michelle" = "jocasta"
lastName "gregory" = "tragos"
Haskell函数定义依赖于模式匹配,如果你使用参数"anthony"调用lastName函数,,那么函数就会返回字符串"gillis",如果你使用"michelle",那么就会返回"jocasta",我们在REPL输入:
lastName "anthony"
-- = "gillis"
但是如果我们输入一个在这三个中不存在姓呢?
lastName "bob"
就会得到一个exception: Non-exhaustive patterns in function lastName问题是因为我们的函数不是total,它并没有对于每个可能的输入有一个定义好的输出,通常函数应该是无论任何可能都是确定的,那么我们重新定义函数如下:
lastName :: String -> String
lastName "anthony" = "gillis"
lastName "michelle" = "jocasta"
lastName "gregory" = "tragos"
lastName n = "<unknown>"
现在我们的函数是total了,最后一个会捕获所有情况,如果上面三个不匹配,那么最后一个总是将参数绑定到n,我们能够使用 _ 来代表n 表示我们其实不关心其值。
多个参数
让我们定义一个函数areAscending,它有三个整数参数,如果它们是严格递增那么就返回真:
areAscending :: Integer -> Integer -> Integer -> Bool
areAscending a b c = a < b && b < c
我们的类型语法看上去是不是有点奇怪?参数之间有箭头,且返回一个值类型?这种多参数函数称为curried,柯里化是将多个参数的一个函数作为输入,转入一系列只有一个参数的函数,返回另外一个函数,比如用伪Swift代码如下:
myFunc(a: A, b: B, c: C) -> Z
函数func1(a: A)当被调用时,返回一个函数func2(b: B),它又返回一个函数func3(c: C),它被调用最后返回Z类型结果。
请注意,在我们的模式匹配中,我们分配第一个参数到a,第二个是b,第三个是c,这样我们能够在=后面使用它们来执行我们的计算,在REPL调用结果如下:
areAscending 1 2 3
-- = True
areAscending 3 4 2
-- = False
如果我们希望使用一个表达式调用这个函数,而不是一个个参数遍历,那么我们需要使用括号包围:
areAscending 1 (1 + 1) 3
-- = True
而没有括号的areAscending 1 1 + 1 3则被解释为(areAscending 1 1) + (1 3),这是没有意义。
在模式匹配中的Guard
有时,模式匹配单独无法有效描述一个函数,引入一个称为guard的模式,如果这个模式匹配,每个guard的测试表达式将按顺序检查,如果测试表达式匹配,guard的值将被使用。
以FizzBuzz为例,这是一个有一个整数参数,返回四个字符串:“fizzbuzz”中任何一个,如果数字能够被3和5整除,如果被3整除,那么是fizz,如果是被5整除是buzz,否则为“”;
fizzBuzzHelper :: Integer -> String
fizzBuzzHelper n
| n `mod` 3 == 0 && n `mod` 5 == 0 = "fizzbuzz"
| n `mod` 3 == 0 = "fizz"
| n `mod` 5 == 0 = "buzz"
| otherwise = ""
Guard开始使用 | ,后面是一个测试表达式,其返回结果或真或假,后面跟着 =,然后是返回的值,
otherwise能够捕获其他所有情况。在REPL执行结果:
fizzBuzzHelper 33
-- = "fizz"
fizzBuzzHelper 15
-- = "fizzbuzz"
零参数函数
如果参数没有怎么办?零参数函数也是一个函数,总是返回一个值,这又是引用透明的原理了,因为只有一个办法调用无参数函数,这个函数也总是返回一个值:
someValue :: String
someValue = "hello world"
零参数函数类似于常量,只要看到someValue,我们都可以使用"hello world"来替代,这正是我们从一个常量中应该预期到结果。
绑定Binding
Haskell使用=实现绑定,前面我们说=是引用透明的案例,是一种数学意义,实际是将两者绑定了。
x = 2 -- 两个连字符号表示注解 y = 3 -- main = let z = x + y -- let 引入一个本地绑定 in print z -- 程序将打印 5
Binding名称不能大写,绑定可以定义一个或多个参数的函数,函数和参数使用空格,这是Haskell区别其他语言的简洁之处:
add arg1 arg2 = arg1 + arg2 -- 定义函数add
five = add 2 3 -- 调用函数add
括号可以包装复合表达式:
bad = print add 2 3 -- error! (打印只能有一个参数)
main = print (add 2 3) -- ok, 使用一个参数5作为打印参数
变量是不可变的
与命令式语言变量不同,Haskell绑定变量 不可变的:
x = 5
x = 6 -- 错误, 不能再绑定x
特点有两个:
- order-independent ——绑定在源代码的顺序并不重要
- Lazy懒赋值 ——定义变量只在需要的时候进行赋值
如代码:
safeDiv x y =
let q = div x y -- 如果 y == 0 安全如q从未赋值
in if y == 0 then 0 else q
main = print (safeDiv 1 0) -- 打印 0
此外,变量名的scope是只在自己定义的范围内递归的。
x = 5 -- 这在main中不能用
main = let x = x + 1 -- 引入新的x, defined in terms of itself
in print x -- 程序无限循环program "diverges" (i.e., loops forever)
没有可变变量如何编程?
在C语言中,我们使用可变变量创建循环:
long factorial (int n)
{
long result = 1;
while (n > 1)
result *= n--;
return result;
}
在Haskell中,可以使用在新的scope使用递归参数符号"re-bind"
factorial n = if n > 1
then n * factorial (n-1)
else 1
表达式和绑定都有一个类型
类型是值的集合set,函数编程中类型和面向对象的Class类型是不同的,因为函数中每个Class类的概念。
类型Bool(Haskell核心类型是大写开始)是一个有两个元素的集合,这两个元素 是True和False,类型Char是所有Unicode字符如'a'或'b'的集合。
集合可以是有限或无限,String类型是Char类型列表集合的代名词,它是一个无限集合。
我们在Haskell中定义x为Intger:
x :: Integer
双冒号::表达的是有某个类型。我们说它x是一个整数集合中的一个元素,Integer在Haskell中是一个无限集合,它能用作做精确的算术,也有一个有限集合Int, 它是对应机器类型,类似C++的int
函数类型的表达是在两个类型之间插入一个箭头来表达:
f ∷ A → B
有一个狡猾之处使得类型集合的识别比较棘手,多态函数会调用循环定义,事实是你不能有一个基于所有集合上的集合,但是可以有集合的范畴Category,称为Set(英文字母第一个大写的set集合)
Set是一个特殊的范畴Category,因为我们会对其中的元素对象做些事情,比如,我们知道空的集合是没有元素,而我们也知道有一个只有一个元素的集合,我们知道,函数会映射一个集合中的元素到另外一个集合中,它们能映射两个元素到一个, 但是一个元素不能映射到两个,一个恒等identity函数会映射一个集合的元素到它自己里面。关键是我们是要忘记这些信息, 而使用纯粹范畴符号也就是对象和箭头来表达
类型有下面几种:
- Bool - 有两个元素: True 或 False
- Char - unicode符合集合
- Int - 固定大小整数
- Integer - 无限大小整数
- Double - IEEE 浮点数字
- type1 -> type2 - 一个输入类型type1 到输出类型 type2的函数
- (type1, type2, ..., typeN) - 类型数组tuple
- () - 零数组tuple, 称为unit (类似C的void); 这个类型只有一个值:空。
定义一个符号类型表达式类型:
x :: Integer
x = (1 :: Integer) + (1 :: Integer) :: Integer
空数组类似与C中:
int f44() { return 44; }
使用Haskell表达:
f44 :: () -> Integer
f44 () = 44
第一行声明了f44的参数类型是一个unit,第二行通过模式匹配f44的唯一构造函数,也就是(),产生44的结果;以后你就可以通过()调用这个函数。如:
f44 ()
Monoid
首先,需要了解什么是Monoid。在Haskell中,表达三个函数的Monoid可以如下表达:
add :: Integer -> (Integer -> Integer) add arg1 arg2 = arg1 + arg2
这里使用括号,其实这个Monoid符合结合律,括号是没有必要的,等同于如下:
add :: Integer -> Integer -> Integer
Lambda抽象
- 你可以通过lambda实现匿名函数
- 符号是: \variable(s) -> body ( \被称为"lambda")
- 案例:countLowercaseAndDigits :: String -> Int countLowercaseAndDigits = length . filter (\c -> isLower c || isDigit c)
- lambda使用模式匹配能够分解值:... (\(Right x) -> x) ...
- But note that guards or multiple bindings are not allowed
- Patterns must have the right constructor or will get run-time error
IDE安装
下载安装:从 haskell.org下载haskell,不过首先你需要有ghc
(编译器) 和 ghci
(交互REPL)已经安装。当然还有cabal
,cabal
是一个依赖包管理器,类似Java的Maven Node.js的NPM
下面是需要一个Haskell编辑器,这里推荐sublime text,你可以通过 sublimes 包管理器安装haskell插件:sublime haskell plugin
在windows下会碰到问题是不能安装 unix-2.7
。其实你不必从cygwin安装,而是下载hdevtools项目。进入项目目录:
> cabal configure
> cabal build
> cabal install
Hdevtools能在命令行让你访问文件的类型信息,这个很重要,否则你的sublimetext中类型推断将不起效果。
另外一个sublime插件是自动加载文件到REPL:plugin
在windows环境的安装过程中会有很多问题,Cabal准备安装某个包时出现Gzip错误,解决方式是解决在path路径里面的mingw/cygwin 和 gnu utils 冲突。建议你全部删除它们,重新安装一个官方gnu32 utils 。更多安装问题见:stack overflow question.
创建一个项目
project.cabal文件中一些设置,然后再重新配置 cabal。如果你想有单元测试设置了自动发现的测试(类似Java或C#测试属性),你必须再进一步,创建一个单独的测试运行器,并添加了一堆神奇的Haskell预处理标签。一些IDE像EclipseFP为你自动设置这一切,但如果你走sublime路线,你只能靠自己。
grunt scaffolding task能够自动创建你的cabalized 项目,设置你的单元测试。
grunt-init haskell-test,它会提示你输入项目名称,创建一个cabal文件和项目目录(有src和test目录),创建一个测试运行器,创建初始的单元测试文件,然后会自动运行cabal configure --enable-tests。最好完成是通过敲入cabal build,运行测试敲入cabal tes
理解Cabal
cabal 文件类似f# or c#的.proj 文件,或者Java的Maven pom.xml,它描述了依赖关系。当你的增加新的依赖库包时,需要更新这个文件。
通过上面步骤,你可以实现编程时的语法高亮,类型推断,语法完成提示,错误突出显示,单元测试,REPL,(通过REPL)debuggging运行等功能。