函数式编程

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

  特点有两个:

  1. order-independent ——绑定在源代码的顺序并不重要
  2. 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的函数
  • (type1type2, ..., 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运行等功能。