作者:大魔头-诺铁
链接:https://zhuanlan.zhihu.com/p/21404404
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在 ghci 里设置一下显示类型信息:
Prelude> :set +t
Prelude> 3 * 4
12
it :: Num a => a
设置了 +t
后,执行结果后面会跟一行显示结果的类型,每次退出 ghci 后要重新设置。Haskell 里声明变量及其类型的语法就如这个例子里的 it :: Num a => a
, it
是变量名,=>
箭头前面的 Num a
是个类型约束,表示编译器现在不确定 it 是个什么具体类型,可能是整数,也可能是浮点数等等,只知道它是 Num 类型类(typeclass)的实例
,因为它能做乘法运算。就目前的使用场景来说,知道它是 Num 也就够了。类型约束是可选的。比如
Prelude> 'c'
'c'
it :: Char
这里编译器明确的确定了 'c' 的类型是 Char。 顺便说一句,这个 it 变量是实际可用的:
Prelude> it
'c'
it :: Char
字符串的类型实际上就是字符列表。
Prelude> "damotou"
"damotou"
it :: [Char]
你可以用字符构造一个列表,结果是一样的
Prelude> ['d','m','o','t','o','u']
"dmotou"
it :: [Char]
列表的所有元素必须是相同类型的。
Prelude> [1,'a']
<interactive>:15:2:
No instance for (Num Char) arising from the literal ‘1’
In the expression: 1
In the expression: [1, 'a']
In an equation for ‘it’: it = [1, 'a']
编译器根据第一个元素,1,推断这个列表应该是 (Num a)
类型的,但是 'a' 是 Char 类型,而 Char 类型不是 Num 类型的实例,所以编译不过。
我这么啰嗦的解释这个编译错误,是因为用 Haskell 写程序大部分时间都在跟编译错误做斗争,当程序编译过了的时候基本上功能就是好的了。所以需要细心的体味编译器给出的贴心的编译错误~~~
Haskell支持元组(tuple)类型
Prelude> (1,'a')
(1,'a')
it :: Num t => (t, Char)
Prelude> ("Haskell",'H',1)
("Haskell",'H',1)
it :: Num t => ([Char], Char, t)
元组的类型就是其每个成员的类型。当然你可以把元组放到列表里
Prelude> [(1,'a'), (2,'b'), (3,'c')]
[(1,'a'),(2,'b'),(3,'c')]
it :: Num t => [(t, Char)]
你就得到了这个元组类型的列表。
有以上这些基本类型基本上就够我们在仙境里的使用了,哦忘了还有个布尔型:
Prelude> True
True
it :: Bool
Prelude> False
False
it :: Bool
Prelude> not True
False
it :: Bool
Prelude> True && False
False
it :: Bool
Prelude> False || True
True
it :: Bool
以上都是在程序员不主动写明类型的情况下,编译器自动推断的结果。 实际上我们是可以主动写明类型的:

