基础语法_Haskell笔记1
一.简介
Haskell是一种纯函数式语言(purely functional programming language),其函数式特性的纯度没有争议
命令式语言要求你提供求解的步骤,Haskell则倾向于让你提供问题的描述
非函数式思维:通过命令告诉电脑要做什么,比如求和是通过循环结构遍历所有的数,相加并记录其和
函数式思维:通过函数来描述出问题是什么,比如求和是把第一个数与其余树的和相加
P.S.关于思维模式的差异,请查看一场函数式思维模式的洗礼
Haskell的特点:
变量不可变:函数式里的变量与常量概念一样,源自数学思维,令x=1,那么x永远都是1
引用透明:函数调用能被直接替换成相应的值,而不会影响函数的行为。即函数仅用来求值,没有副作用(不会影响外部状态),相同输入总能得到相同的输出
惰性求值:真正需要值的时候才现算,所以此时的一连串计算(函数调用)只是作用于输入数据的一系列变换公式,具体来看就是array.map().filter().reduce()只需要遍历array一遍,而不是3遍
静态类型:编译器会做静态类型检查,这没什么奇怪的,但还支持强大的自动类型推断,所以多数情况不必声明类型,这样既拥有了静态类型检查的好处,还保证了代码简洁程度
P.S.引用透明(Referential transparency)的详细解释:
An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program’s behavior. As a result, evaluating a referentially transparent function gives the same value for same arguments. Such functions are called pure functions. An expression that is not referentially transparent is called referentially opaque.
二.基本运算
负数与一元减号
-3
表示对数字3使用一元运算符-,求得其相反数-3。也就是说,-3的-不是数值字面量的一部分,而是个运算符
2 + -3
会得到报错:
cannot mix ‘+’ [infixl 6] and prefix `-‘ [infixl 6] in the same infix expression
二元运算符和一元运算符不能混用在同一个中缀表达式里,这会带来解析时的不确定性(有歧义,编译器不知道该怎样理解)。所以,经验原则是给所有负数字面量都带上括号,如(-3)
P.S.Haskell只有一个一元运算符,就是一元减号-,具体见Unary operator
逻辑运算
3个运算符:与(&&),或(||),非(not)
两个Bool字面量:True,False
相等性判断
==:判断等于,可以用于字符串
/=:判断不等于(数学家的语言,所以不等号长这样)
注意,类型必须严格一致才能比较,否则报错认为没有可比性(1 == True会报错),但认为整型与浮点型是可比的(1 == 1.0是True)
运算符优先级
在GHCi环境可以通过info:命令查看运算符优先级,例如:
> :i *class Num a where ... (*) :: a -> a -> a ... -- Defined in ‘GHC.Num’infixl 7 *> :i -class Num a where ... (-) :: a -> a -> a ... -- Defined in ‘GHC.Num’infixl 6 -
乘法比减法优先级高(分别是7和6),都是中缀函数(infixl的infix),都是左结合的(infixl的l表示left associative),函数签名也相同(Num a => a -> a -> a)
优先级的范围是0-9,值越大越优先
三.函数调用
语法格式
Haskell里的函数调用默认是前缀语法,例如:
succ 2min 1 (-2)
与Bash脚本的函数调用语法一样,函数名 参数1 参数2
但运算符作为特殊的函数,默认要以中缀形式调用,例如:
1 + 2
实际上,任意一个函数(包括运算符),都可以以前缀或者中缀形式调用,规则如下:
-- 前缀转中缀prefixFunc a ba `prefixFunc` b-- 中缀转前缀a infixFunc b(infixFunc) a b
例如:
1 `min` (-2)(+) 1 2dollar函数
$也是个函数:
($) ::forall (r :: GHC.Types.RuntimeRep) a (b :: TYPE r).(a -> b) -> a -> b -- Defined in ‘GHC.Base’infixr 0 $
优先级最低的中缀右结合函数,从签名来看,只是个函数调用符,相当于在右边加括号:
-- 求自然数的平方和加到第多少个时超过1000length (takeWhile (< 1000) (scanl (+) 0 (map sqrt [1..])))-- 等价于length $ takeWhile (< 1000) $ scanl (+) 0 $ map sqrt [1..]
优先级最低,不影响运算,只调整运算顺序:
> max 5 3 * 2 + 111> max 5 $ 3 * 2 + 17
简单地把$理解成做括号的替代品是不合适的,比如:
> 3 * $ 5 - 2 + 1<interactive>:21:5: error: parse error on input ‘$’ Perhaps you intended to use TemplateHaskell
换个姿势:
> (3 *) $ 5 - 2 + 112
因为$是个中缀函数,要求左边是函数,右边是其参数
P.S.还有一个很有意思的东西:($ 2) sqrt,中缀函数柯里化的小把戏
柯里化
Haskell函数默认都是柯里化的,都只接受一个参数:
In Haskell, all functions are considered curried: That is, all functions in Haskell take just one argument.
例如:
> :t (/)(/) :: Fractional a => a -> a -> a> :t (/ 2)(/ 2) :: Fractional a => a -> a
其中,(/ 2)是对(/) :: Fractional a => a -> a -> a函数的不全调用(partially applied),所以没有得到计算结果,而是返回了函数(/ 2) :: Fractional a => a -> a
P.S.(-)函数比较特殊,因为(- 2)表示负2,而不返回一个新函数,非要的话,用(subtract 2)
可以通过curry与uncurry函数进行柯里化或去柯里化:
-- uncurry去柯里化> :t uncurry (/)uncurry (/) :: Fractional c => (c, c) -> c> (uncurry (/)) (4, 2)2.0-- curry柯里化> :t curry (uncurry (/))curry (uncurry (/)) :: Fractional c => c -> c -> c> (curry (uncurry (/))) 4 22.0
注意:多参函数要以元组形式传参,例如f (4, 2)
利用柯里化特性时还需要注意参数顺序,例如:
> (/ 2) 42.0> (2 /) 40.5
偏函数应用
偏函数应用(partial application)与柯里化(currying)的最大区别是对参数数量的影响,从调用函数求值的角度来看,柯里化并不改变参数数量,而偏函数应用会减少参数数量,因为预填了几个,例如:
fn (a, b) = a + bcurriedFn = curry fnpartialFn = curriedFn 2// 调用函数求值fn (2, 3)-- 加上括号让结合性更清楚一些(curriedFn 2) 3partialFn 3
所以,二者的联系是,可以通过柯里化函数来创建偏函数(partialFn = curriedFn 2)。区别是目的不同,偏函数应用是为了减少函数所需参数数量(通过固定一些参数值),柯里化是为了把一个多参函数转换成单参函数,这个单参函数返回另一个单参函数(参数数量不足),或者求值(参数数量够了)
四.函数声明
doubledouble x = double (double x)double x = x * 2isEven x = x - x `div` 2 * 2 == 0x `mod'` y = x - (x `div` y) * y
形式与函数调用差不多,函数名加空格分隔的参数列表,=后面是函数体
2个特点:
声明顺序无所谓
函数名首字母不能大写,不能数字开头
P.S.数学里把相似的东西用x x' x''的命名习惯表示,在Haskell里也可以这样做:
y x = x ^ 2y' x = x ^ 2 + 1
另外,中缀形式转换在函数声明中也可以用:
x `mod'` y = x - (x `div` y) * y
一些场景下能够提升函数声明的可读性
无参函数
常量可以理解成无参函数,例如:
> :t 22 :: Num t => t
或者更生动的例子:
-- 无参函数,就是consttwo = 1 + 1
匿名函数
匿名函数即函数表达式,在Haskell中称之为lambda。语法格式如下:
反斜线 + 参数列表 -> 函数体
例如:
sum' = \x y -> x + yP.S.类似于JS的const sum = (x, y) => x + y
从应用场景来看,lambda的特点是:
用完即扔:不要求复用
足够简单:自解释,单从函数体一眼就能看明白其功能
例如:
map (\x -> x + 1) [1, 2, 3]map (\([x, y]) -> x + y) [[1, 1], [2, 2], [3, 3]]
但很多时候并不需要显式地通过lambda语法来造函数,可以充分利用柯里化、List Comprehension等特性:
map (+1) [1, 2, 3][ x + y | [x, y] <- [[1, 1], [2, 2], [3, 3]]]
另外,有个有趣的东西:
addThree = \x -> \y -> \z -> x + y + z-- 因为haskell自带currying,所以等价于-- addThree x y z = x + y + z
P.S.匿名函数中的->与类型声明中的->语义相同,都表示“映射到”(maps to)
函数组合
数学中的函数组合的表达方式是f·g(x) = f(g(x)),Haskell与之类似:
fg = f . g
用到的运算符是.:
(.) :: (b -> c) -> (a -> b) -> a -> c -- Defined in ‘GHC.Base’infixr 9 .
右结合,所以f . g . h x等价于f (g (h x)):
((/7) . (*2) . (+3)) 4
函数组合可以用来制造新函数,并且能够把参数抽出来,例如:
-- f x = 2 * (sqrt x) + 1-- 对应的pointfree stylef = (+ 1) . (* 2) . sqrt
通过组合把内层参数抽离出来,并利用柯里化特性去掉。这种只通过函数组合得到的,不涉及实际参数的函数风格被称为pointfree style
P.S.注意,巨长的函数链会降低可读性,不鼓励这样做,应该通过let/where等声明把函数链拆开并赋予语义
五.条件语句和模式匹配
条件语句
-- if then elsegt3 x = if x > 3 then True else False
注意:if-then-else完整结构,else部分不可省略
有趣的是,if语句也是表达式,所可以这样做:
lt10 x = if x < 10 then True else Falsegte10 x = not (if x < 10 then True else False)
模式匹配
模式匹配是基本的函数调用机制,例如:
sayOneTwoThree :: (Integral a) => a -> StringsayOneTwoThree x = "Not between 1 and 3"sayOneTwoThree 1 = "One!"sayOneTwoThree 2 = "Two!"sayOneTwoThree 3 = "Three!"
调用函数时会按声明顺序匹配参数类型,所以上面的sayOneTwoThree 2只会返回"Not between 1 and 3"
再比如利用模式匹配递归求阶乘:
fact 0 = 1fact n = n * fact (n - 1)
注意,如果模式匹配失败,就会报错:
mod10 0 = 0mod10 1 = 1
-- 如果最后不用万能匹配兜住,mod10 2就会报错
-- mod10 x = x `mod` 10
匹配失败时:
> mod10 2*** Exception: t.hs:(27,1)-(28,11): Non-exhaustive patterns in function mod10
提示mod10函数的模式定义有遗漏
除了用于函数参数定义,模式匹配还可以用于List Comprehension和let-in表达式、where子句等场景,例如:
[ x + y | (x, y) <- [(1, 2), (3, 4)] ]sumOneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c
常用的模式匹配技巧如下:
-- 拆开list首元与尾巴,要求length >= 1,否则报错x:xs-- 拆开list前两个元素与尾巴x:y:ys-- 拆分同时保留原引用xs@(x:y:ys)P.S.@保留原引用称为as模式
Guard
一个简单的guard模式示例:
plus'' a b | a > 0, b > 0 = "sum is a positive value" | a < 0, b < 0 = "sum is a negative value" | otherwise = "what?"
参数列表后面多了| 条件表示不同的函数体分支,被调用时满足条件就执行对应函数体并返回,否则就按顺序依次向下检查
注意,最后的otherwise比较有意思,因为:
> :i otherwiseotherwise :: Bool -- Defined in ‘GHC.Base’> otherwise == TrueTrue
所以otherwise只是语义需要,直接用True作为默认分支的条件也可以
P.S.单行形式也是合法的(但可读性差,不建议用),例如:
isPositive n | n > 0 = True | otherwise = False
where关键字
where关键字跟在guard后面,用来定义函数声明中需要用到的变量,及辅助函数
checkArea r | area < little = addSpace "little circle" ++ wrap sArea | area < normal = addSpace "normal circle" ++ wrap sArea | otherwise = addSpace "big big circle" ++ wrap sArea where area = pi * r ^ 2 -- !必须对齐,有点傻 --little = 10 --normal = 50 -- where中可以用模式匹配 (little, normal) = (10, 50) sArea = show area -- 可以定义函数 addSpace s = ' ' : s -- where可以嵌套,在辅助函数中定义辅助函数 wrap s = wrapLeft (wrapRight s) where wrapLeft s = " " ++ s wrapRight s = s ++ " "
where子句的几个特点:
多行声明必须对齐缩进,否则编译器无法正确解析(不知道要定义的变量/函数列表结束了没)
子句中声明的变量和函数的作用域是当前函数及其guard,且不包括同名函数的其它模式
子句中可以用模式匹配
允许嵌套使用,辅助函数也可以在自己的where子句中声明需要的变量和辅助函数
注意,where是一种语法结构,用来在函数底部声明变量/函数,作用域是包括guard在内的整个函数
P.S.非要单行的话,可以用分号隔开多个声明,例如:
sayHello = hello ++ " " ++ greeting where hello = "Hi"; greeting = "girls"
let关键字
语法格式:
let [bindings] in [expressions]
例如:
cylinder r h = let sideArea = 2 * pi * r * h bottomArea = pi * r ^ 2 in sideArea + 2 * bottomArea-- 表达式形式一般用法类似于js解构赋值oneTwoThree = let (a, b, c) = (1, 2, 3) in a + b + c
let-in的作用与where类似,都用来定义局部变量/函数,区别如下:
形式上:let xxx in...与...where xxx的声明位置区别,let把定义放在前面了
语法上:let-in是表达式,而where是语法结构,前者可以随便放
作用域上:let-in的作用域限制更严格,在let部分定义的变量/函数只对in部分可见
注意,同样要求多行声明要严格对齐,非要单行就用分号隔开
P.S.let-in的in部分可以省略,作用域扩展到当前函数/List Comprehension,如果是在GHCi环境,在整个交互过程都可见
Case表达式
最常见的case表达式就是函数定义时参数的模式匹配(case表达式的语法糖):
tail' [] = "empty list"tail' [x] = "no tail"tail' (_:xs) = show xs等价于tail'' xs = case xs of [] -> "empty list" [x] -> "no tial" (_:xs) -> show xs
语法格式如下:
case expression of pattern -> result pattern -> result pattern -> result ...
用expression依次尝试匹配pattern,匹配成功就执行对应的代码块并返回结果,否则尝试下一个,都不匹配就报错
P.S.同样,作为表达式,case-of可以用于任何地方,比模式匹配灵活得多(模式匹配只能用于函数声明、where、let、List Comprehension等特定场景)
六.数据结构
List
Haskell中的List是单一类型数组,例如:
emptyArr = []numbers = [1, 2, 3, 4]chars = ['a', 'b', 'c']
实际上,字符串就是Char类型元素的List,例如:
> str = "okay"> :i strstr :: [Char] -- Defined at <interactive>:51:1> charArr = ['o', 'k', 'a', 'y']> :i charArrcharArr :: [Char] -- Defined at <interactive>:54:1
所以,字符串字面量只是个List语法糖
List操作
++连接:
> "Is life always this hard" ++ " ,or is it just when you are a kid ?""Is life always this hard ,or is it just when you are a kid ?"-- 或者> ['A', 'l', 'w', 'a', 'y', 's'] ++ [' '] ++ "like this.""Always like this."
注意,++操作会遍历左边的List,所以性能不高
:左端插入:
> 0 : [1, 2, 3][0,1,2,3]
另外,[1, 2, 3]实际上是1 : 2 : 3 : []的语法糖,:右结合得到完整List
!!按索引取元素:
> "hello there" !! 4'o'> "hello there" !! 14*** Exception: Prelude.!!: index too large
索引从0开始,越界会报错
多维数组
> [[1], [2, 3]] !! 1 !! 13
允许锯齿数组,同样要求元素类型一致
List比较
如果List中元素可比较,则List可比较(遍历比较各元素):
> "hello" == ['h', 'e', 'l', 'l', 'o']True> "0110" > "0101"
True
常用函数
-- head取首元> head [1, 2, 3]1-- tail取尾巴(去首元)> tail [1, 2, 3][2,3]-- last取尾元> last [1, 2, 3]3-- init取身子(去尾元)> init [1, 2, 3][1,2]
注意:如果List为空,这4个函数会报错
-- length求数组长度> length [1, 2]2-- null判空> null []True-- reverse首尾转置> reverse [1, 3, 2][2,3,1]-- take取前n个元素> take 5 [1, 2][1,2]-- drop去掉前n个元素> drop 5 [1, 2][]take/drop越界不会报错-- maximum求最大值> maximum [1, -2, 3 , 0]3-- sum求和> sum [1..10]55-- elem判断包含性> 3 `elem` [1, 2]False
其中[1..10]是Range语法,elem以中缀形式调用更符合语义习惯
Range
一种用来生成可枚举List的便捷方式,例如:
> [1..7][1,2,3,4,5,6,7]> ['A'..'Z']"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
允许通过前两项来指定间隔(step),可以是负间隔(递减序列):
> [1, 3..7][1,3,5,7]> [10, 9..1][10,9,8,7,6,5,4,3,2,1
浮点数存在精度的问题,不建议在Range中使用:
> [0.1, 0.3..1][0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]
另外,还允许无限序列,例如:
-- 不设上限的Range> take 3 [1..][1,2,3]-- 或者cycle函数无限重复> take 7 (cycle [1..3])[1,2,3,1,2,3,1]-- 或者repeat生成单元素无限重复的List> take 6 (repeat 3)[3,3,3,3,3,3]-- 上一句结果等价于> replicate 6 3[3,3,3,3,3,3]List Comprehension
列表推导,是指从既有List按照一定规则产生另一个List。所以需要map, filter等操作的场景都可以用List Comprehension来完成
语法形式与数学集合定义类似,比如用集合描述10以内的偶数为S = {2 * x | x <- N, x <= 5},对应的List Comprehension语法如下:
> [ 2 * x | x <- [1..5] ][2,4,6,8,10]
P.S.用<-表示属于符号,非常形象
还可以添加更多限制条件(predicate):
> [ 2 * x | x <- [1..5], 2 * x `mod` 3 == 1, x `mod` 5 /= 0 ][4]
除了加限制条件过滤,还可以通过let声明变量/函数:
> [ doubleX | x <- [1..5], let doubleX = 2 * x, doubleX `mod` 3 == 1, x `mod` 5 /= 0 ][4]
P.S.省略in部分的话,声明的变量/函数对其右侧限制条件和变量,以及竖线左侧部分可见。带上的话,仅作用于当前条件
复杂一点的,比如求1到100的所有素数:
isPrime n = null [ x | x <- [2..n-1], n `mod` x == 0 ][ x | x <- [1..100], isPrime x ]
看起来与数学公式没什么区别,isPrime的判定规则是n无法被2..n-1中的任何一个数整除,1到100中所有满足该判定规则的元素组成的集合即为所求
像集合定义一样,元素可以来自多个既有集合,例如:
> [ x * y | x <- [1, 2, 3], y <- [4, 5] ][4,5,8,10,12,15]
可以利用List Comprehension自己实现个length函数:
length' arr = sum [ 1 | x <- arr ]-- 或者更符合习惯的length' xs = sum [ 1 | _ <- xs ]
P.S.其中_表示占位符,不关心其值是什么,算一种地道的编码习惯
另外,List Comprehension是可以嵌套的:
-- 滤掉二维数组中的奇数[ [ x | x <- xs, even x ] | xs <- [[1,2], [3, 4]] ][[2],[4]]Tuple
元组不要求单一元素类型,从类型约束来看,相当于结构体
例如:
> :t (1, "Leon")(1, "Leon") :: Num t => (t, [Char])-- List要求类型单一,所以把二元组和三元组放到一个List里会报错> [(1, "Leon"), (2, "Milk"), (3, "Little", "Girl")]<interactive>:148:28: error: • Couldn't match expected type ‘(t, [Char])’ with actual type ‘(Integer, [Char], [Char])’
与List一样,如果元组中的元素可比较,那么同类型元组也可以比较
复杂一点的例子,求所有三边长度皆为整数且小于等于10,周长为24的直角三角形:
[ (a, b, c) | c <- [1..10], b <- [1..c], a <- [1..b], a + b + c == 24, a^2 + b^2 == c^2 ]
注意其中隐含的边长关系:c >= b >= a,算作去重规则
常用函数
fst/snd取二元组的首元/尾元:
fst (1, 2)snd (1, 2)
注意,这两个函数只能用于二元组。一般元组没有类似的工具函数,但可以通过模式匹配来自己实现:
-- 取三元组首元first (x, _, _) = xzip从List组合出元组:> zip [1, 2] ['A', 'B', 'C'][(1,'A'),(2,'B')]
多余的单身元素会被丢掉
参考资料
Infix Functions In Haskell
Currying
Currying versus partial application (with JavaScript code)
4.4.2 Fixity Declarations:运算符优先级表
更多相关文章
- Java 中的构造函数引用和方法引用
- 函数式编程中如何处理副作用?
- 帆软报表自定义函数-取json数据
- 函数和递归
- java的getClass()函数
- 函数的学习
- java多线程(3)Thread构造函数解析
- JavaScript 测试教程–part 3:测试 props,挂载函数和快照测试[每日
- 深入理解 JavaScript 回调函数 [每日前端夜话0xDF]