读书笔记《functional-kotlin》函数、函数类型和副作用
函数式编程围绕不变性和函数的概念展开。我们在上一章了解了不可变性。在讨论不变性时,我们还对纯函数有所了解。纯函数基本上是函数式编程必须提供的众多类型之一(但可能是最重要的一种)。
本章将围绕函数展开。要深入了解函数式编程,您需要强大的函数基础。为了让您的概念清晰,我们将从普通的 Kotlin 函数开始,然后逐步讨论函数式编程定义的函数的抽象概念。我们还将看到它们在 Kotlin 中的实现。
在本章中,我们将介绍以下主题:
- Functions in Kotlin
- Function types
- Lambda
- High order functions
- Understanding side effects and pure functions
所以,让我们从定义函数开始。
函数是编程中最重要的部分之一。我们每周都会为我们的项目编写大量函数。函数也是编程基础的一部分。要学习函数式编程,我们必须清楚地了解函数的概念。在本节中,我们将介绍函数的基础知识,以便让您为本章的下一部分做好准备,我们将讨论抽象函数概念及其在 Kotlin 中的实现。
所以,让我们从定义函数开始。
不是很清楚?我们将解释,但首先,让我们了解为什么我们应该编写函数。简而言之,一个函数的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 中,函数通常如下所示:
在 Kotlin 中,函数声明以 fun
关键字开头,后跟函数名称,然后是大括号。在大括号内,我们可以指定函数参数(可选)。在大括号之后,会有一个冒号(:
)和返回类型,它指定了要返回的值/对象的数据类型(如果你不这样做,你可以跳过返回类型'不打算从函数返回任何东西;在这种情况下,默认返回类型 Unit
将被分配给函数)。之后是函数体,用花括号覆盖(花括号对于单表达式函数也是可选的,接下来将在 Chapter 5,更多关于函数)。
Note
Unit
是 Kotlin 中的一种数据类型。 Unit
是它自己的一个单例实例,并拥有一个 Unit
本身的值。 Unit
对应Java中的void
,但与void
有很大不同。虽然 void
在 Java 中没有任何意义,并且 void
不能包含任何内容,但我们有 Nothing
在 Kotlin 中用于此目的,这表明函数永远不会成功完成(由于异常或无限循环)。
现在,那些返回类型、参数(参数)和函数体是什么?让我们来探索一下。
下面是一个比之前显示的抽象示例更现实的 function 示例:
现在,看一下函数每个部分的以下解释:
- Function arguments/parameters: These are the data (unless lambda) for the function to work on. In our example,
a
andb
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 thereturn
type andreturn 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 章,更多关于函数的内容。
在 Kotlin 中,通过利用 Pair
类型和解构声明,我们可以从一个函数返回两个变量。考虑以下示例:
在前面的程序中,在注释 (1)
上,我们创建了一个返回 Pair<Int,String>
的函数;价值。
在评论 (2)
中,我们使用该函数的方式似乎返回两个变量。实际上,解构声明允许您解构 data class
/Pair
并在独立变量中获取其基础值。当此功能与函数一起使用时,函数似乎返回多个值,尽管它只返回一个值,即 Pair
value 或另一个 数据类
。
Kotlin 为我们提供了 extension 函数。这些是什么?它们就像是现有数据类型/类之上的临时 function。
例如,如果我们想计算一个字符串中的单词数,下面是一个传统的函数:
我们会将 String
传递给函数,让我们的逻辑计算单词,然后返回值。
但是,如果有一种方法可以在 String
实例本身上调用此函数,您不觉得总是会更好吗? Kotlin 允许我们执行这样的操作。
看看下面的程序:
仔细查看函数声明。我们将函数声明为 String.countWords()
,而不仅仅是像以前那样的 countWords
;这意味着现在应该在 String
实例上调用它,就像 String
类的成员函数一样。就像下面的代码:
您可以查看以下输出:
我们可能有一个要求,我们想要一个函数的 optional 参数。考虑以下示例:
我们想让 anotherNumber
参数可选;如果它没有作为参数传递,我们希望它是 0
。传统的方法是有另一个不带任何参数的重载函数,它会用 0
调用这个函数,如下所示:
然而,在 Kotlin 中,事情非常简单明了,它们不需要我们为了使参数可选而再次定义函数。为了使参数成为可选,Kotlin 为我们提供了默认参数,通过它我们可以在声明时立即指定函数的默认值。
以下是修改后的功能:
我们将使用 main
函数,如下所示:
对于第一个,我们跳过了参数,对于第二个,我们提供了 6
。所以,对于第一个 一个,输出应该是真的(因为5
确实大于0
),而对于第二个,它应该是假的(因为 5
不大于 6
)。
以下屏幕截图输出证实了这一点:
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 示例。这是一个简单的示例,我们将接口的实例传递给方法,并且在该方法中,我们从实例调用方法:
所以,在这个程序中,SomeInterface
是一个接口(LambdaIntroClass
的内部接口),只有一个方法——doSomeStuff()
。静态方法(它是静态的,以便 main
方法可以轻松访问)invokeSomeStuff
采用 SomeInterface
并调用其方法 doSomeStuff()
。
这是一个简单的例子;现在,让我们让它变得更简单:让我们添加 lambda 到它。查看以下更新的代码:
因此,在这里,SomeInterface
和 invokeSomeStuff()
的定义保持不变。唯一的区别在于传递 SomeInterface
的实例。我们没有使用新的 SomeInstance
创建一个 SomeInstance
的实例,而是编写了一个看起来非常漂亮的表达式(粗体)像数学函数表达式(显然除了 System.out.println()
)。该表达式称为 lambda 表达式。
那不是很棒吗?您不需要创建接口的实例,然后覆盖方法和所有这些东西。你所做的只是一个简单的表达。该表达式将用作接口内 doSomeStuff()
方法的方法体。
两个程序的输出是相同的;如以下屏幕截图所示:
Java 没有任何 lambda 类型。您只能使用 lambda 在旅途中创建类和接口的实例。 Java 中 lambda 的唯一好处是它使 Java 程序更易于(人类)阅读并减少了行数。
我们实际上不能为此责怪 Java。毕竟,Java 基本上是一门纯面向对象的语言。另一方面,Kotlin 是面向对象和函数式编程范式的完美结合。它使两个世界更紧密地联系在一起。用我们的话来说,如果您想在具有面向对象编程知识的基础上开始函数式编程,那么 Kotlin 是最好的语言。
所以,没有更多的讲座,让我们继续看代码。现在让我们看看同样的程序在 Kotlin 中的样子:
是的,这就是完整的程序(嗯,除了 import
语句和包名)。我知道你有点困惑;你问它是否真的是同一个程序?那么接口定义在哪里呢?好吧,在 Kotlin 中这实际上并不是必需的。
invokeSomeStuff()
函数 实际上是一个高阶函数(接下来介绍);我们在那里传递我们的 lambda,它直接调用该函数。
很棒,不是吗? Kotlin 有很多与 lambda 相关的特性。让我们来看看它们。
Kotlin 还允许我们将 functions 作为属性。函数作为属性意味着函数可以用作属性。
例如,举个例子:
在前面的程序中,我们创建了一个属性,sum
,它实际上包含一个函数,用于将传递给它的两个数字相加。
虽然 sum
是一个 val
属性,但它拥有的是一个函数(或 lambda),我们可以像调用我们调用的常用函数;那里根本没有区别。
如果你很好奇,以下是输出:
现在,让我们讨论一下 lambda 的语法。
在 Kotlin 中,lambda 总是被花括号包围。这使得 lambda 易于识别,不像在 Java 中,参数/参数位于花括号之外。在 Kotlin 中,参数/参数位于大括号内,由 (->
) 与函数的逻辑隔开。 lambda 中的最后一条语句(可能只是变量/属性名称或另一个函数调用)被视为返回语句。因此,无论对 lambda 的最后一条语句的评估是什么,都是 lambda 的返回值。
此外,如果您的函数是单个 parameter 函数,您也可以跳过属性名称。那么,如果不指定名称,如何使用该参数? Kotlin 为您提供了一个默认的 it
属性,用于您不指定属性名称的单参数 lambda。
所以,让我们修改之前的 lambda 以添加它。看看下面的代码:
我们跳过了完整的程序和输出,因为它们保持不变。
Note
您一定注意到我们将函数参数值分配给了另一个 var
属性(无论是在参数命名时还是使用 it
)。原因是,在 Kotlin 中,函数参数是不可变的,但是对于倒数程序,我们需要一种改变值的方法;因此,我们将值分配给一个可变的
var
属性。
现在,您将 lambda 作为属性,但是它们的数据类型呢?每个属性/变量都有一个数据类型(即使类型是推断出来的),那么 lambdas 呢?让我们看一下下面的例子:
在前面的程序中,我们将一个 reverse
属性声明为一个函数。在 Kotlin 中,当您将属性声明为函数时,您应该在大括号内提及参数/参数的数据类型,然后是箭头,然后是函数的返回类型;如果函数不打算返回一些东西,你应该提到Unit
。在将函数声明为属性时,您无需指定参数/参数名称,并且在将函数定义/分配给属性时,您可以跳过提供属性的数据类型。
以下是输出:
因此,我们对 lambda 和函数作为 Kotlin 中的属性有一个很好的概念。现在,让我们继续使用高阶函数。
高阶函数是接受另一个 function 作为参数或返回另一个函数的函数。我们刚刚看到了如何将函数用作属性,因此很容易看出我们可以接受另一个函数作为参数,或者我们可以从函数返回另一个函数。如前所述,从技术上讲,function 接收或返回另一个函数(可能不止一个)或两者都调用高阶函数。
在 Kotlin 的第一个 lambda 示例中,invokeSomeStuff
函数是一个高阶函数。
下面是另一个高阶函数的例子:
在前面的程序中,我们创建了一个高阶函数——performOperationOnEven
,它需要一个 Int
和一个 lambda 操作来执行它Int
。唯一的问题是,如果 Int
是偶数,该函数只会对提供的 Int
执行该操作。
这还不够简单吗?让我们看一下以下输出:
在我们之前的所有示例中,我们看到了如何将函数 (lambda) 传递给另一个函数。然而,这并不是高阶函数的唯一特征。高阶函数还允许您从中返回一个函数。
那么,让我们来探索一下。看看下面的例子:
在前面的程序中,我们创建了一个函数 getAnotherFunction
,它将接受一个 Int
参数 并返回一个函数接受 String
值并返回 Unit
。 return
函数同时打印它的参数(一个 String
)和它的父参数(一个 Int
)。
请参阅以下输出:
所以,我们已经了解了 lambda 和高阶函数。它们是函数式编程中最有趣和最重要的两个主题。在本节中,我们将讨论副作用和纯函数。
所以,让我们从定义副作用开始。然后我们将逐渐走向pure 功能。
在计算机程序中,当 function 修改其自身范围之外的任何对象/数据时,称为 副作用。例如,我们经常编写函数来修改全局或静态属性、修改其中一个参数、引发异常、将数据写入显示或文件,甚至调用具有副作用的另一个函数。
例如,看看下面的程序:
前面的程序是一个简单的面向对象程序。但是,它包含副作用。 addNumbers()
函数会修改 Calc
类的状态,这在函数式编程中是不好的做法。
虽然我们无法避免一些函数的副作用,尤其是在我们访问 IO 和/或 数据库 等时,但应尽可能避免副作用。
纯函数的定义说,如果一个函数的返回值完全依赖于它的参数/参数,那么这个function
可以称为 纯函数。所以,如果我们将一个函数声明为 fun func1(x:Int):Int
,那么它的返回值将严格依赖于它的参数, x
;比如说,如果你调用 func1
的值为 3 N 次,那么,对于每次调用,其返回值将相同。
定义还说,纯函数不应该主动或被动地引起副作用,即不应该直接引起副作用,也不应该调用任何其他引起副作用的函数。
纯函数可以是 lambda 或命名函数。
那么,为什么它们被称为纯函数呢?原因很简单。编程函数起源于数学函数。随着时间的推移,编程函数演变为包含多个任务并执行与传递参数的处理没有直接关系的匿名操作。因此,那些仍然类似于数学函数的函数称为纯函数。
所以,让我们修改我们之前的程序,使其成为一个纯函数:
很容易,不是吗?我们正在跳过输出,因为程序非常简单。