快捷搜索:  as

Kotlin极简教程:第8章 函数式编程

原文链接:https://github.com/EasyKotlin

值便是函数,函数便是值。所有函数都破费函数,所有函数都临盆函数。

"函数式编程", 又称泛函编程, 是一种"编程范式"(programming paradigm),也便是若何编写法度榜样的措施论。它的根基是 λ 演算(lambda calculus)。λ演算可以吸收函数算作输入(参数)和输出(返回值)。

和指令式编程比拟,函数式编程的思维要领加倍重视函数的谋略。它的主要思惟是把问题的办理规划写成一系列嵌套的函数调用。

就像在OOP中,统统皆是工具,编程的是由工具交合创造的天下;在FP中,统统皆是函数,编程的天下是由函数交合创造的天下。

函数式编程中最古老的例子莫过于1958年被创造出来的Lisp了。Lisp由约翰·麦卡锡(John McCarthy,1927-2011)在1958年基于λ演算所创造,采纳抽象数据列表与递归作符号演算来衍生人工智能。较今世的例子包括Haskell、ML、Erlang等。今世的编程说话对函数式编程都做了不合程度的支持,例如:JavaScript, Coffee Script,PHP,Perl,Python, Ruby, C# , Java 等等(这将是一个赓续增长的列表)。

函数式说话在Java 虚拟机(JVM)平台上也迅速地崭露锋芒,例如Scala 、Clojure ; .NET 平台也不例外,例如:F# 。

函数作为Kotlin中的一等公夷易近,可以像其他工具一样作为函数的输入与输出。关于对函数式编程的支持,相对付Scala的学院派风格,Kotlin则是纯的的工程派:实用性、简洁性上都要比Scala要好。

本章我们来一路进修函数式编程以及在Kotlin中应用函数式编程的相关内容。

8.1 函数式编程概述

Kotlin极简教程

函数式编程思惟是一个异常古老的思惟。我们简述如下:

我们就从1900 年 David Hilbert 的第 10 问题(能否经由过程有限步骤来鉴定不定方程是否存在有理整数解?) 开始提及吧。

1920,Schönfinkel,组合子逻辑(combinatory logic)。直到 Curry Haskell 1927 在普林斯顿大年夜学当讲师时从新发清楚明了 Moses Schönfinkel 关于组合子逻辑的成果。Moses Schönfinkel的成果预言了很多 Curry 在做的钻研,于是他就跑去哥廷根大年夜学与认识Moses Schönfinkel事情的Heinrich Behmann、Paul Bernays两人一路事情,并于 1930 年以一篇组合子逻辑的论文拿到了博士学位。Curry Brooks Haskell 全部职业生涯都在钻研组合子,实际创始了这个钻研领域,λ演算顶用单参数函数来表示多个参数函数的措施被称为 Currying (柯里化),虽然 Curry 同砚多次指出这个着实是 Schönfinkel 已经搞出来的,不过其他人都是由于他用了才知道,以是这名字就这定下来了;并且有三门编程说话以他的名字命名,分手是:Curry, Brooks, Haskell。Curry 在 1928 开始开拓类型系统,他搞的是基于组合子的 polymorphic,Church 则建立了基于函数的简单类型系统。

1929, 哥德尔(Kurt Gödel )完整性定理。Gödel 首先证清楚明了一个形式系统中的所有公式都可以表示为自然数,并可以从一自然数反过来得出响应的公式。这对付本日的法度榜样员都来说,数字编码、法度榜样即数据谋略机道理最核心、最基础的知识,在那个期间却脑洞大年夜开的创见。

1933,λ 演算。 Church 在 1933 年搞出来一套以纯λ演算为根基的逻辑,以期对数学进行形式化描述。 λ 演算和递归函数理论便是函数式编程的根基。

