在理解函数式编程之前,先理解什么是结构化程序设计。
结构化程序设计
按照结构化程序设计的观点,任何算法功能都可以通过由程序模块组成的三种基本程序结构的组合: 顺序结构、选择结构和循环结构来实现。
这里得重点是只要语法能实现顺序结构、选择结构和循环结构。那这种语法就能搞定任何算法。
结构化程序设计的两种实践
第一种实践是我们熟知的if else(选择结构)、for while(循环结构)这些语法,顺序结构就是代码自上而下的执行。这通常被称为命令式语法。
第一种实践是函数式,它用自己得方式来实现选择结构和循环结构,顺序结构用函数之间调用来完成。这也被叫做声明式语法。
这里我们要清楚的概念是,命令式语法和函数式语法,都是结构化程序设计的实践。另一个搞清楚的问题是,要想写函数式编程,得抛弃if else / for / while来进行流程控制,因为这些都是属于命令式语法。
要想研究函数式编程,我觉得用JS来研究会很困难,初期会分不清哪些是函数式的语法,哪些是命令式的语法。需学习一个纯粹的函数式编程语言,才能更好的理解函数式。这里我选择来Haskell。
Haskell是纯函数编程语言型。这里我通过它来介绍函数式。
函数式编程语言中的选择结构
这里不求你看懂Haskell代码的全部语法。能了解这些语法的意图即可,这里说的语法,都是Haskell里怎么实现分支结构的。
模式匹配
函数式语言通过模式匹配能替代if else的功能。
-- 下面代码定义了一个lucky的函数。--是haskell中的注释,相当于js中的//
-- 第一行是函数签名,表示函数接受一个整数,返回一个字符串。
-- 第二行说如果入参是7,则匹配这行作为调用结果,返回"LUCKY NUMBER SEVEN!"
-- 注意Haskell的等号意思跟JS不同,不是赋值,等号只是函数声明的一部分
-- 函数名(入参)=(返回值)
-- 第三行表示如果如参不是7,就匹配这一句。
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
-- 调用函数luky的代码。lucky 7 会得到 "LUCKY NUMBER SEVEN!"。lucky 100 会得到"Sorry, you're out of luck, pal!" 注意这里函数调用没有括号包裹着参数,这点跟js语法不同。
lucky 7
lucky 100
复制代码
守卫
模式匹配可以搞定参数的类型不同,但参数类型相同,但值不同,就需使用守卫语法。
-- 这里依然先写函数签名,表示函数bmiTell接受一个float类型的参数,返回字符串。
-- 入参名字是bmi,如果bmi <= 18.5,就走第一条分支,以此类推。
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
复制代码
可见守卫可以承载分支逻辑。无论模式匹配还是守卫,都做到了分支结构,还让函数体的逻辑单一(单一职责)。比如bmi <= 18.5后面跟的函数体,只需写这一种情况下的逻辑。
函数体内的if else
模式匹配和守卫,都是函数级别的分支流程控制。下面看看函数体内的分支结构
-- 不是说没有函数式if else这样的命令式语法吗?这里怎么又有了。
-- 在函数级别,确实是没有if else语法的,但在函数内,有。注意这里的if then else跟JS中是有区别的,这里的if then else更像js中的三元表达式,相当于js中amount < 20 ? 0 : 20。
-- 这个函数的意思是:声明一个lend方法,有一个amount入参,如果amount < 20就返回0,否则返回20。
lend amount = if amount < 20
then 0
else 20
复制代码
表达式语句 vs 陈述语句:函数式中大部分语句都是表达式语句,也就是得有返回值的语句。而不是JS中(如const a = 10;)这种没有返回值的陈述句。
思考
在js中,没有模式匹配和守卫语法,我们似乎不可避免还是使用命令式的语法if else来实现分支结构。但如果你理解了函数式,就不会拘泥于手段。保证函数都是纯函数,虽然用if else也不制造副作用。
函数式编程语言中的循环结构
Haskell使用递归来实现循环。
写一个replicate函数,重复某元素固定次数,如调用 replicate(3,5) 返回 [5,5,5],意思为重复输出3次5。
Haskell冒号语法:1:[]返回[1]。 2: [1]返回[2,1]。冒号就是将一个值拼接到数组中。
-- 第一行函数签名,表示输入两个int类型的值,返回一个int数组。
-- 第二行声明函数replicate,两个入参重复次数n和重复元素x。
-- 第三行表示如果重复次数小于等于0,返回空数组。
-- 第四行中冒号是Haskell语法,将函数体分为x和replicate (n-1) x两部分,
-- replicate (n-1) x 这里就是递归调用了。这句话的整体意思是x和重复n-1次x拼接(那不就是重复n次x嘛)。
replicate :: int -> int -> [int]
replicate n x
| n <= 0 = []
| otherwise = x:replicate (n-1) x
复制代码
再看看在js中常用的数组map和filter,在haskell中同样使用递归方式实现:
-- map函数实现
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
-- filter函数实现
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
复制代码
到此,我们看到haskell虽然不用if else / for这样的命令式语句,同样可以完成流程控制。
数据
程序 = 数据 + 算法。
上面说的流程控制,是算法部分。一个语言要想有强大的能力,同样要能操作复杂的数据。
Haskell同样可以构建复杂的对象。
data Customer = Customer {
customerID :: CustomerID
, customerName :: String
, customerAddress :: Address
} deriving (Show)
复制代码
这说明函数式编程不止可以算‘数’,还可以算对象,让对象在函数之间流转,以达成某种目的。
functor
要理解functor,要从haskell的静态类型说起。
haskell中的数据有类型(Int, String等)的,函数也是由类型的(函数签名如:int -> int -> [int])。
有了类型,haskell又有个类型类的概念,我为了方便理解,可以把类型类理解成面向对象中的基类(事实上不是一回事),类型引用类型类(继承基类)后,就得实现类型类定义的方法。
functor就是有一个fmap函数的类型类。所有实现functor的类型都得实现自己类型的fmap函数。
class Functor f where
fmap :: (a -> b) -> f a -> f b
复制代码
函数式编程为啥要定义这样一个类型类呢?答案是有遍历需要的类型太多,试想这样的情况:
我有一个树形结构的数据:
data Tree a = ...
// 我要为其写遍历的函数treeMap
treeMap :: (a -> b) -> Tree a -> Tree b
treeMap 具体实现
复制代码
我有一个图形结构的数据:
data Graph a = ...
// 我要为其写遍历的函数graphMap
graphMap :: (a -> b) -> Graph a -> Graph b
graphMap 具体实现
复制代码
我要是有一百种数据结构,以此类推,写相应的***Map函数,这样就显得不统一,能不能让所有的map都用一个名字,只是各自类型的具体实现不同呢?最终达到如下效果,统一用fmap方法来遍历那些需要遍历的数据:
-- 定义length方法,它就类似于JS中map函数的那个参数方法,['foo', 'maa'].map(n => n.length)的 n => n.length部分,提供给下面的fmap函数。
length n = n.length
-- 下面这样调用 而不是 treeMap length treeData,length是被map的方法,treeData是被map的数据
fmap length treeData
-- 下面这样调用 而不是 graphMap length graphData,length是被map的方法,graphData是被map的数据
fmap length graphData
复制代码
可以看到,我们希望用fmap这种通用的方式遍历不同类型的数据,而不是每个类型都有自己的一个map函数的名字treeMap、graphMap、***Map。为达到此目的,haskell定义了类型类Functor,让有遍历需要的数据类型都引用(继承)Functor后完成自己的实现。以便有个统一的fmap函数。
函数式高明的地方在于:我刚才一直将f a中的f解释为Tree、Graph这种数据结构是不全面的,f代表的是函数,Tree、Graph也是一种函数,f还可以是Maybe、List等,更通用的说法是f代表一种上下文场景。
函数式编程的特点
这篇文章没有从函数式编程的特点开始说,如函数是第一公民、数据是不可变的、强制使用纯函数、函数只接受一个参数(柯里化)、惰性求值、。是因为要理解函数式,必从结构化程序设计谈起,意识到函数式和命令式是同级的东西。
再来看函数式编程中的这些特点,去理解他们。讲述函数式编程特点的文章很多,我在此就不重复啦。
函数式能完成一切吗
不能。 函数式要想跟键盘和屏幕交流,需要依赖有副作用的函数,用有副作用的函数将值传递给函数式范式里的纯函数,再将计算出的结果,在有副作用的函数里显示到屏幕上。
我们日常用的函数式库,也是如此,从命令式的语法里拿数据给函数式的库计算,得到的值,还是交给命令式的语法来继续后续操作。
面向对象 or 函数式
- 在接近业务方面的编程,使用面向对象范式,面向对象是一种对现实世界的抽象。
- 在接近数学公式、流式操作方面的编程,用函数式比较合适。
在前端编程中,面向对象和函数式是并存的。
前端的一些函数式
我们就来看看前端中存在的一些函数式思想。
-
JS数组操作:map、filter、reduce都是函数式的思路。
-
react中的函数式
- 高阶组件:是一个没有副作用的纯函数。(很函数式)
- Redux:Redux本身还有添加中间件的能力,都是函数式思想的体现。
-
Ramda: 函数式编程风格而设计的JS数据操作库。
-
Rxjs:函数式编程是 Rx 最重要的观念之一, Rxjs是一个非常强大的异步操作库。