函数式编程Haskell初探

简介
🤔想想本科CS教育大多都教的是什么?

算法: 在给你一个问题, 要你尽快算出解.

数据库: 给你一些数据, 要你快速储存查找.

分布式算法/GPU编程: 一个CPU不够用, 给你更快的硬件(集群或者GPU), 要你更快算出解

人工智能课: 写出指数增长的搜索算法, 然后再用剪枝, 学习等方法加速.

体系结构课: 是在用电路门造出更快的CPU.

为什么我们一个劲去优化机器, 程序员难道不重要吗? 随着代码规模的增大, 重构, 调试, 测试, API设计变得越来越复杂. 于是人们提出了函数式编程. 函数式编程不关心代码的逻辑执行速度(复杂度). 程序员只负责将问题描述给计算机, 而速度优化则一口气交给计算机处理.

⚠️注意: Haskell并没有在工业界流行. 这意味着你很难将Haskell应用于大型项目(虽然Haskell具有这样的能力)

🔡Haskell是一门纯粹函数式编程语言

🏎函数式与命令式编程对比

🏃‍♂️执行操作:

命令式编程: 给计算机一系列指令, 计算机根据指令执行变量状态变化. 最后得到结果

函数式编程: 告诉计算机我们需要解决什么样的问题

例如: 获取字符串s​中的大写字母

命令式编程: 遍历s​ - 如果字符c​满足′A′≤c≤′Z′​ - 将c​放入数组res​ - 返回res​

函数式编程: 我要获得一个字符串 - 这个字符串中的字符来自s​ - 只有大写字母满足要求 - 大写字母指的是′A′,′B′,…,′Z′​

不难发现, 我们可以很难将命令式编程中的语句转化为数学函数(比如遍历的for就无法转换为函数), 但是可以很轻易的将函数式编程语句的内容转化为数学表达式

f(s)={x|x∈s,x∈Caps} where Caps={′A′,′B′,…,′Z′}
他的Haskell表达式也很数学化(暂时看不懂也没有大碍)

f s = [x|x<-s, x elem [‘A’…‘Z’]]
回头想想, 我们经常把命令式编程语句中的function称作"函数". 但是这些"函数"内部却总是有数学函数无法实现的内容(例如循环, 变量重复赋值). 所幸函数式编程解决了这个问题

🙅变量与常量

命令式编程: 常量一旦声明就无法变化, 而变量可以随时重新赋值

函数式编程: 变量一旦被指定, 就不可以更改了. 函数能做的唯一事情就是利用引数计算结果(毕竟数学函数中可没有重复赋值的操作, 但数学中可到处都是复合函数)

💊副作用(side effect, 即改变非函数内部的状态)

命令式编程: 函数可能存在副作用(修改外部变量值)

函数式编程: 无副作用, 且函数式编程中的函数是纯函数(即: 以同样的参数调用同一个函数两次, 得到的结果一定是相同)

⏱惰性求值

命令式编程: 除非使用特殊数据结构, 默认非惰性求值, 例如在JS中写下

[1,2,3].map(d=>d+1).map(d=>d/2);
解释器会对数组遍历两次

函数式编程: 默认惰性求值, 例如在Haskell中写下

map (/2) (map (+1) [1,2,3])
数组只会遍历一次, 即每一个元素都调用函数两次, 最后得到结果. 好像有点问题: 如果我想定义一个cnt, 并让cnt在每次执行加法/乘法的时候+1, 最后加cnt到结果上呢? 即

let t = [1,2,3];
let cnt = 0;
for(let i=0; i<t.length;i++){
t[i]+=1;cnt++;
t[i]/=2;cnt++;
t[i]+=cnt;
}
Haskell的惰性求值似乎会让这样的函数无法实现? 但是还记得副作用吗, Haskell中的函数都是纯函数, 纯函数的执行不能对外部产生副作用. 也正是因为函数都是纯函数, 所以惰性求值时候将元素经常连续变化并不会造成结果存在差异.

惰性求值的另一个好处是我们可以处理一个无限数组例如: 获取前10个奇数

