一.简介
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:运算符优先级表

更多相关文章

  1. Java 中的构造函数引用和方法引用
  2. 函数式编程中如何处理副作用?
  3. 帆软报表自定义函数-取json数据
  4. 函数和递归
  5. java的getClass()函数
  6. 函数的学习
  7. java多线程(3)Thread构造函数解析
  8. JavaScript 测试教程–part 3:测试 props,挂载函数和快照测试[每日
  9. 深入理解 JavaScript 回调函数 [每日前端夜话0xDF]

随机推荐

  1. Python英语 - Issue16
  2. 思科dhcp和链路捆绑
  3. 2019年1-5月文章汇总 | Python数据之道
  4. Android中的UI线程详解
  5. 可视化神器 Plotly Express 合并到 Plotl
  6. JVM的新生代跟老年代
  7. 用Python快速分析和预测股票价格
  8. 机器学习爱好者必读的入门指南
  9. 不变的就是变化本身(Vue学习笔记one)
  10. 用Python分析5187位CSDN博主数据,顺便把昨