函数

1 概念

函数式编程,简称FP(Functional Programming)。

1.1 数学含义

y=f(x),用FP的思想解释:主体是“f”(函数,图上的箭头),参数是x,y。

参数,可以是对象,也可以是函数

高阶函数:函数的参数也是函数

映射

1.2 VS 对象

面向对象 --> 狗.吃(食) FP --> 吃(狗,食)
示例
主体 对象 --> 狗 函数 --> 吃
描述 对象“狗” has a method “吃” with param "食" function “吃” with params:狗、食
特点 函数不能单独存在,必须被声明在class里 函数可以独立存在

函数和对象,不存在谁从属于谁,只是思维方式的区别。

  • “对象”,会把混沌的业务拆分成若干固定的模块(架构),然后,再细化。理想情况下,模块设计好后,在实施的过程中,即便出现严重的BUG,也只影响有限的模块。当然,这很难做到。于是,需要持续重构系统。如果模块设计地太粗糙,还需要推倒重来。所以,从对象的角度出发,很自然地会用到很多设计模式,从宏观的角度思考问题,避免代码大范围地糜烂。

  • 函数式思维倾向于把一件事做到极致,恰好可以弥补“对象”的不足。

1.3 编程语言的历史

现代语言通过一层又一层的抽象,封装了琐碎的细节。比如,Java语言的垃圾收集器。每一层抽象,都在吃硬件。

普通的方法,传递的“对象”是“静态”的,而高阶函数传递的“函数”是“动态”的。而且,函数嵌套的代码,可阅读性也差、性能也差、学习曲线陡峭...但是呢,“函数”是把利刃,尤其是在具体的业务场景里,用好“函数”,可以快速解决问题。如果,还是继续用Java的“对象”,或是Java8的lambda,估计人家喝咖啡的时候,你还在加无聊的班。

在kotlin之前,我也用Groovy、Scala写过函数,尤其是Scala。相信我,kotlin的语法更严谨。

函数,用kotlin写啦。没有书么?自己写呢?

2 知识点

2.1 闭包

所有函数都是闭包(忘了在哪里看到的了。感觉差不多,你说呢)

2.1.1 调用外部变量

变量的作用域:全局、局部。

在闭包内部的变量是“局部变量”,但,在闭包内部可以调用外部的变量。代码如下:

context("外部变量number = 1") {
    val number = 1
    on("在闭包中调用外部变量number -- 简化的写法") {
        fun f1() = { number + 1 }
        it("should return 2") {
            assertEquals(2, f1()()) //很奇怪?别急,继续看下面的代码
        }
    }
    on("调用number -- 展开所有的类型") {
        //“()”:没有参数
        //返回值"() -> Int",是个函数
        fun f1(): () -> Int = { number + 1 } 

        it("should return 2") {
            val alsoFunction: () -> Int = f1()
            val value = alsoFunction()
            assertEquals(2, value)
        }
    }
}

在JavaScript中,变量声明的时候,如果不用var修饰,默认是全局的...再次吐槽JavaScript。kotlin没有这个BUG。代码如下:

on("闭包内部的变量"){
    fun f1():() -> Int = {
        val number = 1
        number
    }
    //println(number) 会报错。
    it("should be 1") {
        assertEquals(1, f1()())
    }
}

2.1.2 读取闭包的内部变量

示例有点难。能看懂,还是会有收获的

on("读取闭包的内部变量") {
    fun makeCounter(): () -> Int { //返回值也是函数
        var backing = 0 //声明函数内部的变量
        return fun(): Int { return ++backing } // 匿名函数,可以访问外部变量backing
    }
    it("should all be passed") {
        val counter = makeCounter()
        assertEquals(1, counter())
        assertEquals(2, counter())
        assertEquals(3, counter()) //每次计算,backing都会加1
        
        //顺便学习invoke的用法
        assertEquals(1, makeCounter().invoke())
        assertEquals(1, makeCounter()())
    }
}
  • 场景描述

    临时需要一个计数器,而且,有可能多次使用?

  • Java代码示例

    class Counter{
      private int backing = 0;
      public int getCount(){
        return ++backing;
      }
    }
    
  • Kotlin的另一种实现(介于函数和对象之间)

    delegate,用在这个例子里,有点过了。只是推导哈,验证一些想法,还是有学习的意义的

    on("use delegate 读取闭包的内部变量") {
        var counter: Int by object {
            private var backing: Int = 0
            operator fun getValue(thisRef: Any?, property: KProperty<*>): Int =
                    ++backing
            operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
                backing = value
            }
        }
        it("should all be passed") {
            assertEquals(1, counter)
            assertEquals(2, counter)
            assertEquals(3, counter)
        }
    }
    

2.1.3 函数与闭包