[1…] – 获取一个[1,2,3…]的无限数组
filter (odd) [1…] – 过滤出所有奇数 [1,3,5…]
take 10 filter (odd) [1…] – 取前10项
🚫Haskell是静态强类型语言

静态类型意味着我们需要在运行前明确指出变量的类型, 同时Haskell支持类型推导, 这意味着我们不必告诉Haskell每一个变量的类型(例如Haskell会自动推断a = 1+1的a是数值, 同时由于Haskell不可重复赋值, a的类型不会再有变化)

强类型意味着Haskell不会自动进行类型转换(除了部分语法糖)

🛠最方便的方法就是使用Haskell Platform, 此程序包包含

GHC: Haskell编译器

cabal-install: 包管理器

stack: 跨平台开发工具

haskell-language-server: 语言支持

对于archlinux, 由于GHC采用动态链接, 需要增加几个软件

pacman -S ghc cabal-install stack haskell-language-server happy alex haskell-haddock-library
对于VSCode用户

Haskell Extension Pack

Simple GHC (Haskell) Integration

Haskell GHCi Debug Adapter Phoityne

GHC在编译Haskell文件(.hs文件)的同时提供了交互模式(类似Node, Python, 虽然他是编译型语言(这里应该感谢纯函数的特性)), 只需要终端输入ghci即可进入交互模式. 在交互模式中

:l xx.hs – 可以加载xx.hs文件, 其中.hs可以省略
:r – 刷新已经加载的文件
基础语法
数学运算: 2 + 15, 49 * 100, 1892 - 1472, 5 / 2, 50 * (100 - 4999)

注意: 5 * -3会报错, 因为在Haskell中函数是一等公民, 而*本身就是一个二元函数. Haskell会将表达式解析为( 5 * - ) 3, 所以应该改为5 * (-3)

Boolean运算: True, False(必须大写), &&, ||, not, ==, /=(即!=)

数学运算与Boolean运算都不支持默认类型转换(整数支持默认转换为小数)

函数调用: 调用方法为函数名 参数1 参数2…, 看起来很怪, 没有(), 也没有,分隔. 例如

