缘起
开篇之前先说说为啥开始学习Haskell,作为一个主要写C代码的中老年工程师,总觉得写代码有点那么个思维定式,而Haskell是一个和C完全不同的语言,它会迫使你放弃掉习惯了小半辈子的思维方式,可以帮助咱们中老年朋友跳出编程“舒适区”,避免思维定式。
以下内容与广大中老年朋友分享学习中的粗糙简介,大家感兴趣的话,可以留言区交流。
没有赋值语句
某种意义上说,C语言有两个最重要的东西:表达式和赋值,C语言进行数据运算的方式,就是用表达式做一个运算,然后用赋值写入到变量,如此往复。
C语言里面,变量是一个数据的容器,因此可以多次写入(除非特殊说明是不可写入),比如咱们代码可以这么写:
int test(int x)
{x = x + 10;x = x * 2;return x;
}
这里 x + 10 和 x * 2 都是表达式,然后用赋值把表达式计算的结果写入x。显然,后续的写入会覆盖之前的结果。
由于变量可以多次赋值,因此在C代码里面,一个变量名字,在不同的时间,内部的值显然可以是不同的。因此我们分析和调试C代码的时候,就要特别留意次序,也就是搞清楚“来龙去脉”,这也是很多时候写出来bug的根源。
比如这么一段代码。
int test(int x)
{x = x + 10;。。。if (blahblah) x = 0;。。。return x;
}
假设咱们看代码的时候,漏掉了那个if(blahblah),或者咱们误判了blahblah,结果就完全不同了。
那么Haskell呢?
Haskell里面的=不是赋值,而是绑定(binding),啥叫绑定呢?它的含义更像是数学里面,我们说,把什么什么记作什么什么。
比如下面这行Haskell代码:
x = 1
它的意思是,把1给绑定到符号x上面,也就是说,以后咱们提到x的地方,它其实就是1.要注意的是,一个符号只能固定的绑定一个东西,不能重复绑定,如果我们尝试“修改”x的值会怎样?
x = 1
x = 2
Haskell会报错:Multiple declarations of `x'
它意思是说,您老先说x是1,又说x是2,那x到底是啥呢?
很显然,在Haskell,这个x并不代表一个保存数据的地方,不是C语言变量的概念,它只是一个符号,你可以定义这个符号的含义,而且不能自相矛盾,就是说你不能说这个符号既是一个东西,又是另一个东西。
那么如此有什么好处呢?就是在Haskell里面,我们不需要分析代码执行到哪里了,就可以确定一个符号是什么,也很难搞错。因为不管写在哪里,是啥就是啥,一言九鼎。
补充一点,不同的scope,同一个符号可能绑定不同的内容,比如两个函数里面同一个x可以是不同的,不过这个区分显而易见,基本上不可能混淆
数学意义的函数定义
既然没了“赋值”一说,Haskell的函数和C的函数就不一样了,它不是描述一个计算过程,而是定义一个数学意义的函数关系。比如咱们定义一个一元二次多项式函数:
f x = x*x + 2*x + 1
咱们前面说过,Haskell里面=代表左边的东西是右边的东西。因此上面这句话的意思就是以x为自变量,计算f这个函数,它是x*x + 2*x + 1
这跟数学概念上的函数定义是吻合的。实际上,写法也很像,只是数学上咱们会多个括号,写成 f(x)。
由于=代表一种定义,那么很显然,有些函数在不同情况下,定义是不一样的。比如说斐波那契,当x是0或者1或者>1,情况是不同的。Haskell里面可以分几次定义一个函数。具体来说就是
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
代码的意思显而易见,斐波那契数列,第0个是0,第1个是1,之后的,是前面两个的和。
到这里咱们就能感觉到Haskell和C本质上不同的地方。从根本上说,C程序是直接告诉计算机,你该作甚,一步一步地给你说清楚,计算机不需要“思考”,直接干活拉倒。Haskell则是告诉计算机,哥们我想要什么结果,至于怎么算,你自己琢磨。
咱们再用一个小例子说明如何用Haskell的方式思考问题。假设我想得到一个字符串,内容是n个'X',就是说我想定义一个函数,比如叫做nX,当我调用 nX 5,我希望得到"XXXXX".
如果是C语言,咱们思考的是,计算机一步一步怎么干活,大概就是,咱先弄个空字符串,然后再一个一个字符的增加,如此这般,这般如此。
Haskell里面,我们要换一个思路,我们要想的是,这玩意是什么
nX 0 是什么?显然,应该是空字符串,因为它表达的是0个X.
nX 1是什么?显然,应该是"X",我们也可以这么表达,空字符串基础上,增加一个"X"
nX 2是什么?显然,应该是"XX",我们也可以说是(nX 1)基础上,增加一个"X"
那么一般的,nX n就应该是 nX (n-1)加上一个"X"
nX 0 = ""
nX n = nX (n - 1) ++ "X"
注意到没有,咱们写这个Haskell代码的时候,完全没有思考计算机具体干活的过程。实际上,咱们看着这段Haskell代码,也不晓得编译器最终会编译出什么东西,除非你懂Haskell编译器。但是我们虽然不知道Haskell代码的实际计算过程,我们却很清楚的知道计算结果
所以说写Haskell代码的思维,更像是老板思维,表达清楚自己要什么,让员工,这里就是Haskell编译器,发挥主观能动性想清楚怎么做。甚至说,只要你能定义清楚结果是什么,你就算真的不知道该怎么做才能得到结果,也没关系,因为你是老板,老板为啥需要知道怎么干活?
更强的代码表达能力
由于Haskell代码的重点是表达想要什么,表达能力就非常重要。比如我想表达1到100之间,可以同时被2和3整除的数。
numList = [x | x <- [1 .. 100], x `mod` 2 == 0, x `mod` 3 == 0 ]
这段代码的意思是:
- 构造一个列表,其元素为 x
- x属于1到100
- x对2取模为0(即整除)
- x对3取模为0(即整除)
很显然,这个代码写的内容,和我们想要的结果,几乎是1:1对应关系。
再比如我想表达一个函数,如果x小于100,那么结果就是100,否则结果就是x
f x = if x < 100 then 100 else x
如果我条件多一些,比如小于100则结果是100,大于200则结果是200,其他原封不动,可以这么写:
f x| x < 100 = 100| x > 200 = 200| otherwise = x
这里的|可以读作“当”,然后你看这个代码和我们想表达的意思,几乎是一一对应。
小结
习惯写C代码,初看Haskell会非常不习惯,实际上主要是思维方式的不一样,但是一旦我们转变了思维方式,就是说,不再思考计算机具体怎么工作,而是思考我到底想要什么,你会马上发现,Haskell代码的表达力,不是一般的强,而且很多时候,可以极大的简化写代码的工作。
One More Thing
Haskell的函数调用,跟大多数其他编程语言比,它少了个括号,即人家都是 f(x) 它是 f x,这写法倒更像shell脚本了。其实它这个写法,颇有妙处的。
我们考虑一个多个自变量的函数,比如说
f x y = x + y
然后我们写 f 1 2 结果当然就是3,这个没啥,那如果我们写 f 1,它是啥意思呢?
我们不妨盲猜一下,我觉得它的含义是:
f 1 y = 1 + y
注意这里,f 1我们看做一个整体的话,那么它就应该是1 + y
假设我们再定义一个g
g = f 1
那我觉得,这就等价于
g y = 1 + y
那你觉得对不对呢?实验表明,对的。
怎么理解这件事?我们可以这么理解 f 1 这个东西:把f这个函数的第一个参数x的值绑定成1,然后计算这个函数,显然 x + y 就变成了 1 + y。
因此我们可以把f这个函数,不是理解为两个数字映射到一个数字,而是理解为把一个数字映射到一个函数,而后者这个函数,是把一个数字映射到一个数字。
这么说有那么一点绕,咱们看 f 1 2 这个调用,我们不要看做一个整体,而是看做:
(f 1) 2
即,先是f 1,然后得到的结果(还是个函数)再作用到2上面。
这就相当于
g = f 1
g 2
所以,咱们可以把所有的Haskell函数都看做只有一个参数的函数,只是这个函数的结果,可能还是个函数,实际上,Haskell就是这么理解的。比如我们看f的类型:
ghci> :t f
f :: Num a => a -> a -> a
这个意思是说,有个类型a,这个a是数字(Num),然后这个函数,是把类型 a 映射到 a 映射到 a。
err。它为啥不说是 (a, a) -> a 呢?
咱们仔细瞅瞅这个 a -> a -> a 注意后面这个 a -> a 是一个函数,把一个数映射到另一个数。所以前面的映射,就是把a映射成一个a->a的函数。
有点绕口令的感觉,不过你仔细琢磨清楚了,一定会感叹一句,如此妙哉。