vlambda博客
学习文章列表

读书笔记《functional-kotlin》函数、函数类型和副作用

Chapter 4. Functions, Function Types, and Side Effects

函数式编程围绕不变性和函数的概念展开。我们在上一章了解了不可变性。在讨论不变性时,我们还对纯函数有所了解。纯函数基本上是函数式编程必须提供的众多类型之一(但可能是最重要的一种)。

本章将围绕函数展开。要深入了解函数式编程,您需要强大的函数基础。为了让您的概念清晰,我们将从普通的 Kotlin 函数开始,然后逐步讨论函数式编程定义的函数的抽象概念。我们还将看到它们在 Kotlin 中的实现。

在本章中,我们将介绍以下主题:

  • Functions in Kotlin
  • Function types
  • Lambda
  • High order functions
  • Understanding side effects and pure functions

所以,让我们从定义函数开始。

Functions in Kotlin


函数是编程中最重要的部分之一。我们每周都会为我们的项目编写大量函数。函数也是编程基础的一部分。要学习函数式编程,我们必须清楚地了解函数的概念。在本节中,我们将介绍函数的基础知识,以便让您为本章的下一部分做好准备,我们将讨论抽象函数概念及其在 Kotlin 中的实现。

所以,让我们从定义函数开始。

Note

function 是一个有组织的、可重用的代码块,用于执行单个相关操作。

不是很清楚?我们将解释,但首先,让我们了解为什么我们应该编写函数。简而言之,一个函数的functionality是什么?看一看:

  • Functions allow us to break the program into a bunch of steps and substeps
  • Functions encourages code reuse
  • Functions, if used properly, help us keep the code clean, organized, and easy to understand
  • Functions make testing (unit testing) easy, testing each small part of the program is easier than the complete program in a single go

在 Kotlin 中,函数通常如下所示:

fun appropriateFunctionName(parameter1:DataType1, parameter2:DataType2,...): ReturnType { 
    //do your stuff here 
    return returnTypeObject 
} 

在 Kotlin 中,函数声明以 fun 关键字开头,后跟函数名称,然后是大括号。在大括号内,我们可以指定函数参数(可选)。在大括号之后,会有一个冒号(:)和返回类型,它指定了要返回的值/对象的数据类型(如果你不这样做,你可以跳过返回类型'不打算从函数返回任何东西;在这种情况下,默认返回类型 Unit 将被分配给函数)。之后是函数体,用花括号覆盖(花括号对于单表达式函数也是可选的,接下来将在 Chapter 5更多关于函数)。

Note

Unit 是 Kotlin 中的一种数据类型。 Unit 是它自己的一个单例实例,并拥有一个 Unit 本身的值。 Unit对应Java中的void,但与void有很大不同。虽然 void 在 Java 中没有任何意义,并且 void 不能包含任何内容,但我们有 Nothing 在 Kotlin 中用于此目的,这表明函数永远不会成功完成(由于异常或无限循环)。

现在,那些返回类型、参数(参数)和函数体是什么?让我们来探索一下。

下面是一个比之前显示的抽象示例更现实的 function 示例:

fun add(a:int, b:Int):Int { 
   val result = a+b 
   return result 
} 

现在,看一下函数每个部分的以下解释:

  • Function arguments/parameters: These are the data (unless lambda) for the function to work on. In our example, a and b are the function parameters.
  • Function body: Everything we write inside the curly braces of a function is called the function body. It is the part of a function, where we write the logic or set of instructions to accomplish a particular task. In the preceding example, two lines inside the curly braces is the function body.
  • Return statement, datatype: If we are willing to return some value from the function, we have to declare the datatype of the value we are willing to return; that datatype is called the return type—in this case, Int is the return type and return result is the return statement, it enables you to return a value to the calling function.

我们可以通过删除 val result = a+b 并将 return 语句替换为 return a+b 来缩短前面的示例。在 Kotlin 中,我们可以进一步缩短这个例子,正如我们将在 Chapter 5 中看到的, 更多关于函数的信息

虽然编写函数很容易,但 Kotlin 让您更轻松。

Kotlin 将各种功能与使开发人员的生活更轻松的功能捆绑在一起。以下是与 Kotlin 捆绑的功能的简要列表:

  • Single-expression functions
  • Extension functions
  • Inline functions
  • Infix notation and more

我们将在 Lambda、Generics、Recursions、Corecursion 部分详细介绍它们"ch05">第 5 章更多关于函数的内容

Returning two values from a function

在 Kotlin 中,通过利用 Pair 类型和解构声明,我们可以从一个函数返回两个变量。考虑以下示例:

fun getUser():Pair<Int,String> {//(1) 
    return Pair(1,"Rivu") 
} 
fun main(args: Array<String>) { 
    val (userID,userName) = getUser()//(2) 
     println("User ID: $userID t User Name: $userName") 
} 

在前面的程序中,在注释 (1) 上,我们创建了一个返回 Pair<Int,String>  的函数;价值。

在评论 (2) 中,我们使用该函数的方式似乎返回两个变量。实际上,解构声明允许您解构 data class/Pair 并在独立变量中获取其基础值。当此功能与函数一起使用时,函数似乎返回多个值,尽管它只返回一个值,即 Pair value 或另一个 数据类

Extension functions

Kotlin 为我们提供了 extension 函数。这些是什么?它们就像是现有数据类型/类之上的临时 function

例如,如果我们想计算一个字符串中的单词数,下面是一个传统的函数:

fun countWords(text:String):Int { 
    return text.trim() 
            .split(Pattern.compile("\s+")) 
            .size 
} 

我们会将 String 传递给函数,让我们的逻辑计算单词,然后返回值。

但是,如果有一种方法可以在 String 实例本身上调用此函数,您不觉得总是会更好吗? Kotlin 允许我们执行这样的操作。

看看下面的程序:

fun String.countWords():Int { 
    return trim() 
            .split(Pattern.compile("\s+")) 
            .size 
} 

仔细查看函数声明。我们将函数声明为 String.countWords(),而不仅仅是像以前那样的 countWords;这意味着现在应该在 String 实例上调用它,就像 String 类的成员函数一样。就像下面的代码:

fun main(args: Array<String>) { 
    val counts = "This is an example StringnWith multiple words".countWords() 
    println("Count Words: $counts") 
} 

您可以查看以下输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

Default arguments

我们可能有一个要求,我们想要一个函数的 optional 参数。考虑以下示例:

fun Int.isGreaterThan(anotherNumber:Int):Boolean { 
    return this>anotherNumber 
} 

我们想让 anotherNumber 参数可选;如果它没有作为参数传递,我们希望它是 0。传统的方法是有另一个不带任何参数的重载函数,它会用 0 调用这个函数,如下所示:

fun Int.isGreaterThan(anotherNumber:Int):Boolean { 
    return this>anotherNumber 
} 
fun Int.isGreaterThan():Boolean { 
    return this.isGreaterThan(0) 
} 

然而,在 Kotlin 中,事情非常简单明了,它们不需要我们为了使参数可选而再次定义函数。为了使参数成为可选,Kotlin 为我们提供了默认参数,通过它我们可以在声明时立即指定函数的默认值。

以下是修改后的功能:

fun Int.isGreaterThan(anotherNumber:Int=0):Boolean { 
    return this>anotherNumber 
} 

我们将使用 main 函数,如下所示:

fun main(args: Array<String>) { 
    println("5>0: ${5.isGreaterThan()}") 
    println("5>6: ${5.isGreaterThan(6)}") 
} 

对于第一个,我们跳过了参数,对于第二个,我们提供了 6。所以,对于第一个 一个,输出应该是真的(因为5确实大于0),而对于第二个,它应该是假的(因为 5 不大于 6)。

以下屏幕截图输出证实了这一点:

读书笔记《functional-kotlin》函数、函数类型和副作用

Nested functions

Kotlin 允许您将函数嵌套在另一个函数中。我们可以在另一个函数中声明和使用 function

当您在另一个函数中声明 function 时,嵌套函数、可见性将仅保留在父函数中,无法从外部。

所以,让我们举个例子:

fun main(args: Array<String>) { 
    fun nested():String { 
        return "String from nested function" 
    } 
    println("Nested Output: ${nested()}") 
} 

在前面的程序中,我们在main 函数中声明并使用了一个函数——nested()

如果您好奇,以下是输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

因此,当我们在函数中掌握了基础知识后,让我们继续进行函数式编程。在下一节中,我们将了解函数类型。

Function types in functional programming


函数式编程的主要目标之一是实现模块化编程。副作用(本章后面定义的一个功能术语)通常是错误的来源;函数式编程希望你完全避免副作用。

为此,函数式编程定义了以下类型的函数:

  • Lambda functions as property
  • High order functions
  • Pure functions
  • Partial functions

在本节中,我们将讨论这些概念中的每一个,以便牢牢掌握函数式编程范式。

那么,让我们开始使用 lambda。

Lambda


Lambda,也可以称为匿名函数,拥有一流的citizen 在 Kotlin 中的支持。虽然在 Java 中,仅从 Java 8 开始支持 lambda,但在 Kotlin 中,您可以将 Kotlin 与 JVM 6 一起使用,因此在 Kotlin 中 lambda 确实没有障碍。

现在,我们正在讨论 lambda、匿名类(或对象)和匿名函数,但它们是什么?让我们探索一下。

为了通用,lambda 或 lambda 表达式通常意味着 匿名函数 ,即没有名字的函数,可以赋值给变量,作为参数传递,或者从另一个函数返回。它是一种嵌套函数,但更通用、更灵活。你也可以说所有的 lambda 表达式都是函数,但不是每个函数都是 lambda 表达式。匿名和未命名为 lambda 表达式带来了很多好处,我们将很快讨论。

正如我之前提到的,并非所有语言都支持 lambda,而 Kotlin 是最稀有的语言之一,它为 lambda 提供了广泛的支持。

那么,为什么叫 lambda 呢?现在让我们挖掘一点历史。

Note

Lambda, Λ, λ(大写 Λ,小写 λ)是希腊字母表的第 11th 个字母。发音:lám(b)da。来源:https://en.wikipedia.org/wiki/Lambda

在 1930 年代,当时在普林斯顿大学学习数学的 Alonzo Church 使用希腊字母表,特别是 lambda,来表示他所谓的 函数< /跨度>。需要注意的是,当时计算中只有匿名函数;现代命名函数的概念尚未出现。

因此,通过 Alonzo Church 的这种做法,lambda 这个词被附加到匿名函数(当时唯一的函数类型)上,迄今为止,它的引用方式相同。

Note

阿朗佐·丘奇(Alonzo Church,1903 年 6 月 14 日-1995 年 8 月 11 日),美国数学家和逻辑学家,对数理逻辑和理论计算机科学的基础做出了重大贡献。他最著名的是 lambda 演算,Church-Turing 论文,证明了 Entscheidungsproblem 的不可判定性Frege-Church本体和Church-Rosser< /em> 定理。他还研究语言哲学(例如,Church,1970)。来源:https://en.wikipedia.org/wiki/Alonzo_Church

你不认为我们有足够的理论吗?我们现在不应该专注于学习 lambda 实际上是什么,或者它到底是什么样子吗?我们将看看 lambda 在 Kotlin 中的样子,但我们更愿意先向您介绍 Java 中的 lambda,然后再介绍 Kotlin,以使您充分了解 lambda 在 Kotlin 中的强大功能以及 first 的确切含义-阶级公民的支持。您还将了解 Java 中的 lambda 和 Kotlin 之间的区别。

考虑以下 Java 示例。这是一个简单的示例,我们将接口的实例传递给方法,并且在该方法中,我们从实例调用方法:

public class LambdaIntroClass { 
    interface SomeInterface { 
        void doSomeStuff(); 
    } 
    private static void invokeSomeStuff(SomeInterface someInterface) { 
        someInterface.doSomeStuff(); 
    } 
    public static void main(String[] args) { 
        invokeSomeStuff(new SomeInterface() { 
            @Override 
            public void doSomeStuff() { 
                System.out.println("doSomeStuff invoked"); 
            } 
        }); 
    } 
} 

所以,在这个程序中,SomeInterface是一个接口(LambdaIntroClass的内部接口),只有一个方法——doSomeStuff()。静态方法(它是静态的,以便 main 方法可以轻松访问)invokeSomeStuff 采用 SomeInterface 并调用其方法 doSomeStuff()

这是一个简单的例子;现在,让我们让它变得更简单:让我们添加 lambda 到它。查看以下更新的代码:

public class LambdaIntroClass { 
    interface SomeInterface { 
        void doSomeStuff(); 
    } 
    private static void invokeSomeStuff(SomeInterface someInterface) { 
        someInterface.doSomeStuff(); 
    }   
    public static void main(String[] args) { 
        invokeSomeStuff(()->{ 
                System.out.println("doSomeStuff called"); 
        }); 
    } 
} 

因此,在这里,SomeInterfaceinvokeSomeStuff() 的定义保持不变。唯一的区别在于传递 SomeInterface 的实例。我们没有使用新的 SomeInstance 创建一个 SomeInstance 的实例,而是编写了一个看起来非常漂亮的表达式(粗体)像数学函数表达式(显然除了 System.out.println())。该表达式称为 lambda 表达式

那不是很棒吗?您不需要创建接口的实例,然后覆盖方法和所有这些东西。你所做的只是一个简单的表达。该表达式将用作接口内 doSomeStuff() 方法的方法体。

两个程序的输出是相同的;如以下屏幕截图所示:

读书笔记《functional-kotlin》函数、函数类型和副作用

Java 没有任何 lambda 类型。您只能使用 lambda 在旅途中创建类和接口的实例。 Java 中 lambda 的唯一好处是它使 Java 程序更易于(人类)阅读并减少了行数。

我们实际上不能为此责怪 Java。毕竟,Java 基本上是一门纯面向对象的语言。另一方面,Kotlin 是面向对象和函数式编程范式的完美结合。它使两个世界更紧密地联系在一起。用我们的话来说,如果您想在具有面向对象编程知识的基础上开始函数式编程,那么 Kotlin 是最好的语言。

所以,没有更多的讲座,让我们继续看代码。现在让我们看看同样的程序在 Kotlin 中的样子:

fun invokeSomeStuff(doSomeStuff:()->Unit) { 
    doSomeStuff() 
} 
fun main(args: Array<String>) { 
    invokeSomeStuff({ 
        println("doSomeStuff called"); 
    }) 
} 

是的,这就是完整的程序(嗯,除了 import 语句和包名)。我知道你有点困惑;你问它是否真的是同一个程序?那么接口定义在哪里呢?好吧,在 Kotlin 中这实际上并不是必需的。

 invokeSomeStuff() 函数 实际上是一个高阶函数(接下来介绍);我们在那里传递我们的 lambda,它直接调用该函数。

很棒,不是吗? Kotlin 有很多与 lambda 相关的特性。让我们来看看它们。

Function as property

Kotlin 还允许我们将 functions 作为属性。函数作为属性意味着函数可以用作属性。

例如,举个例子:

fun main(args: Array<String>) { 
    val sum = { x: Int, y: Int -> x + y }  
    println("Sum ${sum(10,13)}") 
    println("Sum ${sum(50,68)}") 
} 

在前面的程序中,我们创建了一个属性,sum,它实际上包含一个函数,用于将传递给它的两个数字相加。

虽然 sum 是一个 val 属性,但它拥有的是一个函数(或 lambda),我们可以像调用我们调用的常用函数;那里根本没有区别。

如果你很好奇,以下是输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

现在,让我们讨论一下 lambda 的语法。

在 Kotlin 中,lambda 总是被花括号包围。这使得 lambda 易于识别,不像在 Java 中,参数/参数位于花括号之外。在 Kotlin 中,参数/参数位于大括号内,由 (->) 与函数的逻辑隔开。 lambda 中的最后一条语句(可能只是变量/属性名称或另一个函数调用)被视为返回语句。因此,无论对 lambda 的最后一条语句的评估是什么,都是 lambda 的返回值。

此外,如果您的函数是单个 parameter 函数,您也可以跳过属性名称。那么,如果不指定名称,如何使用该参数? Kotlin 为您提供了一个默认的 it 属性,用于您不指定属性名称的单参数 lambda。

所以,让我们修改之前的 lambda 以添加它。看看下面的代码:

reverse = { 
        var n = it 
        var revNumber = 0 
        while (n>0) { 
            val digit = n%10 
            revNumber=revNumber*10+digit 
            n/=10 
        } 
        revNumber 
} 

我们跳过了完整的程序和输出,因为它们保持不变。

Note

您一定注意到我们将函数参数值分配给了另一个 var 属性(无论是在参数命名时还是使用 it )。原因是,在 Kotlin 中,函数参数是不可变的,但是对于倒数程序,我们需要一种改变值的方法;因此,我们将值分配给一个可变的 var 属性。

现在,您将 lambda 作为属性,但是它们的数据类型呢?每个属性/变量都有一个数据类型(即使类型是推断出来的),那么 lambdas 呢?让我们看一下下面的例子:

fun main(args: Array<String>) { 
    val reverse:(Int)->Int//(1) 
    reverse = {number -> 
        var n = number 
        var revNumber = 0 
        while (n>0) { 
            val digit = n%10 
            revNumber=revNumber*10+digit 
            n/=10 
        } 
        revNumber 
    }// (2) 
    println("reverse 123 ${reverse(123)}") 
    println("reverse 456 ${reverse(456)}") 
    println("reverse 789 ${reverse(789)}") 
} 

在前面的程序中,我们将一个 reverse属性声明为一个函数。在 Kotlin 中,当您将属性声明为函数时,您应该在大括号内提及参数/参数的数据类型,然后是箭头,然后是函数的返回类型;如果函数不打算返回一些东西,你应该提到Unit。在将函数声明为属性时,您无需指定参数/参数名称,并且在将函数定义/分配给属性时,您可以跳过提供属性的数据类型。

以下是输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

因此,我们对 lambda 和函数作为 Kotlin 中的属性有一个很好的概念。现在,让我们继续使用高阶函数。

High order functions


高阶函数是接受另一个 function 作为参数或返回另一个函数的函数。我们刚刚看到了如何将函数用作属性,因此很容易看出我们可以接受另一个函数作为参数,或者我们可以从函数返回另一个函数。如前所述,从技术上讲,function 接收或返回另一个函数(可能不止一个)或两者都调用高阶函数

在 Kotlin 的第一个 lambda 示例中,invokeSomeStuff 函数是一个高阶函数。

下面是另一个高阶函数的例子:

fun performOperationOnEven(number:Int,operation:(Int)->Int):Int { 
    if(number%2==0) { 
        return operation(number) 
    } else { 
        return number 
    } 
} 
fun main(args: Array<String>) { 
    println("Called with 4,(it*2): ${performOperationOnEven(4, 
            {it*2})}") 
    println("Called with 5,(it*2): ${performOperationOnEven(5, 
            {it*2})}") 
} 