ghci 不支持这样的语法,所以你需要建个文件,叫做 haskellfariyland.hs,然后在编辑器里输入这些内容。
函数
函数式编程,当然是以函数为核心的,所以我们要来学习 add 函数的4种写法!
第一种:
add :: (Int,Int) -> Int
add (x,y) = x + y
虽然 Haskell 具有很强的类型推断能力,但是习惯上每个模块的顶层函数定义和值定义都会声明类型,方便阅读代码和生成文档。而且先写函数的类型签名再写实现也是驱动我们思考程序设计的非常好的方法,这种开发方式被称为类型驱动开发(Type Driven Development —— 简称TDD)
函数式编程里的函数,就如同数学意义上的函数 y = f(x)
,函数就是把一个输入值转化成一个输出值的运算。
Prelude> add (1,2)
3
it :: Num a => a
这种写法的输入参数是一个二元组
,(Int,Int)
共同构成了一个值。返回值是 Int,所以这个函数的类型就如写下来的那样:(Int,Int) -> Int
第二种写法:
add' :: Int -> (Int -> Int)
add' x y = x + y
有了前面那个例子,现在这个函数签名应该也很容易理解吧?入参是一个 Int,返回值是一个 Int -> Int
的函数。 返回的这个函数可以再接受第二个参数,最后返回一个 Int 结果。 所以我们可以这么调用它:
Prelude> add' 1 2
3
it :: Num a => a
可能有聪明的同学已经会想到,如果我只传一个参数会怎样?当然是返回给你一个函数咯,你还可以给返回的函数一个名字,比如:
Prelude> let add1 = add' 1
add1 :: Num a => a -> a
Prelude> add1 2
3
it :: Num a => a
(再提醒一下,在 ghci 里定义一个名字需要用 let
关键字,在编辑器里不用。) 这种行为称为「柯里化」, 柯里化是非常有用的特性,我们以后会看到更多使用场景。返回的函数叫做「部分应用函数」(partially applied functions)
第三种写法就是著名的 lambda 写法
add'' :: Int -> (Int -> Int)
add'' = \x y -> x + y
用法和 add'
一样,实际上他们应该是编译成一样的结果的。 lambda 写法可以用来写「匿名函数」,比如说这个函数我就想用一次,不想给个名字,那么可以直接用 lambda 定义和使用:
Prelude> (\x y -> x + y) 1 2
3
it :: Num a => a
其实lambda更主要的作用是写一个函数作为参数传给另一个函数,后面讲高阶函数的时候大家就会知道。
关于第四种写法。。。大家 现在再回看一下第二种写法:
add' :: Int -> (Int -> Int)
add' x y = x + y
入参(形参x y)和一个Int对不上号~~~ 所以在学习了lambda写法后,我们可以写个对的上号的版本:
add''' :: Int -> (Int -> Int)
add''' x = \y -> x + y
add'''
函数的返回结果是个 Int -> Int
函数,所以我们在函数实现的 =
后面就用lambda来定义这个函数。 这个写法在后面写一个用来组合函数的combinator的时候很重要,请细心体会~
Prelude> add''' 1 2 --加载后在ghci里运行
3
it :: Int
以上总结了add函数的各种写法,有些很文艺,有些很二X,普通青年的写法是:
------------参数1---参数2---返回值
normalAdd :: Int -> Int -> Int
normalAdd x y = x + y
但是你要理解这是柯里化的效果。
我们的add函数有点挫,只能加整数,normalAdd 1.0 2.0
就会出错,我们修改一下函数签名,让它能加天下可加之物!
add :: Num a => a -> a -> a
add x y = x + y
前面已经解释过 Num a
是类型约束,小写的字母 a 是「类型参数」。和函数参数一样,类型参数的名字也是你任意起的,叫b、c、d,x、y都无所谓,只是必须小写字母。大写字母开头的代表具体类型,像Int就是个具体类型。 不要把a理解为Any,也不要完全按照Java的接口来理解,以为a被变成了Num。 实际上a捕捉了你调用函数式时给出的真实类型,并且做出了强类型的严格限定。a -> a -> a
这个简单的签名表明第二个参数的类型必须和第一个参数相同,结果类型也一样。
Prelude> let x = 1 :: Int
x :: Int
Prelude> let y = 2 :: Int
y :: Int
Prelude> add x y
3
it :: Int
Prelude> let x = 1.0 :: Float
x :: Float
Prelude> let y = 2.0 :: Float
y :: Float
Prelude> add x y
3.0
it :: Float
Prelude> let x = 1 :: Int
x :: Int
Prelude> let y = 2.0 :: Float
y :: Float
Prelude> add x y
<interactive>:18:7:
Couldn't match expected type ‘Int’ with actual type ‘Float’
In the second argument of ‘add’, namely ‘y’
In the expression: add x y
以上代码建议大家敲敲试试,并确保完全理解。
然后再介绍add的第5种写法~~~ Haskell是函数式编程语言,Haskell的世界里自然到处都是函数。比如 +
Prelude> :t (+)
(+) :: Num a => a -> a -> a
咦,这签名和我们的add一模一样啊? 其实这是当然的,你有没有发现我们的add实际上不就是调用+吗?只不过有一些规则让+可以作为中置运算符(infix)来使用.
既然我们的add函数就是标准库提供的+函数,所以最简单的实现当然就是
Prelude> let add = (+)
add :: Num a => a -> a -> a
Prelude> add 1 2
3
it :: Num a => a
最后留一个彩蛋,我们的add函数也可以中置使用哦:
Prelude> 1 `add` 2
3
it :: Num a => a