1936,确定性问题(decision problem,德文 Entscheidungsproblem (发音 [ɛntˈʃaɪ̯dʊŋspʁoˌbleːm])。 Alan Turing 和 Alonzo Church,两人在同在1936年自力给出了否定谜底。

1935-1936这个光阴段上,我们有了三个有效谋略模型:通用图灵机、通用递归函数、λ可定义。Rosser 1939 年正式确认这三个模型是等效的。

1953-1957,FORTRAN (FORmula TRANslating ),John Backus。1952 年 Halcombe Laning 提出了直接输入数学公式的设想,并制作了 GEORGE编译器演示该设法主见。受这个设法主见启迪,1953 年 IBM 的 John Backus 团队给 IBM 704 主机研发数学公式翻译系统。第一个 FORTRAN (FORmula TRANslating 的缩写)编译器 1957.4 正式发行。FORTRAN 法度榜样的代码行数比汇编少20倍。FORTRAN 的成功,让很多人熟识到直接把代数公式输入进电脑是可行的,并开始愿望能用某种形式说话直接把自己的钻研内容输入到电脑里进交运算。John Backus 在1970年代搞了 FP 说话,1977 年颁发。虽然这门说话并不是最早的函数式编程说话,但他是 Functional Programming 这个词儿的创造者, 1977 年他的图灵奖演讲题为[“Can Programming Be Liberated From the von Neumann Style? A Functional Style and its Algebra of Programs”]

1956, LISP, John McCarthy。John McCarthy 1956年在 Dartmouth一台 IBM 704 上搞人工智能钻研时,就想到要一个代数列表处置惩罚(algebraic list processing)说话。他的项目必要用某种形式说话来编写语句,以记录关于天下的信息,而他感到列表布局这种形式挺相宜,既方便编写,也方便推演。于是就创造了LISP。正由于是在 IBM 704 上开搞的,以是 LISP 的表处置惩罚函数才会有奇葩的名字: car/cdr 什么的。着实是取 IBM704 机械字的不合部分,c=content of,r=register number, a=address part, d=decrement part 。

8.1.1 面向工具编程(OOP)与面向函数编程(FOP)

面向工具编程(OOP)

在OOP中,统统皆是工具。

在面向工具的敕令式(imperative)编程说话里面,构建全部天下的根基是类和类之间沟通用的消息,这些都可以用类图(class diagram)来表述。《设计模式:可复用面向工具软件的根基》(Design Patterns: Elements of Reusable Object-Oriented Software,作者ErichGamma、Richard Helm、Ralph Johnson、John Vlissides)一书中,在每一个模式的阐明里都附上了至少一幅类图。

OOP 的天下提倡开拓者针对详细问题建立专门的数据布局,相关的专门操作行径以“措施”的形式附加在数据布局上,自顶向下地来构建其编程天下。

OOP追求的是万事万物皆工具的理念,自然地弱化了函数。例如:函数无法作为通俗数据那样来通报(OOP在函数指针上的约束),以是在OOP中有各类各样的、五花八门的设计模式。

GoF所著的《设计模式-可复用面向工具软件的根基》从面向工具设计的角度启程的,经由过程对封装、承袭、多态、组合等技巧的反复应用,提炼出一些可重复应用的面向工具设计技术。而多态在此中又是重中之重。

多态、面向接口编程、依附反转等术语,描述的思惟着实是相同的。这种反转模式实现了模块与模块之间的解耦。这样的架构是壮实的, 而为了实现这样的壮实系统,在系统架构中基础都必要应用多态性。

绝大年夜部分设计模式的实现都离不开多态性的思惟。换一种说法便是,这些设计模式背后的本色着实便是OOP的多态性,而OOP中的多态本色上又是受约束的函数指针。

引用Charlie Calverts对多态的描述: “多态性是容许你将父工具设置成为和一个或更多的他的子工具相等的技巧,赋值之后,父工具就可以根据当前赋值给它的子工具的特点以不合的要领运作。”

简单的说,便是一句话:容许将子类类型的指针赋值给父类类型的指针。而我们在OOP中的那么多的设计模式,着实便是在OOP的多态性的约束规则下,对这些函数指针的调用模式的总结。

很多设计模式,在函数式编程中都可以用高阶函数来代替实现:

Kotlin极简教程

面向函数编程(FOP)

在FP中,统统皆是函数。

函数式编程(FP)是关于不变性和函数组合的一种编程范式。

函数式编程说话实现重用的思路很不一样。函数式说话提倡在有限的几种关键数据布局(如list、set、map)上 , 运用函数的组合 ( 高阶函数) 操作,自底向上地来构建天下。

当然,我们在工程实践中,是不能极度地追求纯函数式的编程的。一个简单的缘故原由便是:机能和效率。例如:对付有状态的操作,敕令式操作平日会比声明式操作更有效率。纯函数式编程是办理某些问题的巨大年夜对象,然则在别的的一些问题场景中,并不适用。由于副感化老是真实存在。

OOP爱好自顶向下架构层层分化(解构),FP爱好自底向上层层组合(复合)。 而实际上,编程的本色便是次化分化与复合的历程。经由过程这样的历程,创造一个美妙的逻辑之塔天下。

我们常常说一些代码片段是优雅的或美不雅的,实际上意味着它们更轻易被人类有限的思维所处置惩罚。

对付法度榜样的复合而言,好的代码是它的外面积要比体积增长的慢。

代码块的“外面积”是我们复合代码块时所必要的信息(接口API协议定义)。代码块的“体积”便是接口内部的实现逻辑(API内部的实今世码)。

在OOP中,一个抱负的工具应该是只裸露它的抽象接口(纯外面, 无体积),其措施则扮演箭头的角色。假如为了理解一个工具若何与其他工具进行复合,当你发明不得不深入掘客工具的实现之时,此时你所用的编程范式的蓝本上风就荡然无存了。

FP经由过程函数组合来构造其逻辑系统。FP倾向于把软件分化为其必要履行的行径或操作,而且平日采纳自底向上的措施。函数式编程也供给了异常强大年夜的对事物进行抽象和组合的能力。

在FP里面,函数是“一类公夷易近”(first-class)。它们可以像1, 2, "hello",true,工具…… 之类的“值”一样,在随意率性位置出生,经由过程变量,参数和数据布局通报到其它地方,可以在任何位置被调用。

而在OOP中,很多所谓面向工具设计模式(design pattern),都是由于面向工具说话没有first-class function(对应的是多态性),以是导致了每个函数必须被包在一个工具里面(受约束的函数指针)才能通报到其它地方。

均匀的数据布局 + 均匀的算法

在面向工具式的编程中,统统皆是工具(侧重数据布局、数据抽象,轻算法)。我们把它叫做:胖数据布局-瘦算法(FDS-TA)。

在面向函数式的编程中,统统皆是函数(侧重算法,轻数据布局)。我们把它叫做:瘦数据布局-胖算法(TDS-FA)。

可是,这个天下很繁杂,你怎么能说统统皆是啥呢?真实的编程天下,自然是均匀的数据布局结合均匀的算法(SDS-SA)来创造的。

我们在编程中,弗成能应用纯的工具(工具的行径措施着实便是函数),或者纯的函数(调用函数的工具、函数操作的数据着实便是数据布局)来创造一个完备的天下。假如数据布局是阴,算法是阳,那么在办理实际问题中,每每是阴阳交合而整天下。照样那句经典的:

法度榜样 = 均匀的数据布局 + 均匀的算法

我们用一幅图来简单阐明:

OOP vs FP (2).png

函数与映射

统统皆是映射。函数式编程的代码主要便是“对映射的描述”。我们说组合是编程的本色,着实,组合便是建立映射关系。

一个函数无非便是从输入到输出的映射,写成数学表达式便是:

f : X -> Y

p : Y -> Z

p(f) : X ->Z

用编程说话表达便是:

fun f(x:X) : Y{}

fun p(y:Y) : Z{}

fun fp(f: (X)->Y, p: (Y)->Z) : Z {

return {x -> p(f(x))}

}

8.1.2 函数式编程基础特点

在常常被引用的论文 “Why Functional Programming Matters” 中,作者 John Hughes 阐清楚明了模块化是成功编程的关键,而函数编程可以极大年夜地改进模块化。

在函数编程中,我们有一个内置的框架来开拓更小的、更简单的和更一样平常化的模块, 然后将它们组合在一路。

函数编程的一些基础特征包括:

函数是"第一等公夷易近"。

闭包(Closure)和高阶函数(Higher Order Function)。

Lambda演算与函数柯里化(Currying)。

怠惰谋略(lazy evaluation)。

应用递归作为节制流程的机制。

引用透明性。

没有副感化。

8.1.3 组合与范畴

函数式编程的本色是函数的组合,组合的本色是范畴(Category)。

和搞编程的一样,数学家爱好将问题赓续加以抽象从而将本色问题抽掏出来加以论证办理,范畴论便是这样一门以抽象的措施来处置惩罚数学观点的学科,主要用于钻研一些数学布局之间的映射关系(函数)。

在范畴论里,一个范畴(category)由三部分组成:

工具(object)

态射(morphism)

组合(composition)操作符

范畴的工具

这里的工具可以当作是一类器械,例如数学上的群,环,以及有理数,无理数等都可以归为一个工具。对应到编程说话里,可以理解为一个类型,比如说整型,布尔型等。

态射

态射指的是一种映射关系,简单理解,态射的感化便是把一个工具 A 里的值 a 映射为 另一个工具 B 里的值 b= f(a),这便是映射的观点。

态射的存在反应了工具内部的布局,这是范畴论用来钻研工具的主要伎俩:工具内部的布局特点是经由过程与其余工具的映射关系反应出来的,动静是相对的,范畴论经由过程钻研映射关系来达到探知工具的内部布局的目的。

组合操作符

组合操作符,用点(.)表示,用于将态射进行组合。组合操作符的感化是将两个态射进行组合,例如,假设存在态射 f: A -> B, g: B -> C, 则 g.f : A -> C.

一个布局要想成为一个范畴, 除了必须包孕上述三样器械,它还要满意以下三个限定:

结合律: f.(g.h) = (f.g).h 。

封闭律:假如存在态射 f, g,则一定存在 h = f.g 。

同一律:对布局中的每一个工具 A,必须存在一个单位态射 Ia: A -> A, 对付单位态射,显然,对随意率性其它态射 f,有 f.I = f。

在范畴论里别的钻研的重点是范畴与范畴之间的关系,就正如工具与工具之间有态射一样,范畴与范畴之间也存在映射关系,从而可以将一个范畴映射为另一个范畴,这种映射在范畴论中叫作函子(functor),详细来说,对付给定的两个范畴 A 和 B, 函子的感化有两个:

将范畴 A 中的工具映射到范畴 B 中的工具。

将范畴 A 中的态射映射到范畴 B 中的态射。

显然,函子反应了不合的范畴之间的内在联系。跟函数和泛函数的思惟是相同的。

而我们的函数式编程商量的问题与思惟理念可以说是跟范畴论完全吻合。假如把函数式编程的全部的天下看做一个工具,那么FP真正搞的工作便是建立经由过程函数之间的映射关系,来构建这样一个标致的编程天下。

很多问题的办理(证实)着实都不涉及详细的(数据)布局,而完全可以只依附映射之间的组合运算(composition)来搞定。这便是函数式编程的核心思惟。

假如我们把法度榜样看做图论里面的一张图G,数据布局算作是图G的节点Node(数据布局,存储状态), 而算法逻辑便是这些节点Node之间的Edge (数据映射,Mapping), 那么这整幅图 G(N,E) 便是一幅美妙的抽象逻辑之塔的 映射图 , 也便是我们编程创造的天下:

Kotlin极简教程

函数是"第一等公夷易近"

函数式编程(FP)中,函数是"第一等公夷易近"。

所谓"第一等公夷易近"(first class),无意偶尔称为 闭包 或者 仿函数(functor)工具,指的是函数与其他数据类型一样,处于平等职位地方,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为其余函数的返回值。这个以函数为参数的观点,跟C说话中的函数指针类似。

举例来说,下面代码中的print变量便是一个函数(没有函数名),可以作为另一个函数的参数:

>>> val print = fun(x:Any){println(x)}

>>> listOf(1,2,3).forEach(print)

1

2

3

高阶函数(Higher order Function)

FP 说话支持高阶函数,高阶函数便是多阶映射。高阶函数用另一个函数作为其输入参数,也可以返回一个函数作为输出。

代码示例:

fun isOdd(x: Int) = x % 2 != 0

fun length(s: String) = s.length

fun http://coffee-script.org/。

然则,这个Y组合子 如果 应用 OOP 说话编程范式, 就要显得繁杂许多。为了加倍深刻地熟识OOP 与 FP编程范式,我们应用Java 8 以及 Kotlin 的实例来阐明。这里应用Java给出示例的缘故原由,是为了给出Kotlin与Java说话上的比较,鄙人一章节中,我们将要进修Kotlin与Java的互操作。

首先我们应用Java的匿名内部类实现Y组合子 :

package com.easy.kotlin;

public class YCombinator {

public static Lambda yCombinator(final Lambda f) {

return new Lambda() {

@Override

public Lambda call(Object input) {

final Lambda u = (Lambda)input;

return u.call(u);

}

}.call(new Lambda() {

@Override

public Lambda call(Object input) {

final Lambda x = (Lambda)input;

return f.call(new Lambda() {

@Override

public Object call(Object input) {

return x.call(x).call(input);

}

});

}

});

}

public static void main(String[] args) {

Lambda y = yCombinator(new Lambda() {

@Override

public Lambda call(Object input) {

final Lambda fab = (Lambda)input;

return new Lambda() {

@Override

public Integer call(Object input) {

Integer n = Integer.parseInt(input.toString());

if (n{

E call(Object input);

}

}

这里定义了一个Lambda类型, 然后经由过程E call(Object input)措施实现自调用,措施实现里有多处转型以及嵌套调用。逻辑对照绕,代码可读性也对照差。当然,这个问题本身也对照繁杂。

我们应用Java 8的Lambda表达式来改写下匿名内部类:

package com.easy.kotlin;

public class YCombinator2 {

public static Lambda yCombinator2(final Lambda f) {

return ((Lambda)(Object input) -> {

final Lambda u = (Lambda)input;

return u.call(u);

}).call(

((Lambda)(Object input) -> {

final Lambda v = (Lambda)input;

return f.call((Lambda)(Object p) -> {

return v.call(v).call(p);

});

})

);

}

public static void main(String[] args) {

Lambda y2 = yCombinator2(

(Lambda)(Object input) -> {

Lambda fab = (Lambda)input;

return (Lambda)(Object p) -> {

Integer n = Integer.parseInt(p.toString());

if (n{

E call(Object input);

}

}

着末,我们应用Kotlin的工具表达式(顺便复习回首一下上一章节的相关内容)实现Y组合子:

package com.easy.kotlin

// lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))

object YCombinatorKt {

fun yCombinator(f: Lambda>): Lambda> {

return object : Lambda> {

override fun call(n: Any): Lambda {

val u = n as Lambda>

return u.call(u)

}

}.call(object : Lambda> {

override fun call(n: Any): Lambda {

val x = n as Lambda>

return f.call(object : Lambdahttps://gist.github.com/Jason-Chen-2017/88e13b63fa5b7c612fddf999739964b0 ; 别的,关于Y combinator的道理先容,保举看《The Little Schemer 》这本书。

从上面的例子,我们可以看出OOP中的对接口以及多态类型,跟FP中的函数的思惟表达的,本色上是一个器械,这个器械到底是什么呢?我们姑且称之为“编程之道”罢!

Y combinator 给我们供给了一种措施,让我们在一个只支持first-class函数,然则没有内建递归的编程说话里完成递归。以是Y combinator给我们展示了一个说话完全可以定义递归函数,纵然这个说话的定义一点也没提到递归。它给我们展示了一件美妙的事:仅仅函数式编程自己,就可以让我们做到我们从来不觉得可以做到的事(而且还不止这一个例子)。

严谨而精美的lambda演算体系,从最基础的观点“函数”入手,创造出一个鲜丽而宏伟的天下,这不能不说是人类思维的骄傲。

没有"副感化"

Kotlin极简教程

所谓"副感化"(side effect),指的是函数内部与外部互动(最范例的环境,便是改动全局变量的值),孕育发生运算以外的其他结果。

函数式编程强调没有"副感化",意味着函数要维持自力,所有功能便是返回一个新的值,没有其他行径,尤其是不得改动外部变量的值。

函数式编程的念头,一开始便是为了处置惩罚运算(computation),不斟酌系统的读写(I/O)。"语句"属于对系统的读写操作,以是就被排斥在外。

当然,实际利用中,不做I/O是弗成能的。是以,编程历程中,函数式编程只要求把I/O限定到最小,不要有不需要的读写行径,维持谋略历程的纯真性。

函数式编程只是返回新的值,不改动系统变量。是以,不修改变量,也是它的一个紧张特征。

在其他类型的说话中,变量每每用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程应用参数保存状态,最好的例子便是递归。

引用透明性

函数法度榜样平日还加强引用透明性,即假如供给同样的输入,那么函数老是返回同样的结果。便是说,表达式的值不依附于可以改变值的全局状态。这样我们就可以从形式上逻辑揣摸法度榜样行径。由于表达式的意义只取决于其子表达式而不是谋略顺序或者其他表达式的副感化。这有助于我们来验证代码精确性、简化算法,有助于找出优化它的措施。

8.2 在Kotlin中应用函数式编程

好了亲,前文中我们在函数式编程的天下里遨游了一番,现在我们把思绪收回来,放到在Kotlin中的函数式编程中来。

严格的面向工具的不雅点,使得很多问题的办理规划变得较为愚蠢。为了将一行有用的代码包装到Runnable或者Callable 这两个Java中最盛行的函数式示例中,我们不得不去写五六行模板典型代码。为了让工作简单化(在Java 8中,增添Lambda表达式的支持),我们在Kotlin中应用通俗的函数来替代函数式接口。事实上,函数式编程中的函数,比C说话中的函数或者Java中的措施都要强大年夜的多。

在Kotlin中,支持函数作为一等公夷易近。它支持高阶函数、Lambda表达式等。我们不仅可以把函数当做通俗变量一样通报、返回,还可以把它分配给变量、放进数据布局或者进行一样平常性的操作。它们可所以未经命名的,也便是匿名函数。我们也可以直接把一段代码丢到 {}中,这便是闭包。

在前面的章节中,着实我们已经涉及到一些关于函数的地方,我们将在这里系统地进修一下Kotlin的函数式编程。

8.2.1 Kotlin中的函数

首先,我们来看下Kotlin中函数的观点。

函数声明

Kotlin 中的函数应用 fun 关键字声明

fun double(x: Int): Int {

return 2*x

}

函数用法

调用函数应用传统的措施

fun test() {

val doubleTwo = double(2)

println("double(2) = $doubleTwo")

}

输出:double(2) = 4

调用成员函数应用点表示法

object FPBasics {

fun double(x: Int): Int {

return 2 * x

}

fun test() {

val doubleTwo = double(2)

println("double(2) = $doubleTwo")

}

}

fun main(args: Array) {

FPBasics.test()

}

我们这里直接用object工具FPBasics来演示。

8.2.2扩展函数

经由过程 扩展 声明完成一个类的新功能 扩展 ,而无需承袭该类或应用设计模式(例如,装饰者模式)。

一个扩展String类的swap函数的例子:

fun String.swap(index1: Int, index2: Int): String {

val charArray = this.toCharArray()

val tmp = charArray[index1]

charArray[index1] = charArray[index2]

charArray[index2] = tmp

return charArrayToString(charArray)

}

fun charArrayToString(charArray: CharArray): String {

var result = ""

charArray.forEach { it -> result = result + it }

return result

}

这个 this 关键字在扩展函数内部对应到接管者工具(传过来的在点符号前的工具)。 现在,我们对随意率性 String 调用该函数了:

val str = "abcd"

val swapStr = str.swap(0, str.lastIndex)

println("str.swap(0, str.lastIndex) = $swapStr")

输出: str.swap(0, str.lastIndex) = dbca

8.2.3中缀函数

在以了局景中,函数还可以用中缀表示法调用:

成员函数或扩展函数

只有一个参数

用 infix 关键字标注

例如,给 Int 定义扩展

infix fun Int.shl(x: Int): Int {

...

}

用中缀表示法调用扩展函数:

1 shl 2

等同于这样

1.shl(2)

8.2.4函数参数

函数参数应用 Pascal 表示法定义,即 name: type。参数用逗号隔开。每个参数必须显式指定其类型。

fun powerOf(number: Int, exponent: Int): Int {

return Math.pow(number.toDouble(), exponent.toDouble()).toInt()

}

测试代码:

val eight = powerOf(2, 3)

println("powerOf(2,3) = $eight")

输出:powerOf(2,3) = 8

默认参数

函数参数可以有默认值,当省略响应的参数时应用默认值。这可以削减重载数量。

fun add(x: Int = 0, y: Int = 0): Int {

return x + y

}

默认值经由过程类型后面的 = 及给出的值来定义。

测试代码:

val zero = add()

val one = add(1)

val two = add(1, 1)

println("add() = $zero")

println("add(1) = $one")

println("add(1, 1) = $two")

输出:

add() = 0

add(1) = 1

add(1, 1) = 2

别的,覆盖带默认参数的函数时,老是应用与基类型措施相同的默认参数值。

当覆盖一个带有默认参数值的措施时,署名中不带默认参数值:

open class DefaultParamBase {

open fun add(x: Int = 0, y: Int = 0): Int {

return x + y

}

}

class DefaultParam : DefaultParamBase() {

override fun add(x: Int, y: Int): Int { // 不能有默认值

return super.add(x, y)

}

}

命名参数

可以在调用函数时应用命名的函数参数。当一个函数有大年夜量的参数或默认参数时这会异常方便。

给定以下函数

fun reformat(str: String,

normalizeCase: Boolean = true,

upperCaseFirstLetter: Boolean = true,

divideByCamelHumps: Boolean = false,

wordSeparator: Char = ' ') {

}

我们可以应用默认参数来调用它

reformat(str)

然而,当应用非默认参数调用它时,该调用看起来就像

reformat(str, true, true, false, '_')

应用命名参数我们可以使代码更具有可读性

reformat(str,

normalizeCase = true,

upperCaseFirstLetter = true,

divideByCamelHumps = false,

wordSeparator = '_'

)

并且假如我们不必要所有的参数

reformat(str, wordSeparator = '_')

可变数量的参数(Varargs)

函数的参数(平日是着末一个)可以用 vararg 修饰符标记:

funasList(vararg ts: T): List {

val result = ArrayList()

for (t in ts) // ts is an Array

result.add(t)

return result

}

容许将可变数量的参数通报给函数:

val list = asList(1, 2, 3)

8.2.5 函数返回类型

函数返回类型必要显式声明

具有块代码体的函数必须始终显式指定返回类型,除非他们旨在返回 Unit。

Kotlin 不揣摸具有块代码体的函数的返回类型,由于这样的函数在代码体中可能有繁杂的节制流,并且返回类型对付读者(无意偶尔对付编译器)也是不显着的。

返回 Unit 的函数

假如一个函数不返回任何有用的值,它的返回类型是 Unit。Unit 是一种只有一个Unit 值的类型。这个值不必要显式返回:

fun printHello(name: String?): Unit {

if (name != null)

println("Hello ${name}")

else

println("Hi there!")

// `return Unit` 或者 `return` 是可选的

}

Unit 返回类型声明也是可选的。上面的代码等同于

fun printHello(name: String?) {

.....

}

8.2.6 单表达式函数

当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可

fun double(x: Int): Int = x * 2

当返回值类型可由编译器揣摸时,显式声明返回类型是可选的:

fun double(x: Int) = x * 2

8.2.7 函数感化域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不必要像一些说话如 Java、C# 或 Scala 那样创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部感化域、作为成员函数以及扩展函数。

局部函数(嵌套函数)

Kotlin 支持局部函数,即一个函数在另一个函数内部

fun sum(x: Int, y: Int, z: Int): Int {

val delta = 0;

fun add(a: Int, b: Int): Int {

return a + b + delta

}

return add(x + add(y, z))

}

局部函数可以造访外部函数(即闭包)中的局部变量delta。

println("sum(1,2,3) = ${sum(0, 1, 2, 3)}")

输出:sum(1,2,3) = 6

成员函数

成员函数是在类或工具内部定义的函数

class Sample() {

fun foo() { print("Foo") }

}

成员函数以点表示法调用

Sample().foo() // 创建类 Sample 实例并调用 foo

8.2.8 泛型函数

函数可以有泛型参数,经由过程在函数名前应用尖括号指定。

例如Iterable的map函数:

public inline funIterable.map(transform: (T) -> R): List {

return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)

}

8.2.9高阶函数

高阶函数是将函数用作参数或返回值的函数。例如,Iterable的filter函数:

public inline funIterable.filter(predicate: (T) -> Boolean): List {

return filterTo(ArrayList(), predicate)

}

它的输入参数predicate: (T) -> Boolean便是一个函数。此中,函数类型声明的语法是:

(X)->Y

表示这个函数是从类型X到类型Y的映射。即这个函数输入X类型,输出Y类型。

这个函数我们这样调用:

fun isOdd(x: Int): Boolean {

return x % 2 == 1

}

val list = listOf(1, 2, 3, 4, 5)

list.filter(::isOdd)

此中,::用来引用一个函数。

8.2.10 匿名函数

我们也可以应用匿名函数来实现这个predicate函数:

list.filter((fun(x: Int): Boolean {

return x % 2 == 1

}))

8.2.11 Lambda 表达式

我们也可以直接应用更简单的Lambda表达式来实现一个predicate函数:

list.filter {

it % 2 == 1

}

lambda 表达式老是被大年夜括号 {} 括着

其参数(假如有的话)在 -> 之前声明(参数类型可以省略)

函数体(假如存在的话)在 -> 后面

上面的写法跟:

list.filter({

it % 2 == 1

})

等价,假如 lambda 是该调用的独一参数,则调用中的圆括号可以省略。

应用Lambda表达式定义一个函数字面值:

>>> val sum = { x: Int, y: Int -> x + y }

>>> sum(1,1)

2

我们在应用嵌套的Lambda表达式来定义一个柯里化的sum函数:

>>> val sum = {x:Int ->{y:Int -> x+y }}

>>> sum

(kotlin.Int) -> (kotlin.Int) -> kotlin.Int

>>> sum(1)(1)

2

8.2.11 it:单个参数的隐式名称

Kotlin中另一个有用的约定是,假如函数字面值只有一个参数,

那么它的声明可以省略(连同 ->),其名称是 it。

代码示例:

>>> val list = listOf(1,2,3,4,5)

>>> list.map { it * 2 }

[2, 4, 6, 8, 10]

8.2.12 闭包(Closure)

Lambda 表达式或者匿名函数,以及局部函数和工具表达式(object declarations)可以造访其 闭包 ,即在外部感化域中声明的变量。 与 Java 不合的是可以改动闭包中捕获的变量:

fun sumGTZero(c: Iterable): Int {

var sum = 0

c.filter { it > 0 }.forEach {

sum += it

}

return sum

}

val list = listOf(1, 2, 3, 4, 5)

sumGTZero(list) // 输出 15

我们再应用闭包来写一个应用Java中的Thread接口的例子:

fun closureDemo() {

Thread({

for (i in 1..10) {

println("I = $i")

Thread.sleep(1000)

}

}).start()

Thread({

for (j in 10..20) {

println("J = $j")

Thread.sleep(2000)

}

Thread.sleep(1000)

}).start()

}

一个输出:

I = 1

J = 10

I = 2

I = 3

...

J = 20

8.2.13带接管者的函数字面值

Kotlin 供给了应用指定的 接管者工具 调用函数字面值的功能。

应用匿名函数的语法,我们可以直接指定函数字面值的接管者类型。

下面我们应用带接管者的函数类型声明一个变量,并在之后应用它。代码示例:

>>> val sum = fun Int.(other: Int): Int = this + other

>>> 1.sum(1)

2

当接管者类型可以从高低文揣摸时,lambda 表达式可以用作带接管者的函数字面值。

class HTML {

fun body() {

println("HTML BODY")

}

}

fun html(init: HTML.() -> Unit): HTML { // HTML.()中的HTML是吸收者类型

val html = HTML()// 创建接管者工具

html.init()// 将该接管者工具传给该 lambda

return html

}

测试代码:

html {

body()

}

输出:HTML BODY

应用这个特点,我们可以构建一个HTML的DSL说话。

8.2.14 详细化的类型参数

无意偶尔候我们必要造访一个参数类型:

funTreeNode.findParentOfType(clazz: Class): T? {

var p = parent

while (p != null && !clazz.isInstance(p)) {

p = p.parent

}

@Suppress("UNCHECKED_CAST")

return p as T?

}

在这里我们向上遍历一棵树并且反省每个节点是不是特定的类型。

这都没有问题,然则调用场不是很优雅:

treeNode.findParentOfType(MyTreeNode::class.java)

我们真正想要的只是传一个类型给该函数,即像这样调用它:

treeNode.findParentOfType()

为能够这么做,内联函数支持详细化的类型参数,于是我们可以这样写:

inline funTreeNode.findParentOfType(): T? {

var p = parent

while (p != null && p !is T) {

p = p.parent

}

return p as T?

}

我们应用 reified 修饰符来限制类型参数,现在可以在函数内部造访它了,

险些就像是一个通俗的类一样。因为函数是内联的,不必要反射,正常的操作符如 !is 和 as 现在都能用了。

虽然在许多环境下可能不必要反射,但我们仍旧可以对一个详细化的类型参数应用它:

inline funmembersOf() = T::class.members

fun main(s: Array) {

println(membersOf().joinToString("\n"))

}

通俗的函数(未标记为内联函数的)没有详细化参数。

8.2.10 尾递归tailrec

Kotlin 支持一种称为尾递归的函数式编程风格。 这容许一些平日用轮回写的算法改用递归函数来写,而无客栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满意所需的形式时,编译器会优化该递归,天生一个快速而高效的基于轮回的版本。

tailrec fun findFixPoint(x: Double = 1.0): Double

= if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) // 函数必须将其自身调用作为它履行的着末一个操作

这段代码谋略余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,孕育发生0.7390851332151607的结果。终极代码相称于这种更传统风格的代码:

private fun findFixPoint(): Double {

var x = 1.0

while (true) {

val y = Math.cos(x)

if (x == y) return y

x = y

}

}

要相符 tailrec 修饰符的前提的话,函数必须将其自身调用作为它履行的着末一个操作。在递归调用后有更多代码时,不能应用尾递归,并且不能用在 try/catch/finally 块中。尾部递归在 JVM 后端中支持。

Kotlin 还为聚拢类引入了许多扩展函数。例如,应用 map() 和 filter() 函数可以流通地操纵数据,详细的函数的应用以及示例我们已经在 聚拢类 章节中先容。

本章小结

本章我们一路进修了函数式编程的简史、Lambda演算、Y组合子与递归等核心函数式的编程思惟等相关内容。然后重点先容了在Kotlin中若何应用函数式风格编程,此中重点先容了Kotlin中函数的相关常识,以及高阶函数、Lambda表达式、闭包等核心语法,并给出响应的实例阐明。

我们将鄙人一章 中先容Kotlin的 轻量级线程:协程(Coroutines)的相关常识,我们将看到在Kotlin中,法度榜样的逻辑可以在协程中顺序地表达,而底层库会为我们办理其异步性。

本章示例代码工程:https://github.com/EasyKotlin/chapter8_fp

您可能还会对下面的文章感兴趣: