if-then-else
Haskell是以表达式为主导的语言(expression-oriented),所有语句必须要能给出一个具体的值。比如我们喜闻乐见的if-else结构:
Prelude> if True then 1
<interactive>:24:15:
parse error (possibly incorrect indentation or mismatched brackets)
无法通过编译?实际上很简单,如果if a then 1 可以通过编译的话,当a取定False的时候这个表达式就无法给出一个具体的值了——这和命令式语言有很大的不同。命令式语言是一堆陈述(statement)的序列,关心 if-else的返回值几乎没什么意义。而Haskel则要求一切表达式必须要能给出值,因此——
我们必须要填补else:
Prelude> if True then 1 else "foo"
<interactive>:2:14:
No instance for (Num [Char])
arising from the literal `1'
Possible fix: add an instance declaration for (Num [Char])
In the expression: 1
In the expression: if True then 1 else "foo"
In an equation for `it': it = if True then 1 else "foo"
还是无法通过编译?
错误还是比较明显的。作为纯函数至上的语言,作为一个可以进行静态类型分析的语言,一个纯的表达式在不同输入下的返回值的类型不同,这种行为简直是不能忍受的。因此,将"foo"改成2,则就可以顺利通过:
Prelude> if True then 1 else 2
1
下面来看一个比较复杂的例子(别忘了函数定义最好放在hs文件中):
myDrop n xs = if n <= 0 || null xs
then xs
else myDrop (n - 1) (tail xs)
这是模拟一个模仿drop的函数。好吧其实也不复杂。。顶多也就是个递归什么的。。
另外注意,null是一个函数,它接受一个列表,返回一个Bool表示该列表是否为空:
Prelude> :info null
null :: [a] -> Bool -- Defined in `GHC.List'
另外也可以从这个例子略看出Haskell的类型推导的一些方法,为什么Haskell能知道xs是一个列表呢?Haskell又怎么知道myDrop的返回值呢?Haskell又是通过什么方式推断出类型矛盾的呢?——虽然这并不是本文想讨论的内容,但还是比较有意思所以写一下好了。
首先,可以看出,null的定义里的输入必须是一个[a],tail的输入也必须是一个[a],因此Haskell可以自动判断xs是一个[a]类型。
然后,根据if-then-else中then的返回可以看出myDrop的一种返回为xs,因此myDrop的返回类型至少是[a]——前面已经判断出xs是[a]类型,但有时候可能会更具体化为[Num]啊[Char]之类的,所以说"至少是"。
最后,根据else语句,现在已经假定了myDrop的返回类型是[a],而myDrop (n - 1) (tail xs) 又符合myDrop的输入是一个合法的语句,因此它在没有其他条件限制下返回类型也是[a],匹配成功,喜闻乐见。
上述三步只是一个很简略的过程,实际上的操作比这个略复杂,因为Haskell还需要考虑各种subtyping问题,但原理实际上并不复杂。有兴趣的可以去看Types+and+Programming+Languages一书。RWH和TPL两本搭配着看,效果拔群!
另外注意,实际上这个函数写到同一行也是没问题的,只是读起来艰涩些:
myDropX n xs = if n <= 0 || null xs then xs else myDropX (n - 1) (tail xs)
虽然Haskell没有python那样对缩进要求那么严格,但千万不要省略缩进——它会延续一个已存在的定义,而不是新创建一个。
模式匹配
惰性求值
True || (length[1..] > 0)
还记得那个无限列表[1..]么?直接在ghci中查看[1..]的话,你除非强制结束ghci进程,否则只能等着被无限刷屏。但这个表达式可以正常终止,因为左边的True把右边的计算给“短路”了。
head [1..]
如果是python的话,这会带来很大的麻烦——传递给head的参数必须要被规约,这样就会挂起。而采用了惰性求值策略,它却可以正常给出结果1。
head (tail [1..])
Haskell可以给出正确结果2,但不要忘了(tail [1..])的结果
也是一个无限列表!因此,惰性求值的策略既不要求输入是被规约的,也不会要求让输出也被规约。它只要求在
必须要进行规约的时候才开始规约。
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
在定义的时候并不会进行对列表的求值,否则它理论上将是无穷无尽的,除非把你的内存耗尽。
这样的无限列表有什么好处呢?