在前面的程序中,我们创建了一个高阶函数——performOperationOnEven,它需要一个 Int 和一个 lambda 操作来执行它Int。唯一的问题是,如果 Int 是偶数,该函数只会对提供的 Int 执行该操作。

这还不够简单吗?让我们看一下以下输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

在我们之前的所有示例中,我们看到了如何将函数 (lambda) 传递给另一个函数。然而,这并不是高阶函数的唯一特征。高阶函数还允许您从中返回一个函数。

那么,让我们来探索一下。看看下面的例子:

fun getAnotherFunction(n:Int):(String)->Unit { 
    return { 
        println("n:$n it:$it") 
    } 
} 
fun main(args: Array<String>) { 
    getAnotherFunction(0)("abc") 
    getAnotherFunction(2)("def") 
    getAnotherFunction(3)("ghi") 
} 

在前面的程序中,我们创建了一个函数 getAnotherFunction,它将接受一个 Int 参数 并返回一个函数接受 String 值并返回 Unitreturn 函数同时打印它的参数(一个 String)和它的父参数(一个 Int )。

请参阅以下输出:

读书笔记《functional-kotlin》函数、函数类型和副作用

Note

在 Kotlin 中,从技术上讲,您可以将高阶函数嵌套到任何深度。然而,这样做弊大于利,甚至会破坏可读性。所以,你应该避免它们。