succ 8 – 获取8的后继, 即9
min 9 10 – 8,9最小值
max 9 10 – 8,9最大值
succ 9 + max 5 4 + 1 – 函数调用具有最高优先级, 即(succ 9) + (max 5 4) + 1
中缀函数: 对于二元函数, 我们可以将f x y写成x f y, 注意, 这里的`, 是必须的. 例如

2 min 4 – 即 min 2 4
1 elem [2,3,1] – 即elem 1 [2,3,1], 其中elem x xs返回x是否在xs中
函数调用是自左向右的

函数定义与数学中的函数表达式很类似, 例如

需要定义表达式doubleMe(x)=x+x​, 只需要

doubleMe x = x+x
需要定义表达式doubleUs(x,y)=x+x+y+y​, 只需要

doubleUs x y = x+x+y+y
当然, 也可以调用函数

doubleUs x y = doubleMe x + doubleMe y
变量就是常函数(因为变量不可修改值, 所以, 可以像构建常函数一样构建变量)

testValue = 12
无需关心函数之间的位置, 例如

doubleUs x y = doubleMe x + doubleMe y
doubleMe x = x + x
demoRes = demoUs 1 2
并不会报错

条件语句If: if-then-else结构, 例如

doubleSmallNumber x = if x > 100
then x
else x*2
支持压行书写

使用_表示我们不关系这个变量取值, 例如定义函数

f(x,y,z)=xg(x,y,z)=yg(x,y,z)=z
Haskell表示就是

f x _ _ = x
g _ y _ = y
h _ _ z = z
这和JS中_表示不关心变量不一样, 这个甚至可以重名

在Haskell中使用’表示类似, 但是不同的函数, 比如我们想使用两种方式实现Fibonacci​

fib n = if n<=2 then n else fib (n-1) + fib (n-2)
忽然我们又想实现一个Fibonacci​

fib’ n = if n<=3 then n else fib (n-1) + fib (n-2)
这只是一种命名习惯, 不强制, 也没有其他效果

📜这里的List和JS/Python的数组类似, 我喜欢把他作为可重复无序集合使用.

声明一个List很简单

t = [1,2,3]
⚠️List中的元素类型必须相同

字符串实际上是字符List的语法糖

“231” == [‘2’,‘3’,‘1’] – True
可以使用++运算合并List, 例如

t = [1,2,3] ++ [4,5,6] – [1,2,3,4,5,6]
⚠️其实现原理是遍历++前的数组并合并到后者, 所以这是一个低效运算子

可以使用:运算符将元素加入List头部, 例如

t = 1:[2,3,4] – [1,2,3,4]
支持链式调用, 例如

t = 1:2:3:[4,5,6] – [1,2,3,4,5,6]
可以使用!!取List的某一位

t = [1,2,3,4,5,6] !! 2 – 3
⚠️越界访问会报错

取值方法

head List返回首个元素: head [1,2,3]为1

tail List返回非首个元素们: tail [1,2,3]为[2,3]

last List返回最后一个元素: last [1,2,3]为3

init List返回非最后一个元素: init [1,2,3]为[1,2]

⚠️对空数组执行均会报错

其他方法

length List返回数组长度: length [1,2,3]为3

null List返回是否为空: null [1,2,3] 为 False

reverse List反转数组: reverse [1,2,3]为[3,2,1], 并不会反转原数组(因为纯函数)

take n List返回前n的元素, 越界部分不返回, n==0返回[]

take 2 [1,2,3,4] – [1,2]
take 10 [1,2,3,4] – [1,2,3,4]
take 0 [1,2,3,4] – []
take -1 [1,2,3,4] – Error!
drop与take类似, 作用为删除前n个元素

maximum List返回最大值: maximum [1,9,2,3,4]为9

maximum List返回最大值: minimum [8,4,2,1,5,6]为1

sum List返回和: sum [8,4,2,1,5,6]为26

product List返回积: product [8,4,2,1,5,6]为1920

elem ele List判断ele是否在List中: 4 elem [3,4,5,6]为True

📜类似于Python的Range, 但是更加智能

t = [1…5] – [1,2,3,4,5]
t = [‘a’…‘f’] – [‘a’,‘b’,‘c’,‘d’,‘e’,‘f’]
– 默认Step为1, 自定义时需要列前两项
t = [1,1.2…2] – [1.0,1.2,1.4,1.5999999999999999,1.7999999999999998,1.9999999999999998]
– 但是精度堪忧, 建议使用其他方法(后面会提到)
t = [1…] – [1,2,3…]定义无限长List
t = [1,3…] – [1,3,5,7…]
repeat n返回无限个n组成的List(等价于[n,n…])

t = repeat 5 – [5,5,5,5…]
一般搭配take使用

🔰非常类似于集合的定义(这也是我把List当无序可重集合的原因)

对于一个集合

S={2x|x∈N,x∈{1,…,100}}
首先他是一个List, 所以应该包着[], 之后有一个竖线分隔符, 左边是输出函数(集合中的代表元素), 右边是约束, 例如[x|条件], 条件中∈​使用<-表示, 那么刚刚集合就可以表示为t = [ 2*x | x <- [1…100], (sqrt x) elem [1…100]]

还可以结合之前的函数与if语句, 例如:

定义List它能够使List中所有大于 10 的奇数变为 “BANG”,小于 10 的奇数变为 “BOOM”,其他则统统扔掉

boomBangs xs = [ if x < 10 then “BOOM!” else “BANG!” | x <- xs, odd x]
同时支持同多List中取元素

[ x*y | x <- [2,5,10], y <- [8,10,11]] --[16,20,22,40,50,55,80,100,110]
🤔使用comprehension的时候注意思考方式: 我需要的List是什么样子的, 而不是List是怎么算出来的

📜这里的Tuple和Python的元组类似. 与List不同的就是: Tuple是定长的, 其中可以为任意不同数据类型(例如(‘a’,1))

⚠️Tuple也是有类型的, 这意味着若List中有Tuple, 那么所有的Tuple类型应该相同(每个Tuple的长度相同, 每一个位置的类型相同), 即

[(1,2,3), (4,5,6)] – 👍
[(1,2,3), (4,5,True)] – 💩
[(1,2,3), (4,5)] – 💩
[(1,2), (4,5.0)] – 👍 同时你将获得[(1,2.0), (4,5.0)]
方法:

fst Tuple获取二元Tuple的第一个元素, 不可用于其他长度Tuple!

snd Tuple获取二元Tuple的第二个元素, 不可用于其他长度Tuple!

zip List List获取一个交叉配对的Tuple List

zip [1,2,3,4,5] [5,5,5,5,5]
– [(1,5),(2,5),(3,5),(4,5),(5,5)]
zip [1 … 5] [“one”, “two”, “three”, “four”, “five”]
– [(1,“one”),(2,“two”),(3,“three”),(4,“four”),(5,“five”)]
这个zip函数确实很形象啊😂, 同时若两个List长度不一样的, 则舍弃长出的部分(拉拉链的时候要是两边不一样长也只能拉到较短的位置), 这种特性与惰性求值组合后zip就可以处理无限数组了

zip “Karry” [1…]
– [(‘K’,1),(‘a’,2),(‘r’,3),(‘r’,4),(‘y’,5)]
zipWi1th f List List: 与zip类似, 将每次取得的两个元素调用f并返回

add x y = x + y
zipWith add [1 … 10] [1 … 10]
– [2,4,6,8,10,12,14,16,18,20]
😆有趣的例子: 还是要注意思考方式

所有三边长度皆为整数且小于等于 10,周长为 24 的三角形

triangles = [ (a,b,c) | c <- [1…10], b <- [1…10], a <- [1…10], a+b>c, a+c>b, b+c>a]
三边都小于等于 10 的直角三角形(三边按顺序输出)

triangles’ = [ (a,b,c) | c <- [1…10], b <- [1…c], a <- [1…b], a^2 + b^2 == c^2]
周长为24, 三边都小于等于 10 的直角三角形

triangles’’ = [ (a,b,c) | c <- [1…10], b <- [1…c], a <- [1…b], a^2 + b^2 == c^2, a+b+c == 24]
类型与类型类
Haskell是静态类型语言且支持类型推导. 但Haskell不支持隐式类型转换(除了Int->Float)

可以在ghci中使用:t 表达式的方式获取类型

:t ‘a’ – ‘a’::Char
:t True – True::Bool
:t “HELLO!” – “HELLO”::String
:t max – max :: Ord a => a -> a -> a
:t [1,2,3] – [1,2,3] :: Num a => [a]
:t 12.3 – 12.3 :: Fractional p => p
:t (True, 1) – (True, 1) :: Num b => (Bool, b)
:t () – () :: Eq a => a -> a -> Bool
🙄常见的类型有

Int: 表示−231∼231−1​的整数

Integer: 表示整数, 无界

Float: 单精度浮点数

Double: 双精度浮点型

Bool: 布尔型, 取值为True与False

Char: 字符型, String = [Char]表示字符串

🙄类型表示时的术语

使用大写字母开头表示类型

::表示"类型为", 例如: "HELLO"的类型为String

[a]表示a类型的数组

对于函数, 将参数与返回值类型依次使用->连接即可, 例如

a->b表示这是一个函数, 接受一个a类型的参数, 返回一个b类型变量

a->b->c->d表示这是一个函数, 按顺序接受a,b, c类型变量, 返回d类型变量

将参数与返回值类型简单粗暴的连接在一起看起来有点"欠考虑", 实际上, 这样的模式在函数式编程中十分符合直觉

当函数可以接受多种类型的参数并返回不同类型的类型时, 我们一般采用a, b, c…表示某一种类型, 这与命令式语言中的多态类似, 例如reverse函数: [a] -> [a]

运算符也是一个函数, 例如类型就是一个a->a->Bool, 不过在进行类型判断应该使用括号将运算符括起来, 如:t ()

Tuple的类型是每一项的类型组成的Tuple

至今没有解决的=>表示什么, 这需要类型类的知识

🎁前面提到, 我们可以通过使用a, b等变量表示任意类型, 例如sum函数表示[a]->a, 此时的a就是类型变量, 例如

fst函数: [a] -> a

length函数: [a] -> Int

div函数: a -> a -> a

此时, div函数似乎有点问题, 我们只用a代表了某一种类型, 但是Char类型能除吗? 我们应该将类型变量限定到一定类型范围, 例如div函数的a应该是一个可计算类型, 用:t检查div函数

ghci> :t div
div :: (Integral a) => a -> a -> a
这里的(Integral a)用来描述a这个类型是一个Integral类型类(括号表示省略). 注意描述: 类型变量a是一个Intergral类型类的类型变量, 在描述结束时候使用=>链接类型声明

🪆有点套娃的意思了. 将这些术语与命令式编程对应一下.

函数的参数与返回值可能是多种类型的(对应多态)

于是我们将每种类型用不同的类型变量表示(对应模板, 用类型变量代表某一个类)

为了约束类型变量, 我们提出了类型类. 那什么样的类属于某个类型类呢?

完成了类型类中定义的成员与方法的类都可以属于类型类(对应接口).

🌰看几个常见的例子

Eq类型类表示可以表示相等的类型类. Eq类型类要求实现==函数以用于判断.

例如:t ()类型为() :: Eq a => a -> a -> Bool

Ord类型类表示可以比较类型类, Ord类型类要求实现<, >, <=, >=函数.

:t min类型为min :: Ord a => a -> a -> a

Show类型类表示可以转换为字符串的类型类, Show类要求实现show函数用于转换为字符串

例如: :t show类型为show :: Show a => a -> String

例如: show 123表示"123", show [1,2,3]表示"[1,2,3]"

Read类型与Show类型相反. read函数可以将字符串转换为Read类型类的成员

例如: :t read类型为read :: Read a => String -> a

但是: 将String转换为Read类型类中哪个类型呢, 比如"True"应该转换为字符串还是布尔呢

可以使用Haskell自带的类型推导: read “123” + 1得到124

可以使用Haskell类型声明手动指定: read “123” :: Float得到123.0

Enum类型类的成员都是可枚举的. 其成员实现了succ(后继子)与pred(前继子)方法. Bool, Char, Ordering, Int, Integer, Float, Double类型都术语该类型类

例如: :t succ类型为succ :: Enum a => a -> a

Bounded类型类的成员都有上限与下限

:t minBound类型为minBound :: Bounded a => a, 例如: minBound :: Int为-9223372036854775808

:t maxBound类型为maxBound :: Bounded a => a

Num为数字类型类

Integral: 表示整数, 包含Int 和 Integer

当我们想显式将Integral转化为Num时, 可以使用fromIntegral函数

⚠️Integer与Integral区别

Floating: 表示浮点数, 包含Float 和 Double

⚠️如果一个类型属于多个类型类可以这样写

函数
Haskell有一套独特的函数语法

模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据. 这在函数定义中很常用

👂听起来和字符串正则匹配很像. 在定义函数时可以这样写

lucky :: (Integral a) => a -> String
lucky 7 = “LUCKY NUMBER SEVEN!”
lucky x = “Sorry, you’re out of luck, pal!”
在调用 lucky 时, 模式会从上至下进行检查, 一旦有匹配, 那对应的函数体就被应用了. 这个模式中的唯一匹配是参数为7,如果不是7,就转到下一个模式,它匹配一切数值并将其绑定为 x . 若是自上而下检查所有模式都没有命中, Haskell会报错. 所以在使用模式匹配的时候务必要考虑边界条件与特殊值(这与你在数学表达式中考虑边界值一样重要)

一个实现阶乘的例子

factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
🤔看起来模式匹配只是用于类似数学中递归定义的一个语法糖?(简化了switch-case)

👻并不是, 模式匹配还有高级用法(我更喜欢把他理解为JS正则中if(regExp.test()){args = regExp.exec()}的语法糖或者是Object结构赋值的语法糖)

实现一个二维向量相加

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
用模式匹配写后

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
在定义函数的时候我就将参数与传入值进行了匹配

实现一个List的reverse(注意实现思路与模式匹配的应用)

reverse’ :: [a] -> [a]
reverse’ (x : xs) = reverse’ xs ++ [x]
reverse’ [] = []
x : xs: 表示匹配一个List, 将这个List的第一个元素设置为x, 剩下的设置为xs

例如: 使用其匹配的时候[1,2,3]就会匹配为1:[2,3], 于是x = 1, xs = [2,3]

⚠️使用这样的方式匹配数组时需要加上括号表示他们是一体的

reverse函数是什么呢? 就是把数组的第一个元素放到最后, 在前面加上反转后的剩下的元素

什么时候会匹配失败呢? 当参数是空数组的时候就取不出来头, 于是设置一个边界值

实现一个List的head函数

head’ :: [a] -> a
head’ (x:_) = x
head’ [] = error “empty list”
我们并不关心模式匹配时首元素后面的元素, 那么可以用_代替

实现一个快速排序

qsort :: (Ord a) => [a] -> [a]
qsort (target:xs) = [x|x<-xs, x<=target] ++ [target] ++ [x|x<-xs, x>target]
qsort [] = []
这是一个经典的例子, 快速排序是什么, 就是随便这一个元素, 把比他小的排序后放在左边, 比他大的排序后放在右边

还可以在匹配时使用@语法保留对整体的引用

capital :: String -> String
capital “” = “Empty string, whoops!”
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
💂🏽guard用来检查一个值的某项属性是否为真. 听起来和路由守卫一样, 如果条件判断通过就放行. 例如计算BMI函数:

bmiTell :: (RealFloat a) => a -> String
bmiTell bmi – 注意这里没有等号
| bmi <= 18.5 = “underweight” – 等号在这里
| bmi <= 25.0 = “Pffft” – 与if-else一样只会匹配第一个通过的
| bmi <= 30.0 = “fat”
| otherwise = “whale” – 最后可以使用otherwise兜底
👀看起来是个语法糖: |和if-else-if一样, otherwise和兜底else一样, 但是用在此处相当简洁.

⚠️如果使用Guard且没有使用otherwise且全部匹配失败, Haskell会匹配下一个函数, 例如:

f :: (Ord a, Num a) => a -> [a]
f x
| x < 0 = error “make sure x >= 0”
| x == 0 = [0]
f x = x : f (x - 1)
如果x>0, f x会先进入第一个函数, 两个guard都匹配失败了, 于是进入下一个模式匹配

改进一下BMI, 要求用户输入身高与体重👇

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = “underweight”
| weight / height ^ 2 <= 25.0 = “Pffft”
| weight / height ^ 2 <= 30.0 = “fat”
| otherwise = “whale”
与命令式语言一样, 我们想把weight / height ^ 2定义成变量, 可以使用where关键字

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= 18.5 = “underweight”
| bmi <= 25.0 = “Pffft”
| bmi <= 30.0 = “fat”
| otherwise = “whale”
where bmi = weight / height ^ 2
就像写数学公式一样

f(h,w)={underweightBMI≤18.5Pffft18.5<BMI≤25fat25<BMI≤30whale30<BMI where BMI=w/h2
where后面可以跟多个名字和函数定义

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = “You’re underweight, you emo, you!”
| bmi <= normal = “You’re supposedly normal. Pffft, I bet you’re ugly!”
| bmi <= fat = “You’re fat! Lose some weight, fatty!”
| otherwise = “You’re a whale, congratulations!”
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
⚠️注意

where 绑定中定义的名字只对本函数可见, 其中的名字都是一列垂直排开

where 绑定也可以使用模式匹配, 前面那段代码可以改成:

where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)
where可以嵌套使用

与where类似, 作用域不同. where绑定在函数底部, 在所有guard内可见, 但let只对let-in绑定的in表达式可见, 例如

cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea
与命令式编程的case类似, 同样支持模式匹配

case expression of pattern -> result
pattern -> result
pattern -> result

例如

describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of [] -> “empty.”
[x] -> “a singleton list.”
xs -> “a longer list.”
不使用反引号也可以定义中缀函数. 但是, 函数名只能使用:|!@#$%^&*-+./<>?~, 之后可以使用下面任意方式定义

a |+| b = method1
(|+|) a b = method1 a b
(|+|) = method1
例如

infixr 9 op
infi*定义结合性
infixr是右结合, infixl是左结合, infix无左右优先性.

数字定义优先级
优先级一共有十个, 0-9, 数字越大越高, 如果定义时省略了数字, 则默认为9. 预定义的有

值 左结合 无结合 右结合
9 !! .
8 ^, ^^, **
7 *,/,div
6 +, -
5 :, ++
4 ==,/=,<,<=,>,>=,elem,notElem
3 &&
2
1 >>, >>=
0 ,​!,seq
递归
🪆使用模式匹配与递归可以优雅的实现递归. 在实现递归时最需要关注的就是边界条件. 而递归的的实现思路就是描述问题是如何定义的

实现List的max函数

命令式思路: 设一个变量来存储当前的最大值,然后用循环遍历该 List,若存在比这个值更大的元素,则修改变量为这一元素的值

函数式思路: List的最大值就是head和tail最大值的最大值. 空List的最大值为Error

maximum’ :: (Ord a) => [a] -> a
maximum’ [] = error “maximum of empty list”
maximum’ [x] = x
maximum’ (x:xs)
| x > maxTail = x
| otherwise = maxTail
where maxTail = maximum’ xs
实现replicate n x函数(将x重复n次)

replicate’ :: (Num i, Ord i) => i -> a -> [a]
replicate’ n x
| n <= 0 = []
| otherwise = x:replicate’ (n-1) x
⚠️这里使用Guard而不是模式匹配是因为模式匹配无法匹配<0

实现take函数

take’ :: (Num i, Ord i) => i -> [a] -> [a]
take’ 0 _ = []
take’ _ [] = []
take’ n (x:xs) = x : take’ (n-1) xs
更加周全的的代码(同样因为我们要匹配n<0的情况, 所以不能用模式匹配了)

take’ :: (Num i, Ord i) => i -> [a] -> [a]
take’ n _
| n <= 0 = []
take’ _ [] = []
take’ n (x:xs) = x : take’ (n-1) xs
实现repeat函数

repeat’ :: a -> [a]
repeat’ x = x:repeat’ x
实现zip函数

zip’ :: [a] -> [b] -> [(a, b)]
zip’ [] _ = []
zip’ _ [] = []
zip’ (x1:xs1) (x2:xs2) = (x1,x2):zip’ xs1 xs2
实现elem函数

elem’ :: (Eq a) => a -> [a] -> Bool
elem’ e [] = False
elem’ e (x : xs) = (e == x) || elem’ e xs
但是有点不函数式, 改一改

elem’ :: (Eq a) => a -> [a] -> Bool
elem’ e (x : xs)
| e == x = True
| otherwise = elem’ e xs
elem’ e _ = False
温习一下快速排序(并使用where让其看起来更像函数式)

qsort :: (Ord a) => [a] -> [a]
qsort (target:xs) = lowers ++ [target] ++ uppers
where lowers = qsort [x|x<-xs, x<=target]
uppers = qsort [x|x<-xs, x>target]
qsort _ = []
⚠️思路: 定义边界条件, 再定义个函数, 让它从一堆元素中取一个并做点事情后, 把余下的元素重新交给这个函数

实现埃筛

primes = filterPrime [2…]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x mod p /= 0]
这种生成器生成+验证器验证的模式值得学习

实现斐波那契

fib :: Int -> [Int]
fib n = take n $ fibList [1, 1]
where
fibList [a, b] = a : fibList [b, a + b]
注意学习如何存储递归中需要的调用值

函数Pro
😕函数式编程与数学表达式看起来太像了.

👼于是我天真的以为函数式编程就是用数学的方式描述问题, 然后将其表示为函数式编程语句.

🤔实际上函数式编程更加注重将函数作为"一等公民", 从而操作函数或是函数的一部分解决问题

在JS中经常能听到这个函数柯里化这个词语, 在JS中, 柯里化就是把多参函数变成单参函数, 并返回一个单参数函数用于吃下下一个参数.

🍬但是, Haskell中所有函数都只有一个参数, 所有函数都是柯里化函数. 而多参函数只是一个语法糖!

😱拿max函数举例. max函数实际上只接受一个参数x, 然后返回一个和x比较大小的函数,例如

comp x y = x y – 接受x,y 返回x y的结果
maxWith5 = max 5 – 返回一个max 5函数
res = zipWith comp (repeat maxWith5) [1 … 10]
– [5,5,5,5,5,6,7,8,9,10]
也就是说maxWith5就是一个函数, 函数接受一个参数, 返回和5比较大的那个, 也就是我们之前写的max 5 6可以写成(max 5) 6

再看看maxWith5, 试试写出他的类型:Ord a => a -> a. 显而易见, 接受一个Ord类型类的a类变量, 返回一个a类变量. 而之前那个max函数呢? 收到一个a类变量, 返回一个Ord a => a -> a类函数. 试试写出max函数类型: Ord a => a -> (a->a)这个括号没啥用(因为Haskell是自左向右解析的)于是简化成Ord a => a -> a -> a这也就解释了为什么把参数类型与结果用->连在一起是符合直觉的

⚛像max 5这样的函数调用就是不全调用, 而中缀函数也存在不全调用, 例如elem [1…], ==4, *5

🌌高阶函数: 接收函数作为参数或返回函数的函数就是高阶函数, 例如

applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
这个类型似乎有点特别, 多了一个括号, 表示第一个参数是一个函数而不是类型a(因为Haskell是右结合的)

结合函数柯里化与不全调用, 我们可以写出这样的表达式

applyTwice (+3) 10 – 16 (调用函数子 😲
applyTwice (++ " HAHA") “HEY” --“HEY HAHA HAHA”
applyTwice ("HAHA " ++) “HEY” – “HAHA HAHA HEY”
ghci> applyTwice (multThree 2 2) 9 – 144
ghci> applyTwice (3:) [1] – [3,3,1]
结合函数子可以实现多种炫酷的操作. 这就是把函数当成对象用

实现一个zipWith, 体验一下无参数的不全调用

zipWith’ :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith’ f (x : xs) (y : ys) = f x y : zipWith’ f xs ys

zipWith’ (+) [4,2,5,6] [2,6,2,3] --[6,8,7,9]
zipWith’ max [6,3,2,1] [7,3,1,5] – [7,3,2,5]
zipWith’ (++) ["foo ", "bar ", "baz "] [“fighters”, “hoppers”, “aldrin”]
– [“foo fighters”,“bar hoppers”,“baz aldrin”]
zipWith’ () (replicate 5 2) [1…] – [2,4,6,8,10]
zipWith’ (zipWith’ (
)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]]
– [[3,4,6],[9,20,30],[10,12,12]]
可以借助高阶函数实现命令式中的for、while、赋值、状态检测

flip是一个常用高阶函数, 实现功能很简单

flip :: (a -> b -> c) -> b -> a -> c
flip f y x = f x y
也就是传入一个二元函数, 传回一个接受参数相反的二元函数(注意: 传回的是函数而不是函数的运行结果!)

⚠️flip经常用来对库函数进行改进, 例如我需要函数

pushFont :: [a]->a->[a]
pushFont xs x = x:xs
要是函数参数能反过来就好了, 于是我就可以写

pushFont :: [a]->a->[a]
pushFont = flip (😃
不需要添加参数, 就算固执的添加上了参数, 函数只是变成这样

pushFont :: [a]->a->[a]
pushFont x xs = flip (😃 x x

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liukairui

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值