再看几个例子,还是有点难。

describe("闭包的应用") {
    data class Employee(val name: String, val salary: Int) //测试用的model
  
    context("返回值是闭包") {
        fun paidMore(amount: Int): (Employee) -> Boolean = { it.salary > amount }
      
        context("使用闭包,设定标准low level:> 100") {
            val amount = 100
            val isHighPaid = paidMore(amount) //请注意:还是函数
          
            it("should all be passed") {
                assertTrue(isHighPaid(Employee("yuri", amount + 1)))
                assertFalse(isHighPaid(Employee("yuri", amount - 1)))
            }
        }
    }
    context("参数是闭包,且有默认的实现") {
        //Employee.paidMore,kotlin的扩展函数。又扯远了。先尝尝吧
        //这种写法相当于元编程,给Employee添加了函数paidMore
        fun Employee.paidMore(amount: Int, predicate: (Int) -> Boolean = {
            this.salary > it //this特指Employee
        }): Boolean = predicate(amount)
      
        on("use the default implementation") {
            val amount = 100
            fun Employee.isHighPaid() = paidMore(amount)
          
            it("should all be passed") {
                assertTrue(Employee("Alice", amount + 1).isHighPaid())
                assertFalse(Employee("Alice", amount - 1).isHighPaid())
            }
        }
      
        on("override the default implementation") {
            val amount = 100
            fun Employee.isHighPaid() = paidMore(amount) {
                this.salary > it * 2
            }
          
            it("should all be passed") {
                assertTrue(Employee("Bob", amount * 2 + 1).isHighPaid())
                assertFalse(Employee("Bob", amount * 2 - 1).isHighPaid())
            }
        }
    }
}

2.1.4 总结

  • 简单、有效

简单的场景,专门定义一个class & method,感觉有点脱了裤子放屁的意思。
如果业务场景经常变,就需要封装变化的部分了。

注意:你得区分变化的是“结构”还是“细节”

如果是“流程”、“结构”变化了,你需要create new classes
如果是具体的“算法”、“逻辑”,你需要函数、闭包、高阶函数

  • 闭包占用额外的内存

闭包是动态的,通过 2.1.2 的例子,你会发现,闭包的状态一直都在内存里。所以,对性能有特殊要求的场景,你需要对闭包专门调优。

  • 代码阅读性差

使用闭包后,代码可以写得很花,但是,阅读的时候就费劲了。相比groovy、Scala,kotlin的语法已经严谨很多了,至少,回头看自己写的代码,花点时间还是能看懂地。至于阅读的速度,自己练吧。

当然,还有函数和闭包的区别,这个...不要计较了,会用就行。

2.2 偏函数

其中,g(y,z)是“偏函数”

given("闭包: (Int,Int) -> Int") {
    val add = { x: Int, y: Int -> x + y }
    
    it("should all be passed") {
        fun addY(y: Int) = add(1, y) //指定add的参数x=1,得到新的函数addY
        assertEquals(3, addY(2))
    }
}

2.3 柯里化 (Carrying)

柯里化

那么,kotlin支持柯里化吗?且看下面的例子:

describe("柯里化") {
    given("柯里化的闭包: (Int) -> (Int) -> Int") {
        val add = { x: Int ->
            { y: Int ->
                x + y
            }
        }
      
        it("should all be passed") {
            assertEquals(3, add(1)(2))
            val addY = add(1)
            assertEquals(3, addY(2))
        }
    }
    given("不能柯里化的闭包: (Int,Int) -> Int") {
        val add = { x: Int, y: Int -> x + y }
      
        it("should all be passed") {
            assertEquals(3, add(1, 2))
            fun addY(y: Int) = add(1, y)
            assertEquals(3, addY(2))
        }
    }
    given("柯里化的函数:(Int) -> (Int) -> Int") {
        fun add(): (Int) -> (Int) -> Int = { x: Int ->
            { x + it }
        }
      
        it("should all be passed") {
            assertEquals(3, add()(1)(2))
            
            val addY = add()(1) //柯里化+偏函数
            assertEquals(3, addY(2))
        }
    }
    given("不能柯里化的函数:(Int, Int) -> Int") {
        fun add(): (Int, Int) -> Int = { x: Int, y: Int -> x + y }
      
        on("直接调用add函数") {
            it("should be equal") {
                assertEquals(3, add()(1, 2))
            }
        }
        on("使用偏函数,模拟柯里化的效果") { //瞎玩,貌似也没什么效果
            fun addY(y: Int) = add()(1, y) //指定原函数add的参数x=1,得到新的函数addY
            it("should also be equal") {
                assertEquals(3, addY(2))
            }
        }
    }
}

kotlin不支持柯里化,为什么呢?

你可能感兴趣的:(函数)