Pure functions and side effects


所以,我们已经了解了 lambda 和高阶函数。它们是函数式编程中最有趣和最重要的两个主题。在本节中,我们将讨论副作用和纯函数。

所以,让我们从定义副作用开始。然后我们将逐渐走向pure 功能。

Side effects

在计算机程序中,当 function 修改其自身范围之外的任何对象/数据时,称为 副作用。例如,我们经常编写函数来修改全局或静态属性、修改其中一个参数、引发异常、将数据写入显示或文件,甚至调用具有副作用的另一个函数。

例如,看看下面的程序:

class Calc { 
    var a:Int=0 
    var b:Int=0 
    fun addNumbers(a:Int = this.a,b:Int = this.b):Int {  
        this.a = a 
        this.b = b 
        return a+b 
    } 
} 
fun main(args: Array<String>) { 
    val calc = Calc() 
    println("Result is ${calc.addNumbers(10,15)}") 
} 

前面的程序是一个简单的面向对象程序。但是,它包含副作用。 addNumbers() 函数会修改 Calc 类的状态,这在函数式编程中是不好的做法。

虽然我们无法避免一些函数的副作用,尤其是在我们访问 IO 和/或 数据库 等时,但应尽可能避免副作用。

Pure functions

纯函数的定义说,如果一个函数的返回值完全依赖于它的参数/参数,那么这个function 可以称为 纯函数。所以,如果我们将一个函数声明为 fun func1(x:Int):Int,那么它的返回值将严格依赖于它的参数, x;比如说,如果你调用 func1 的值为 3 N 次,那么,对于每次调用,其返回值将相同。

定义还说,纯函数不应该主动或被动地引起副作用,即不应该直接引起副作用,也不应该调用任何其他引起副作用的函数。

纯函数可以是 lambda 或命名函数。

那么,为什么它们被称为纯函数呢?原因很简单。编程函数起源于数学函数。随着时间的推移,编程函数演变为包含多个任务并执行与传递参数的处理没有直接关系的匿名操作。因此,那些仍然类似于数学函数的函数称为纯函数。

所以,让我们修改我们之前的程序,使其成为一个纯函数:

fun addNumbers(a:Int = 0,b:Int = 0):Int { 
    return a+b 
} 
 
fun main(args: Array<String>) { 
    println() 
} 

很容易,不是吗?我们正在跳过输出,因为程序非常简单。

Summary


在本章中,我们了解了函数、如何使用它们以及它们的分类。我们还介绍了 lambda 和高阶函数。我们了解了纯函数和副作用。

下一章将带您更深入地了解函数。正如我已经说过的,您需要掌握函数才能正确学习函数式编程。那你还在等什么?马上翻页。