函数式编程是神马
惯例,先提问题:
- 什么是函数式编程
- 为了解决什么问题
- 怎么解决的
- Scala实现函数式编程的方法
背景故事
函数式编程出现的背景是摩尔定律在今天逐渐失效,多核多节点并发才是未来的主流发展方向。但是并发天生带有线程不安全,状态维护能力差,加锁阻塞的弊病。函数式编程不存在这个问题,因为从原则上保障同函数相同结果,不共享变量这个情况。
尝试回答问题:
- 函数是数据的映射关系,函数作为编程范式中的角色之一,可以作为一个变量被传递,并且支持高阶函数,偏函数,柯里化等符合函数特性的语法;
- 函数式编程是一种解决问题的思路,虽然不是唯一解,但是在高并发,函数映射关系较为复杂的场景中,解决方案较为“优雅”的编程范式;
- 函数式编程有一些原则,例如“不变量”,“纯函数”等,保障了高并发场景下的线程安全。
“函数式编程”是一种“编程范式”(programming paradigm),也就是如何编写程序的方法论。
函数的定义方法
def methodName (参数名:参数类型, 参数名:参数类型) : [return type] = { // 方法体:一系列的代码 }
// 示例
def funcDemo(a:Int,b:String):Unit= {
// func body
// 没有return关键字,最后一行的值就是return值
}
复制代码
函数关键词:def
函数名称:funcDemo
案例中入参:a:Int,b:String
出参类型:Unit
常见函数类型
高阶函数
高阶抽象函数:高阶函数是指以函数对象作为参数的函数。
例如Map(),Filter()等函数都是高阶函数。
val map_test = Array(1,2,3,4,5)
var tmp=map_test.map((i:Int) => i * i)
println("处理后的列表:" + tmp.mkString(","))
/**
处理后的列表:1,4,9,16,25
*/
复制代码
匿名函数
函数体:箭头左边是参数列表,右边是函数体。
println("开始匿名函数测试:")
// 定义一个计算器,传参是3个,两个数值,一个计算方法,计算方法计算两个值的结果
def col(i:Int,j:Int,func:(Int,Int)=>Int):Int= func(i,j)
// 传参一个匿名函数
println("匿名加法:" + col(1,2,(x:Int,y:Int) => x + y))
/***结果
开始匿名函数测试:
匿名加法:3
*/
复制代码
偏函数
当创建偏应用函数时,Scala内部会创建一个新类,它有一个特殊的apply()方法。
偏函数就是多个参数时,套一个壳,固定了部分参数?
示例中,add2就是add函数的偏函数
def add(a:Int,b:Int):Int= a + b
def add3(i:Int):Int = add(3,i)
def add2(i:Int):Int = add(2,i)
println("参数+2:" + add2(5))
println("参数+3:" + add3(5))
/***
结果
参数+2:7
参数+3:8
*/
复制代码
函数特性
闭包
闭包是一个函数的功能,返回值依赖于声明在函数外部的一个或多个变量。
闭包通常来讲可以简单的认为是可以访问一个函数里面局部变量的另外一个函数。
它引用到函数外面定义的变量,定义这个函数的过程是将这个自由变量捕获而构成一个封闭的函数。
var factor = 3
// 定义闭包函数,“closure”是一个变量,该变量类型是函数,返回一个匿名函数,应用于参数 * factor变量
val closure = (i : Int) => i * factor
复制代码
柯里化
柯里化解决的是多参数问题,当函数的参数足够多,维护成本较高是,目前的解决方法主要有以下三种:
- 保持多参数函数(硬刚)
- 传参为List型:容易有不安全问题
- 柯里化(Currying)
柯里化定义:
是指原来接受两个参数的函数变成新的接受一个参数的函数的过程。
新的函数返回一个以原有第二个参数为参数的函数。
示例:
// 正常多参数函数
def add (a:Int, b:Int) = a + b
// 柯里化之后的函数,每次传入一个参数,返回是新的函数
def add(a:Int) = (b:Int) => a + b
/**
第一步先返回一个匿名函数,第二个函数返回两个函数的叠加值
*/
// 也支持多参数定义柯里化函数
def add(a:Int)(b:Int) = a + b
// 调用方法
add(3)(4)
复制代码
为什么函数需要柯里化?
柯里化理论上非必须, 实际上是必须的. 柯里化本质是用单参数的函数拼出多参数的函数, 这样拼出来的函数有个特点就是只接受部分参数时是另外一个函数. 这对函数式语言来说太重要了, 可以简化一大堆东西。
而且是惰性计算的基础。
惰性计算
关键词:lazy
lazy是scala中用来实现惰性赋值的关键字,被lazy修饰的变量初始化的时机是在第一次使用此变量的时候才会赋值,并且仅在第一次调用时计算值,即值只会被计算一次,赋值一次,再之后不会被更改了。
注:lazy修饰的变量必须同时是val修饰的不可变变量。
val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val past = dateFormat.format(new Date())
lazy val now = dateFormat.format(new Date())
// 休眠10秒钟
Thread.sleep(10000)
println("休眠前的时间:" + past)
// 当调用参数变量时,才真正发起获取值,时间差10s
println("休眠后的时间:" + now)
/**
休眠前的时间:2021-09-10 15:38:23
休眠后的时间:2021-09-10 15:38:33
*/
复制代码
函数式编程原则
- 函数是”第一等公民”
指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
高阶函数中有所体现。
- 只用”表达式”,不用”语句”
“表达式”(expression)是一个单纯的运算过程,总是有返回值;”语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
只关心计算,不关心“动作”。
- 函数应该是纯粹的(纯函数)
纯函数:一个函数在程序的执行过程中除了根据输入参数给出运算结果之外没用其他影响,就可以说是没有副作用的,我们就可以将这一类函数称之为纯函数。
纯函数最核心的目的是为了编写无副作用的代码,它的很多特性,包括不变量,惰性求值等等都是为了这个目标。
函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
- 不能修改传递给函数的变量
- 不能修改全局变量
- 对于同样的输入参数,返回值总是相同的
- 不修改状态
要有常量值,不做不同线程之间的状态维护,降低多线程语境下的程序复杂度。
不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。
因此:尽量不用for循环,使用递归(内存溢出问题,采用尾递归解决)
函数和方法的区别
- 函数是一个对象(变量),方法是对象的一个组成部分,无法独立存在;
- 方法是隶属于类或者对象的,在运行时,它是加载到JVM的方法区中;
- 函数定义不需要使用
def
定义;- 可以将函数对象赋值给一个变量,在运行时,它是加载到JVM的堆内存中;
- 函数是一个对象,继承自FunctionN,函数对象有apply,curried,toString,tupled这些方法,方法则没有。
题外话
在这篇 博客 中读到这段话挺有感触,技术并不一定要追新,有一些本质性的思想是长久不变的,这些内功心法更值得我们投入时间,例如信息时代的《信息论》、《系统论》、《控制论》等核心思想。
深谙核心,懂得边界,做T型的技术人,共勉:
最近几年,很多新火起来的概念,但它们其实早在上世纪就已经被发明出来,无论时机器学习,深度学习,Python语言,还是函数式编程。这是为什么呢?这是因为这些技术的边界发生变化,或者说这个时代的技术边界变了。
每个时代都有每个时代的技术边界,真正的工程师会知道边界在哪里,只有外行才会无法无边。巴菲特说他不投资自己不懂的东西,正是因为他给自己的划定了一个边界。
苹果公司能够成功的一个重要原因正是因为它清楚得知道时代的边界,并且能在边界内做到最好。你看苹果很多产品都具有划时代的意义是吧,但其实那些产品都不是苹果首创,比如智能手机,最早是日本公司 DOCOMO 发明,个人平板电脑是英国首先发明。IPod,MP3 也是韩国先出品的。苹果公司用的很多技术甚至在 30 年前就有了,但为什么直到被发明出来才为人们所知?
正是因为苹果了解时代的技术边界,并在边界内做到最好。
往小了说,当我们在学习新的技术,或是使用新技术完成某项工作的时候,我们一定要直到它的边界在哪里。往大了说,我们应该像苹果一样,多多思考这个时代的技术边界在哪里,这样才不至于陷入无休止的技术追赶之中。