函数式编程(FP)是基于一个简单又意义深远的前提的:只用纯函数来构建程序。这句话的深层意思是,咱们应该用无反作用的函数来构建程序。什么是反作用呢?带有反作用的函数在调用的过程当中不只仅是只有简单的输入和输出行为,它还干了一些其它的事情。而且这些反作用会把影响扩散到函数外,好比:java
举个简单的例子:git
public class BookStore {
//书店的丛书,省略初始化过程
public Map<String, Book> collection;
public Book buyABook(CreateCard lc, String bookCode){
if(!collection.containsKey(bookCode)){
return null;
}
Book book = collection.get(bookCode);
lc.charge(book.getPrice());
return book;
}
}
复制代码
buyBook方法的的做用是根据图书编号从书店中买一本书,这个方法是一个反作用函数。由于买书的过程当中会涉及一些外部操做的,例如须要和库存管理系统进行交互、须要经过web service联系支付公司进行支付等操做。而咱们的函数只不过是返回了一本书,获取书的过程当中发生了一些额外的行为,这些行为咱们就称为“反作用”。程序员
为何会把这些行为称为“反作用”呢?由于这些行为的做用域不仅仅是属于书店系统的范畴的了,它会把影响扩散到其它的系统中。反作用会致使这段代码很难测试,由于咱们测试这段代码的时候,会影响到其它系统,牵一发而动全身。而且,客户端(调用这段代码的地方)没办法为所欲为的调用这段代码,在使用的时候,还要考虑反作用带来的具体影响,避免把系统带入异常状态。反作用让咱们的代码使用、维护、测试、修改都更麻烦。github
函数式编程的最重要也是最基础的知识点是:经过纯函数构建程序。web
在文章的最后,会给出一些思路解决上面例子中存在的反作用问题编程
咱们在使用函数式编程时,最新接触的概念通常是闭包、Lambda表达式等,这两个概念表达的是一样的意思。Lambda表达式是一种语法糖,它能帮助咱们写出更简洁,更容易理解的程序。因此在开始函数式编程以前,咱们要先掌握Lambda表达式究竟是什么,它的原理是什么。设计模式
下面先看一段你们很是熟悉的代码:缓存
//匿名函数
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread 1");
}
}).start();
//Lambda表达式
new Thread(() -> System.out.println("thread 2")).start();
复制代码
线程调度器运行线程时,线程的run方法会被执行,线程的调度原理这里不作介绍,有兴趣的读者能够自行了解。安全
上面两段代码的做用是同样的,它们的做用都是:当线程被调度时,就执行run方法中的代码。能够看到,使用Lambda表达式会让咱们的代码更简单,更容易理解,这一点在Kotlin上会更明显。bash
Kotlin版本:
Thread{
print("kotlin thread")
}.start()
复制代码
Kotlin版本语法结构更简单
Lambda和匿名函数它们有什么不同呢?答案是,没有任何不一样的地方。在JVM的角度来讲,它们是如出一辙的。不管是Java8仍是Kotlin甚至是全部运行在JVM平台上的语言,它们的原理都是同样的,Lambda表达式只是匿名函数的一种高糖写法。那么什么样的匿名函数能用Lambda呢?答案是:只有一个方法的接口,也能够说是只有一个方法的匿名函数。
咱们能够看看Runnable这个接口的定义:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
复制代码
这里咱们要注意一下@FunctionalInterface
这个注解,这个注解的意思是,这是一个函数接口。函数接口的做用是,代表这个接口能使用Lambda表达式的风格来实现。这个注解是Java8后引入的,只起到提示做用。
咱们再来看看Kotlin的一些Lambda的基础类是怎么样的:
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
复制代码
咱们观察下Java的一些函数式接口,和Kotlin的一些能使用Lambda的接口就能发现,在JVM上,全部的Lambda都是经过只有一个方法的接口来实现的。例如Android中常用的View#setOnClickListnner
方法。
咱们可使用Kotlin写一个简单的demo体验下:
fun main(args: Array<String>) {
delay(2000) { println("delay: $it ms hello word") }
}
fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
action(ms)
}
复制代码
当咱们运行main函数时,会打印出:delay: 2000 ms hello word。咱们重点关注下调用代码:
delay(2000) { println("delay: $it ms hello word") }
复制代码
不是特别了解FP范式的同窗能够这样理解:延迟2000毫秒后,咱们就打印"delay: $it ms hello word"
。
如今再回顾下线程调用的例子:当线程被调度时,就执行run方法中的代码。
Android中的click事件的监听:当view被点击时,就执行onClick方法中代码。
因此当咱们大量使用Lambda时,其实会大大加强咱们代码的可读性的,由于它们均可以这样去理解:当x发生时,咱们就执行y行为,这会比匿名函数容易理解不少。
如今,再来看看咱们再Java中以匿名函数的方式调用delay方法时,代码是怎么样的:
public class Main {
public static void main(String[] args) {
MainKt.delay(2000, new Function1<Long, Unit>() {
@Override
public Unit invoke(Long aLong) {
System.out.print("delay: " + aLong + " ms hello word")
return Unit.INSTANCE;
}
});
}
}
复制代码
Kotlin中定义delay方法中的action(注: fun delay(ms: Long, action: (Long) -> Unit) )变量在Java中会被编译成了Function1接口,这和上述讨论的函数式接口的结论一致。
这段代码,一眼看下去很难理解它究竟是作什么的。繁琐、难以理解、不够优雅。因此当咱们使用FP范式编程时,建议大量使用Lambda表达式。固然,若是有人特别喜欢匿名函数的话,也是能够以FP的方式来使用匿名函数的,在Java7的环境中使用RxJava就会有相似的体验。(笔者不太推荐大家虐待本身,若是匿名函数里面的代码有几十行的话,画面太美我不敢看)
这里总结一下,若是大家的项目比较保守,须要追求稳定的话,尽可能把大家的语言升级到Java8,若是激进点的话,能够直接升级到Kotlin。好的语法糖,能大大增强咱们代码的可读性的。
使用高阶函数是会带来一些性能损失的,由于每一个Lambda表达式都是一个对象。从上文的分析可知,在JVM中,Lambda表达式实际上是经过函数接口实现的。因此咱们在使用lambda的时候,就至关于new了一个新对象出来。并且因为Lambda在访问外部变量时,会捕获变量的缘由,捕获变量也会带来必定的内存开销。若是咱们大量使用Lambda的时候不想带来这些影响的话,咱们可使用内联函数来解决这些问题。
内联函数时如何解决这些问题的呢?咱们能够尝试下把上面的delay
函数改形成内联函数的形式。
fun main(args: Array<String>) {
delay(2000) {
println("delay: $it ms hello word")
}
}
inline fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
action(ms)
}
复制代码
咱们如今知道了,若是delay
不是内联函数的话,编译器会把上面的代码编译成new函数接口对象的形式。而内联函数的调用代码会编译成下面的形式:
fun main(args: Array<String>) {
Thread.sleep(2000)
println("delay: $2000 ms hello word")
}
复制代码
上面的代码不就是咱们一开始编写的非函数式代码吗?没错。经过内联函数,咱们能够通知编译器,让编译器帮咱们作这种脱糖处理。这样作的好处是,避免了大量使用lambda表达式致使对象大量建立和lambda捕获致使的性能开销。若是咱们能合理使用内联函数,咱们的应用会在性能上有所提高。
内联函数会致使编译生成的代码量变多,因此咱们要合理使用避免内联过大的函数。举个简单的例子,若是咱们的delay函数里面有100句代码的话,那么代码就会变成下面这个样子。
fun main(args: Array<String>) {
Thread.sleep(2000)
//假设还有一百句代码
println("delay: $2000 ms hello word")
}
inline fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
//假设还有一百行代码
action(ms)
}
复制代码
这样看起来问题好像不大,可是若是delay函数在项目中被大量调用的话,这将是一场灾难(想象下若是有100处调用,内联致使的代码增量是 100 * 100 -100)。合理使用内联函数才能带来性能的提高。
熟悉JVM编译器的小伙伴会对内联这个词很是熟悉。这里的内联函数的原理和JVM中内联的原理是同样的,有兴趣的读者能够了解JVM的内联优化下。
篇幅有限,这里只对内联函数的做用与原理做个简单的介绍。读者有兴趣的话,能够自行查阅JVM内联优化的相关资料。
函数式编程中,咱们用得最多的就是集合操做。集合操做的链式调用比起普通的for循环会更直观、写法更简单。下面咱们以一个简单的例子来学习下函数式的集合操做。
如今假设有一个书店在线销售商场,他的初始化代码以下:
fun initBookList() = listOf(
Book("Kotlin", "小明", 55, Group.Technology),
Book("中国民俗", "小黄", 25, Group.Humanities),
Book("娱乐杂志", "小红", 19, Group.Magazine),
Book("灌篮", "小张", 20, Group.Magazine),
Book("资本论", "马克思", 50, Group.Political),
Book("Java", "小张", 30, Group.Technology),
Book("Scala", "小明", 75, Group.Technology),
Book("月亮与六便士", "毛姆", 25, Group.Fiction),
Book("追风筝的人", "卡勒德", 30, Group.Fiction),
Book("文明的冲突与世界秩序的重建", "塞缪尔·亨廷顿", 24, Group.Political),
Book("人类简史", "尤瓦尔•赫拉利", 40, Group.Humanities)
)
data class Book(
val name: String,
val author: String,
//单位元,假设只能标价整数
val price: Int,
//group为可空变量,假设可能会存在没有(不肯定)分类的图书
val group: Group?)
enum class Group{
//科技
Technology,
//人文
Humanities,
//杂志
Magazine,
//政治
Political,
//小说
Fiction
}
复制代码
咱们先尝试用命令式的风格获取Technology类型的书名列表。
fun getTechnologyBookList(books: List<Book>) : List<String>{
val result = mutableListOf<String>()
for (book in books){
if (book.group == Group.Technology){
result.add(book.name)
}
}
return result
}
复制代码
若是咱们要使用函数式的风格来实现这个功能的话,能够经过filter与map函数来实现。
fun getTechnologyBookListFp(books: List<Book>) =
books.filter { it.group == Group.Technology }.map { it.name }
复制代码
这两段代码输出的结果都是同样的,咱们能够把返回的列表打印出来看看。
[Kotlin, Java, Scala]
复制代码
能够看出来,若是用函数式的风格,代码会比使用for循环更容易理解,而且更简洁。上面的函数式代码实现的功能一目了然:先过滤出group 等于 Technology的书本,而后把书本转换成书本的名字。
这个例子简单的介绍了filter和map的做用
那么咱们面对复杂一点的功能的时候又如何呢?
如今有一个简单的需求: 把书按照分组分类放好。若是咱们用命令式编程风格的话,咱们会写出相似下面的代码:
fun groupBooks(books: List<Book>){
val groupBooks = mutableMapOf<Group?, MutableList<Book>>()
for (book in books){
if (groupBooks.containsKey(book.group)){
val subBooks = groupBooks[book.group] ?: mutableListOf()
subBooks.add(book)
}else{
val subBooks = mutableListOf<Book>()
subBooks.add(book)
groupBooks[book.group] = subBooks
}
}
for (entry in groupBooks){
println(entry.key)
println(entry.value.joinToString(separator = "") { "$it\n" })
println("——————————————————————————————————————————————————————————")
}
}
复制代码
那咱们再看看,若是要用函数式的方式来实现一下这段函数要怎样写呢?咱们可使用操做符groupBy实现这个功能
fun groupBooksFp(books: List<Book>){
books.groupBy { it.group }.forEach { (key, value) ->
println(key)
println(value.joinToString(separator = "") { "$it\n" })
println("——————————————————————————————————————————————————————————")
}
}
复制代码
咱们运行这两个方法看看这两段函数的输出结果:
由于是输出没有区别,因此只贴一段结果
Technology
Book(name=Kotlin, author=小明, price=55, group=Technology)
Book(name=Java, author=小张, price=30, group=Technology)
Book(name=Scala, author=小明, price=75, group=Technology)
——————————————————————————————————————————————————————————
Humanities
Book(name=中国民俗, author=小黄, price=25, group=Humanities)
Book(name=人类简史, author=尤瓦尔•赫拉利, price=40, group=Humanities)
——————————————————————————————————————————————————————————
Magazine
Book(name=娱乐杂志, author=小红, price=19, group=Magazine)
Book(name=灌篮, author=小张, price=20, group=Magazine)
——————————————————————————————————————————————————————————
Political
Book(name=资本论, author=马克思, price=50, group=Political)
Book(name=文明的冲突与世界秩序的重建, author=塞缪尔·亨廷顿, price=24, group=Political)
——————————————————————————————————————————————————————————
Fiction
Book(name=月亮与六便士, author=毛姆, price=25, group=Fiction)
Book(name=追风筝的人, author=卡勒德, price=30, group=Fiction)
——————————————————————————————————————————————————————————
复制代码
能够看到,函数式的实现更简单,并且也更直观,当你习惯了这种方式以后,你的代码会更简洁。而且使用函数式的风格,能让你更容易避开烦人的反作用。
关于集合的操做就介绍到这里了,集合还有不少其它的函数(flatMap、find等限于篇幅,这里不做深刻介绍)能让你的代码更简洁。
高阶函数和咱们在学习代数的时候的高级代数很是像,咱们能够用一句话来解析清楚什么是高阶函数:**入参是函数或者出参是函数的函数就是高阶函数。**这句话很是绕口,直接看代码会更加容易理解:
//入参是函数的高阶函数
fun fooIn(func: () -> Unit){
println("foo")
func()
}
//出参是函数的高阶函数
fun fooOut() : () -> Unit{
println("hello")
return { println(" word!")}
}
复制代码
上面这两种就是最简单的两种形式的高阶函数,那么高阶函数有什么做用呢?咱们先来介绍一下第一种高阶函数。咱们先回顾下咱们上面集合操做的函数,在这里以filter
为例,filter
函数是怎么样实现的呢?直接看源码:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
复制代码
这是filter的实现,咱们再来看看filterTo是怎么样的:
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
//具体实现
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
复制代码
从代码咱们能够分析到,在咱们调用filter函数的时候,会经历以下步骤:
这里能够看出,filter函数返回的是一个新集合,不会影响调用集合自己。
对于函数式编程的初学者可能会以为,不管是代码仍是笔者的描述都很是难以理解(若是理解了,跳过这段)。若是理解不了的,咱们能够把函数在Java里面脱糖后再理解。在这里咱们会以上面books.filter { it.group == Group.Technology }
这段调用代码为例,进行脱糖处理。
由于filter函数涉及到Function1这个函数,因此在看实际代码前先看下Function1的定义:
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
复制代码
Function1其实就是一个普通的函数接口,没有什么特别。下面看看脱糖的代码。
books.filter(object : Function1<Book, Boolean>{
override fun invoke(p1: Book): Boolean {
return p1.group == Group.Technology
}
})
//为了更方便读者理解,这里把泛型也去掉了,并在语法上也作了一些处理
inline fun Iterable<Book>.filter(predicate: Function1<Book, Boolean>): List<Book> {
return filterTo(ArrayList<Book>(), predicate)
}
inline fun Collection<Book>.filterTo(destination: MutableCollection<Book>, predicate:Function1<Book, Boolean>): MutableCollection<Book> {
for (element in this) {
val isAdd = predicate.invoke(element)
if (isAdd) destination.add(element)
}
return destination
}
复制代码
能够看到,脱糖处理后的filter函数和咱们经常使用的一种设计模式是很是相似的,这个filter函数咱们能够看做是策略模式的一种实现,而传入的predicate实例就是咱们的一种策略。在上面这种场景中,咱们也能够用命令式编程的策略模式实现(如Java集合操做中的sorted函数)。
函数式编程可读性更强更易于维护的缘由之一就是:在函数式编程的过程当中,咱们会被动使用大量的设计模式。就算咱们不刻意去定义/使用,咱们也会大量使用相似设计模式中的策略模式和观察者模式去实现咱们的代码。
语法不重要,重要的是思想
柯里化函数,这个名词听起来逼格很是高,给个人感受就和第一次听到依赖注入这个词同样(笑哭)。可是当你稍微了解下以后就能发现,柯里化函数和依赖注入同样,都是一些很是简单很是基本的东西。柯里化函数实际上是高阶函数的一种,它的定义是:**返回值是一个函数的函数。**就是这么简单。可是这句话理解起来会有点抽象,咱们直接看代码吧:
fun sum(x: Int) : (Int) -> Int{
return { y: Int ->
x + y
}
}
复制代码
这就是一个简单的求和柯里化函数,咱们能够这样用它:
fun main(args: Array<String>) {
val s = sum(5)
println(s(10))
}
复制代码
输出结果是:15
这种函数看起来合普通的求和函数好像也没有什么区别。咱们能够拓展下上面的调用代码再看看:
fun main(args: Array<String>) {
val s = sum(5)
println(s(10))
println(s(20))
println(s(30))
}
复制代码
这下输出的结果是:
15 25 35
是否是以为有点意思了。柯里化函数的特色是,第一次调用会获得一个特定功能的函数,上面的例子就是,获得一个和5求和的函数。而后第二次调用的做用是,求传入的值和5的和。这样看起来貌似也没有什么做用,只是语法上好像更炫了一点而已。
咱们能够把上面的例子改为普通函数的方式再对比下。
fun sum1(x: Int, y: Int) : Int{
return x + y
}
fun main(args: Array<String>) {
println(sum(5, 10))
println(sum(5, 20))
println(sum(5, 30))
}
复制代码
不用柯里化函数的话,会很是依赖调用方的自觉性,由于咱们要获得5与某个数字的和的话,咱们必需要要求调用方须要在使用函数的时候,第一个值须要传入5。
咱们换个角度想象下,若是咱们如今有一个很是复杂的两段式运算,咱们可能会须要复用第一段运算的结果。那么这种场景下使用柯里化函数是很是方便的,咱们能够直接把第一段运算的结果直接缓存起来。而且因为柯里化函数第一次调用返回的是一个函数,因此,柯里化函数是无反作用的。柯里化函数会起到延迟做用的效果,第一次调用返回一个函数,第二次调用才会获得一个值,即在真正被消费的时候,才会生成值。
柯里化函数很是适合框架的开发者使用,咱们能够经过柯里化函数实现一些阅读简单而且很是有效的API。
scala源码中有大量的柯里化函数
主要是scala和kotlin的对比,没兴趣能够跳过
Kotlin的柯里化函数其实也是有本身的局限性的,从语法角度来讲,Kotlin的柯里化函数更难以理解,远没有Scala的简单。例如咱们上面的sum函数,从定义上来讲就没这么好理解。而Scala的柯里化函数会更加简单明晰。下面咱们对比下两种语言的柯里化函数的特色。
Scala
object Main{
def main(args: Array[String]): Unit = {
val s = sum(5)(_)
println(s(10))
println(s(20))
println(s(30))
//最终打印出15,25,35
}
def sum(x: Int)(y: Int) : Int = x + y
}
复制代码
Kotlin
fun sum(x: Int) : (Int) -> Int{
return { y: Int ->
x + y
}
}
复制代码
能够看到,Scala的柯里化函数从定义上来讲是更简单的,和普通的函数定义差很少。而Kotlin的柯里化函数会更加难以理解一点。但愿Kotlin有一天也能支持这种风格的柯里化函数。
这里只是简单介绍下kotlin和scala柯里化函数的语法区别,限于篇幅,这里不对柯里化函数的应用做过多介绍
当咱们掌握了上面的知识点后,咱们就掌握了函数式编程的基础知识了。是的,掌握了上面的知识后,只是处于FP编程入门的状态。上文提到过,咱们在使用函数式编程的时候,会被动地使用一些命令式编程中的设计模式,设计模式能够说是命令式编程的一种高阶应用。那么咱们要进一步理解、提升函数式编程技能,咱们须要了解一些函数式设计的通用结构。这种结构和咱们常说的设计模式有点相似,可是又不太同样。在初学阶段能够把它当成是函数式编程中设计模式来理解。
咱们主要介绍三种比较经常使用的通用结构。
在咱们刚接触Kotlin的时候,大部分人会先了解Kotlin的一个特性,就是:空安全。空安全是经过可空变量/常量实现的。在咱们使用可空变量/常量的时候,编译器会强制咱们要作空检查才能使用。通常咱们会使用?
这个语法糖实现,固然也能够在使用前先判空,判空后Kotlin会自动帮咱们进行智能转换,会把可空变量转换成非空变量。通常状况下,咱们可使用相似下面的代码处理可空变量。
如今假设咱们须要定义一个函数,入参是可能为空的书本列表,当列表长度大于5时,返回下表为5的元素,小于5时,返回下标为1的元素,为0时返回空。咱们能够用下面的三种方法来实现这个函数。
fun foo(books: List<Book>?) : Book?{
val size = books?.size ?: 0
return if (size > 5){
books?.get(5)
}else {
books?.firstOrNull()
}
}
fun foo1(books: List<Book>?) : Book?{
return if (books != null){
if (books.size > 5){
books[5]
}else{
books.firstOrNull()
}
}else{
null
}
}
fun foo2(books: List<Book>?) : Book?{
books ?: return null
return if (books.size > 5){
books[5]
}else{
books.firstOrNull()
}
}
复制代码
相对来讲foo和foo2这两种风格可读性都比较强,而foo1就有点啰嗦了。在面对这种比较简单的场景的时候,Kotlin的空安全写起来十分简洁,维护也挺方便的。那么假如咱们须要面对一些更为复杂的需求的时候呢?这种时候可能咱们会须要写一堆**?**号来解决这种问题。
例如:如今咱们用Kotlin实现一次文章开头的那个BookStore的程序。
CreateCard
class CreateCard{
fun charge(price: Int){
println("pay $price yuan")
}
}
复制代码
class BookStoreOption {
private val bookCollection = initBookCollection()
fun buyABook1(lc: CreateCard?, bookCode: Int) : Book?{
val result = bookCollection[bookCode]
//判空,result为空时不能产生交易行为
//lc?.charge(result?.price ?: 0) 这种写法会致使金额为0的交易行为
if (lc != null && result != null){
lc.charge(result.price)
}
return result
}
}
复制代码
上面是在不使用函数式通用结构时的比较合理的一种写法。
这样写的时候,客户端的调用代码多是这样的。
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
}
复制代码
输出结果:
pay 55 yuan book name = Kotlin, author = 小明
客户端的调用代码,咱们经过Kotlin的语法糖稍微简化了下代码。如今加多个条件,当buyBook1不为空时,打印出购买的书名、做者名。而且当buyBook1的group不为空时,再打印书的分组。
这种状况下咱们很容易就能写出下面这种代码:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null){
println("book group = ${it.group}")
}
}
复制代码
上面这种写法虽然功能上没什么问题,可是结构不太合理。假如咱们再加多几个判断条件的话,这种代码几乎没有可读性。难以阅读,也难以维护。那咱们试试用Option结构优化下这段代码。在优化以前,咱们先简单介绍下Option
这里咱们直接采用arrow开源库的Option演示,Option的结构很是简单,不想依赖库的话,能够本身定义一个。
Kotlin里面的Option其实和Java8的Optional是一个意思,就是用来处理可空变量的。咱们先简单看下Option是怎么用的:
fun main(args: Array<String>) {
fooOption("hello word!!!")
println("——————————————————————")
fooOption(null)
}
fun fooOption(str: String?){
val optionStr = Option.fromNullable(str)
val printStr = optionStr.getOrElse { "str is null" }
println(printStr)
optionStr.exists {
println("str is not null! str = $it")
true
}
}
复制代码
Option.fromNullable(str)能够用语法糖str.toOption()代替
咱们看看打印结果:
hello word!!! str is not null! str = hello word!!! —————————————————————— str is null
结合例子,咱们能够知道:
Option.getOrElse函数的做用是:若是对象不为空,返回对象自己,为空则返回一个默认值。
Option.exists函数的做用是:若是对象不为空,则执行Lambda(函数、闭包)里面的代码。
对于Option,咱们先了解这么多就好了。
咱们直接看看,若是要用Option来优化buyABook1咱们要如何优化:
class BookStoreOption {
private val bookCollection = initBookCollection()
fun buyABook2(lc: CreateCard?, bookCode: Int) : Option<Book>{
val lcOption = lc.toOption()
val bookOption = bookCollection[bookCode].toOption()
lcOption.map2(bookOption){
it.a.charge(it.b.price)
}
return bookOption
}
}
//客户端调用函数
fun main(args: Array<String>) {
println("\n——————————— createCard is null, bookCode 1 ———————————————")
val bookStoreOption2 = BookStoreOption()
buyBook2ForStore(bookStoreOption2, null, 1)
println("\n—————————————————————————— bookCode 1 ————————————————————————————————")
buyBook2ForStore(bookStoreOption2, CreateCard(), 1)
println("\n—————————————————————————— bookCode 20————————————————————————————————")
buyBook2ForStore(bookStoreOption2, CreateCard(), 20)
}
fun buyBook2ForStore(store: BookStoreOption, createCard: CreateCard?, bookCode: Int){
val buyBook2 = store.buyABook2(createCard, bookCode)
buyBook2.map{
println("book name = ${it.name}, author = ${it.author}")
it.group
}.exists {
println("book group = $it")
true
}
}
复制代码
咱们能够看到,buyABook2和buyABook1的主要区别是,buyABook2返回的是一个非空的Option<Book>
对象。单纯看这个函数咱们看不出太大的优点,那咱们们再看看buyBook2ForStore这个函数。咱们回顾下前面的客户端调用函数:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null){
println("book group = ${it.group}")
}
复制代码
这样一看好像用Option后,代码反而更多了。可是不难看出,buyBook2ForStore的结构更加清晰,可读性更强。咱们能够这样理解这个函数:
使用Option以后,代码结构和咱们人类的思考方式是很是相似的,可读性会强不少。可是这样看起来,使用Option其实也没有带来太大的提高。可是若是当咱们的代码规模和复杂度更高了以后呢?例如,Book中的Group实际上是更加复杂的对象呢?Group还包含:名字,id,等等信息呢?而且他们都是可空变量(在Kotlin和Java混合编程里面很是常见,由于Java对变量是否可空的限制比较弱)呢?假如咱们如今须要在打印group后再,读取group里面的name的值,可能要这样写:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null ){
println("book group = ${it.group}")
if(buyBook1.group.name != null){
println("book group name = ${it.group.name}")
}
}
复制代码
随着迭代,这段代码变得愈来愈难以理解了,如今它嵌套了三层了。在不使用Option的时候,咱们只能一层层使用if语句去判断。**代码嵌套会使复杂度暴增。**我相信没什么人会想维护一段多重嵌套的代码,特别是这段代码仍是别人写的。若是使用Option的话,上面这个需求,能够这样实现:
val buyBook2 = store.buyABook2(createCard, bookCode)
buyBook2.map{
println("book name = ${it.name}, author = ${it.author}")
it.group
}
.map {
println("book group = $it")
it.name
}
.exists {
println("book name = $it")
true
}
复制代码
使用Option以后,会比咱们使用if嵌套代码结构清晰不少。Option在处理大量可空值的时候,能以线性的方式去处理,而简单使用if的话,咱们的代码须要以相似多重嵌套的方式去实现,代码复杂度会暴增。
固然,在使用命令式编程的时候,咱们能够经过设计模式去优化代码。而在函数式编程中,咱们使用函数式的通用结构的话,自然就是在使用相似设计模式的方式去编写代码。函数式的通用结构和设计模式的做用是相似的,可是函数式结构能提供更高程度的抽象(例如,咱们不须要为具体的业务场景定义一个具体的Option,全部场景使用Option的方式是同样的,而设计模式作不到这么完全的抽象)。使用这些结构,能让咱们以相似代数演算的方式去实现咱们的代码,经过不一样的组合能让咱们构建出更加复杂的功能。
虽然函数式通用模式在不少场景下工做得很好,可是并不能彻底替代设计模式。在实际开发中要根据业务场景来选择,同时使用两种方式进行cc设计也是能够的。
Option是一个比较简单的函数通用结构,可是它的功能场景比较局限。读者能够把它做为一个入门的函数式通用结构来学习。接下来咱们会介绍其它两个更增强大也更有用的通用结构。
前面咱们介绍了如何使用Option更优雅的处理可空变量的问题,可是Option实际上是没有解决咱们文章开头提到的一个最核心的问题的。**如何实现无反作用的函数。**咱们这里回顾下,由于咱们在调用buyABook函数的时候,会包含一个支付行为。支付行为会和外界系统进行交互,因此这个buyABook行为不仅仅影响了咱们的图书销售系统,还会把影响扩散到其它系统,这就是咱们所说的反作用。
要消除这个反作用,咱们要作的是支付行为从图书销售系统中分离开,实现图书销售系统 —— 支付系统解耦。如今咱们从新整理下咱们的需求。咱们的需求是要实用向书店购买书本并支付,现实场景中,咱们可能会购买多本书本。若是使用咱们上面的buyABook的话,咱们能够循环遍历这个方法,直到所有购买成功为止。不过这里会有个问题,每购买一本书,就付款一次,显然是很是不合理的,而且可能会存在余额不足的问题。咱们要重构BookStore这个对象,以实现咱们的需求。整理一下,咱们的重构的BookStore要支持下面的功能:
为了实现这个需求,咱们会增长一个Charge类,这个类的做用是记录费用。
下面直接上代码,第一次重构:
/** * 费用 * [id] 惟一标识符,太懒了,用随机数表示 */
data class Charge(
val createCard: CreateCard,
val price: Int,
val id: Int = Random.nextInt()
)
复制代码
/** * 第一次重构BookStore */
class RefactoringBookStore{
private val bookCollection = initBookCollection()
/** * 购买多个 */
fun buyBooks(cc: CreateCard, bookIds: List<Int>) : Pair<List<Book>, Charge>{
val purchases = bookIds
.map { buyABook(cc, it).orNull() }
.filterNotNull()
val (books, charges) = purchases.unzip()
//传入的createCard是同一个,因此reduce操做的时候不用判断createCard是否一致
val totalCharge = charges.reduce {
acc, charge -> Charge(acc.createCard, acc.price + charge.price) }
return books to totalCharge
}
@Suppress("MemberVisibilityCanBePrivate")
fun buyABook(cc: CreateCard, bookId: Int) : Option<Pair<Book, Charge>> {
val book = bookCollection[bookId]
return book.toOption().map { Pair(it, Charge(cc, it.price)) }
}
}
复制代码
重构后的代码已经把支付这个“反作用”分离出去了,购买书的函数中不会产生支付行为。支付行为客户端须要经过读取咱们封装好的Charge对象,而后再调用支付模块实现支付(限于篇幅,省略支付模块)。
如今咱们来看看客户端的调用代码:
fun main() {
val (books1, charge1) =
RefactoringBookStore().buyBooks(CreateCard(123), 1, 3, 5, 10, 20, 30)
printBuyBooks(books1, charge1)
println("——————————————————————————————\n")
val (books2, charge2) =
RefactoringBookStore().buyBooks(CreateCard(111, 120), 7, 2, 8)
printBuyBooks(books2, charge2)
}
fun printBuyBooks(books: List<Book>, charge: Charge){
val buyBookName = books.map { it.name }
println("但愿购买书本名字: $buyBookName")
if (charge.createCard.amount >= charge.price){
val cc = charge.createCard.charge(charge.price)
println("支付成功,支付金额 = ${charge.price}; 剩余额度 = ${cc.amount}")
}else{
println("额度不足,须要支付金额 = ${charge.price}; 可用额度 = ${charge.createCard.amount}")
}
}
复制代码
咱们看看调用结果
但愿购买书本名字: [Kotlin, 娱乐杂志, 资本论, 文明的冲突与世界秩序的重建] pay 148 yuan 支付成功,支付金额 = 148; 剩余额度 = 352 ——————————————————————————————
但愿购买书本名字: [Scala, 中国民俗, 月亮与六便士] 额度不足,须要支付金额 = 125; 可用额度 = 120
上面的这段代码已经成功把反作用分离出去了(省略支付模块)。通常状况下,这段代码已经能工做得很好了(经验丰富的同窗可能会以为,这种代码算是比较好维护的代码了😂)。可是其实这段代码仍是有问题的,它的问题就是,咱们须要经过List来维护Charge这个对象,在组合Charge的时候须要经过相似下面的方式来实现:
fun foo(){
Charge(acc.createCard, acc.price + charge.price)
charge.copy(charge.createCard, charge.price + charge1.price)
}
复制代码
这样看起来好像也没什么问题。可是假若有这样的一个场景:书店里面有位客人,买了一批书后,发现还有书忘记买了,再买一批后,又发现了一本本身很想买的书。在这种场景下咱们客户端的代码可能会变成相似这种:
val cc = CreateCard(12423)
val (b1, c1) = RefactoringBookStore().buyBooks(cc, 1, 2)
val (b2, c2) = RefactoringBookStore().buyBooks(cc, 4, 5)
val (b3, c3) = RefactoringBookStore().buyBooks(cc, 6, 7)
val books = mutableListOf<Book>().apply {
addAll(b1)
addAll(b2)
addAll(b3)
}
printBuyBooks(books, Charge(c1.createCard, c1.price + c2.price + c3.price))
复制代码
这种代码主要的问题就是在于Charge的合并上面,当咱们在组合更多更复杂的Charge的时候,会写不少相似这种繁琐又不利于维护的代码。固然,你能够把多个Charge放到List里面再作处理,可是这两种方式都不是那么的方便。那么有没有更合理的方法解决这种问题呢?有的,咱们可使用Monoid来解决这个问题。
Monoid和Option同样,也是函数设计的通用结构的其中一种。Monoid是一种纯代数结构,它的中文名字叫幺半群,它是一个代数定义。Monoid在函数式编程中常常出现,操做列表、链接字符、循环中进行累加操做均可以背解析成Monoid。Monoid的主要做用是:将问题拆分红小部分而后并行计算和将简单的部分组装成复杂的计算。Monoid是一种数学上的概念。
在抽象代数此一数学分支中,幺半群(英语:monoid,又称为单群、亚群、具幺半群或四分之三群)是指一个带有可结合二元运算和单位元的代数结构。—— 维基百科
看不懂上面这段话也不要紧,大部分第一次接触这个定义的时候,都会以为很难理解。咱们下面经过简单的例子来学习Monoid到底能作些什么。
举个整数计算的例子,假设有 x 、y、z三个整数,那么咱们很容易就能得出下面的一些公式:x + y + z 等于( x + y ) + z 等于x + ( y + z )。并且有一个单位元(Monoid的定义)元素0,当0和其它整数相加时,结果不会发生改变。乘法运算也有一个单位元元素1。
咱们先看看Kotlin中的Monoid的定义是怎么样的:
Monoid
/** * ank_macro_hierarchy(arrow.typeclasses.Monoid) */
interface Monoid<A> : Semigroup<A>, MonoidOf<A> {
/** * A zero value for this A */
fun empty(): A
/** * Combine an [Collection] of [A] values. */
fun Collection<A>.combineAll(): A =
if (isEmpty()) empty() else reduce { a, b -> a.combine(b) }
/** * Combine an array of [A] values. */
fun combineAll(elems: List<A>): A = elems.combineAll()
companion object
}
复制代码
Semigroup
interface Semigroup<A> {
/** * Combine two [A] values. */
fun A.combine(b: A): A
operator fun A.plus(b: A): A =
this.combine(b)
fun A.maybeCombine(b: A?): A = Option.fromNullable(b).fold({ this }, { combine(it) })
}
复制代码
这是Monoid在arrow开源库中的定义,若是在使用时不想依赖arrow库的话,本身实现一个Monoid也是很是简单的,30多行代码就足以。
咱们简单使用下Monoid:
fun main() {
val monoid = Int.monoid()
val sum = listOf(1, 2, 3, 4, 5).foldMap(monoid, ::identity)
println("sum = $sum")
}
复制代码
结果:
sum = 15
能够看到IntMonoid在这里的做用就是定义了元素之间的结合规则。
Int.monoid()
给咱们返回了一个IntMonoid。咱们再来看看IntMonoid的定义:
interface IntMonoid : Monoid<Int>, IntSemigroup {
override fun empty(): Int = 0
}
interface IntSemiring : Semiring<Int> {
override fun zero(): Int = 0
override fun one(): Int = 1
override fun Int.combine(b: Int): Int = this + b
override fun Int.combineMultiplicate(b: Int): Int = this * b
}
复制代码
IntMonoid的定义也很是简单,主要是定义了0值,1值和它们两两结合的操做。
这样看来Monoid好像给人一点多此一举的感受,咱们不用monoid的话,折叠集合的时候能够这样写啊:
listOf(1, 2, 3, 4, 5).fold(0){acc, i ->
acc + i
}
复制代码
那么咱们为何要使用Monoid呢,上一小节咱们经过Charge把反作用分离了,可是Charge的后续处理仍是存在不完善的地方。那么咱们来尝试使用Monoid来优化咱们的代码。
咱们先定义个Monoid<Charge>
/** * 须要传入[CreateCard] 由于同一张信用卡的费用才能作合并处理 */
class ChargeMonoid(private val cc: CreateCard) : Monoid<Charge>{
/** * 固定的单位元值,id也须要是固定的 * 由于同一个ChargeMonoid(equals 为 true)的单位元([empty])是相等(equals 为 true)的 */
override fun empty(): Charge = Charge(cc, 0, 0)
/** * 只会合并createCard等于[cc]的Charge * 不一样信用卡的费用没法合并 */
override fun Charge.combine(b: Charge): Charge =
if (cc == b.createCard){
Charge(cc, b.price + price)
}else{
this
}
}
复制代码
咱们回顾下上文说的书店的例子:
咱们先来看看新的书店的代码:
class MonoidBookStore {
private val bookCollection = initBookCollection()
/** * bookId能够传入多个 */
fun buyBooks(cc: CreateCard, vararg bookIds: Int) : Pair<List<Book>, Charge>{
val purchases = bookIds
.map { buyABook(cc, it).orNull() }
.filterNotNull()
val (books, charges) = purchases.unzip()
//使用ChargeMonoid折叠列表
val totalCharge = charges.foldMap(Charge.monoid(cc), ::identity)
return books to totalCharge
}
@Suppress("MemberVisibilityCanBePrivate")
fun buyABook(cc: CreateCard, bookId: Int) : Option<Pair<Book, Charge>> {
val book = bookCollection[bookId]
return book.toOption().map { Pair(it, Charge(cc, it.price)) }
}
}
复制代码
和第一次重构后的RefactoringBookStore
很是像,他们只有一句代码是有区别的:
RefactoringBookStore
//传入的createCard是同一个,因此reduce操做的时候不用判断createCard是否一致
val totalCharge = charges.reduce {
acc, charge -> Charge(acc.createCard, acc.price + charge.price) }
复制代码
MonoidBookStore
//使用ChargeMonoid折叠列表
val totalCharge = charges.foldMap(Charge.monoid(cc), ::identity)
复制代码
::identity是Kotlin的一个语法糖,在这里的做用是,返回折叠后的Charge对象。::identity的做用是返回自己,有兴趣的能够看看它的具体实现。
咱们能够看到它们的差异很小,那么咱们这样写有什么好处呢?咱们再来回顾一下上面的一个场景:店里面有位客人,买了一批书后,发现还有书忘记买了,再买一批后,又发现了一本本身很想买的书。
那么咱们用ChargeMonoid要怎么实现这个功能呢?直接看代码。
fun main() {
val cc = CreateCard(12423)
val (b1, c1) = MonoidBookStore().buyBooks(cc, 1, 2)
val (b2, c2) = MonoidBookStore().buyBooks(cc, 4, 5)
val (b3, c3) = MonoidBookStore().buyBooks(cc, 6, 7)
val books = listOf(b1, b2, b3).flatten()
val charge = listOf(c1, c2, c3).foldMap(Charge.monoid(cc),::identity)
printBuyBooks(books, charge )
}
复制代码
如今来看看输出:
但愿购买书本名字: [Kotlin, 中国民俗, 灌篮, 资本论, Java, Scala] pay 255 yuan 支付成功,支付金额 = 255; 剩余额度 = 245
单单看代码的话,上面这段代码和前面的对比起来貌似只有一个好处:不须要人工维护Charge对象的合并。是的,它就真的只有这一个好处。Monoid它的主要做用就是这个,它定义了相同类型(群)的对象的合并规律。在这里咱们Charge的合并规律就是:CreateCard相同的Charge对象以价格累计的方式合并在一块儿。
咱们为了这个简单的功能引入一个这样复杂的概念(对初学者来讲,这的确是有点难以理解)值得吗?
如今咱们再来改一下这个需求( 敏捷开发😂 ):店里面有位客人,买了一批书后,发现还有书忘记买了,再买一批后,又发现了一本本身很想买的书。而后他又想再买一批书,可是如今想用另一张卡付款,而后再买一批,再用另一张卡付款。这个过程当中,一共用了三张卡付款,你们能够尝试用命令式编程实现这个需求,这里就不演示了,直接上Monoid的例子:
fun main() {
//数据初始化
val cc2 = CreateCard(124234)
val (b4, c4) = MonoidBookStore().buyBooks(cc2, 1, 2)
val (b5, c5) = MonoidBookStore().buyBooks(cc2, 4, 5)
val (b6, c6) = MonoidBookStore().buyBooks(cc2, 6, 7)
val cc3 = CreateCard(124234512)
val (b7, c7) = MonoidBookStore().buyBooks(cc3, 3)
val cc4 = CreateCard(151212)
val (b8, c8) = MonoidBookStore().buyBooks(cc4, 8)
//数据处理
val monoid3 = monoidTuple3(
//a monoid
Charge.monoid(cc2),
//b monoid
Charge.monoid(cc3),
//c monoid
Charge.monoid(cc4))
val result = listOf(c6, c7 ,c8).foldMap(monoid3){
Tuple3(it, it, it)
}
println("\n—————————————————第一张信用卡———————————————————")
//result.a -> monoid a收集的数据
printBuyBooks(listOf(b4, b5, b6).flatten(), result.a)
println("\n—————————————————第二张信用卡———————————————————")
//result.b -> monoid b收集的数据
printBuyBooks(b7 , result.b)
println("\n—————————————————第二张信用卡———————————————————")
//result.b -> monoid b收集的数据
printBuyBooks(b8, result.c)
}
/** * 不用理解下面的代码(Tuple3是另一种函数设计通用结构) * 只须要明白个函数的做用值组合3个Monoid就好了 */
fun <A, B, C> monoidTuple(MA: Monoid<A>, MB: Monoid<B>, MC: Monoid<C>): Monoid<Tuple3<A, B, C>> =
object: Monoid<Tuple3<A, B, C>> {
override fun Tuple3<A, B, C>.combine(y: Tuple3<A, B, C>): Tuple3<A, B, C> {
val (xa, xb, xc) = this
val (ya, yb, yc) = y
return Tuple3(MA.run { xa.combine(ya) }, MB.run { xb.combine(yb) }, MC.run { xc.combine(yc) })
}
override fun empty(): Tuple3<A, B, C> = Tuple3(MA.empty(), MB.empty(), MC.empty())
}
复制代码
如今咱们看看输出的内容
—————————————————第一张信用卡——————————————————— 但愿购买书本名字: [Kotlin, 中国民俗, 灌篮, 资本论, Java, Scala] pay 105 yuan 支付成功,支付金额 = 105; 剩余额度 = 395
—————————————————第二张信用卡——————————————————— 但愿购买书本名字: [娱乐杂志] pay 19 yuan 支付成功,支付金额 = 19; 剩余额度 = 481
—————————————————第二张信用卡——————————————————— 但愿购买书本名字: [月亮与六便士] pay 25 yuan 支付成功,支付金额 = 25; 剩余额度 = 475
咱们回顾下代码的数据处理部分
val monoid3 = monoidTuple3(
//a monoid
Charge.monoid(cc2),
//b monoid
Charge.monoid(cc3),
//c monoid
Charge.monoid(cc4))
val result = listOf(c6, c7 ,c8).foldMap(monoid3){
Tuple3(it, it, it)
}
复制代码
是的,数据处理就这么多代码,在这里咱们经过了Monoid来处理了数据。ChargeMonoid会把数据按照CreateCard的类型来收集,因此咱们不用关心数据是如何收集的。若是采用命令式编程,你们能想象到代码的糟糕程度。
而且在上面的场景中,就算咱们再用多几张信用卡用来支付也不会增长太多代码量,这就是Monoid的优点。
monoidTuple3在这里的做用是组合Monoid,它能组合任何三个Monoid,是一段复用性很是强的代码。固然读者也能够本身写一个monoidTuple2用于组合两个Monoid。
从文章开头的思惟导图能够看出来,这里少写了一个函数式通用结构:Monad。少写的缘由是:文章的文字量已经接近1.2W字了,做为一篇博客,内容量已是过于多了。因此这里就暂时就不继续Monad结构了。对函数式通用结构有兴趣的同窗能够继续关注个人博客/公众号:代码以外的程序员(懒鬼,一年没更新了还好意思叫我关注。好吧,我今年会努力保持跟新的)。笔者会慢慢继续更新函数式通用结构中其它的实用结构的。
函数式编程,你们能够不局限于Kotlin,由于FP更可能是一种思想,相似一条条代数公式,和语言是无关的。当你们掌握了FP以后,就算是切换到别的FP语言中,大部分
这里给你们一个学习建议,我知道你们看到一篇感兴趣的文章的时候,都喜欢当一个马来人(mark)。你们回想下,收藏夹里面有多少篇文章是你们只看了题目的?感兴趣的东西,要立刻过一遍,由于这个时候你的热情是最高的,过了一天你可能就没兴趣了。学习就是一个这样的过程:兴趣 -> 理解 -> 实战 -> 深刻
因此这里建议有兴趣的同窗(看到这里的同窗很赞),下载Demo体验一下,本身再尝试写感兴趣的部分。最后祝你们尽快在本身的项目中愉快地使用FP风格进行编程。
Demo是开源库中的KotlinFp项目,请使用IntelliJ打开。若是以为本文对你有帮助的话,能够点一下star以资鼓励😊。
若是下面的项目你有兴趣的话,也能够点进去看看。若是以为还能够的话,能够点一下star
关于MVVM的两个项目的文档还在整理中,笔者会尽快整理出来。有兴趣的同窗能够关注下个人github动态,最后,我写的这么辛苦你们记得点个star哦。