使用 Vavr 进行函数式编程

小夏 科技 更新 2024-02-16

V**r 使函数式编程更简单、更易于使用。

在本系列的上一篇文章中,我们介绍了 J**a 平台提供的 lambda 表达式和流。 受 J**A 标准库的通用性要求和二进制文件大小的限制,J**a 标准库对函数式编程的 API 支持相对有限。 函数声明只有两种类型:函数和双函数,流上支持的操作数量也很少。 为了更好的函数式编程,我们需要第三方库的支持。 V**R 是 J**a 平台上最好的函数式编程库。 许多开发人员可能不熟悉 v**r 这个名字。 它的前身 J**Aslang 可能更熟悉。 V**r 作为标准的 j**a 库,使用简单。 只需添加对io.v**r:v**r可以完成库的 m**en 依赖项。 v**r 需要支持 J**a 8 及更高版本。 本文基于 v**r 09.版本 2,基于 Ja 10 的示例。

元组是具有固定数字的不同类型的元素的组合。 元组与集合的不同之处在于,元组中的元素类型可以不同,并且数量是固定的。 元组的好处是可以将多个元素作为一个单元传递。 如果方法需要返回多个值,则可以将这些值作为元组返回,而无需创建其他类来表示它们。 根据元素的数量,v**r 总共提供 9 个类,例如 tuple0、tuple1 到 tuple8。 每个元组类都需要声明其元素类型。

如:tuple2它表示两个元素的元组,第一个元素是字符串类型,第二个元素是整数类型。 对于元组对象,您可以使用它以访问其中的元素。 所有元组对象都是不可变的,创建后无法更改。

元组由接口元组的静态方法创建。 元组类还提供了一些操作它们的方法。 由于元组是不可变的,因此所有相关操作都会返回一个新的元组对象。 在清单 1 中,使用 tupleof 创建一个 Tuple2 对象。

Tuple2 的 Map 方法用于转换元组中的每个元素,返回一个新的元组对象。 另一方面,apply 方法将元组转换为单个值。 其他元组类也有类似的方法。 除了map除了方法之外,还有map1、map2、map3等方法来转换第n个元素; update1、update2 和 update3 等方法用于更新各个元素。

清单 1使用元组。

tuple2 tuple2 = tuple.of("hello", 100);tuple2 updatedtuple2 = tuple2.map(string::touppercase, v ->v * 5);string result = updatedtuple2.apply((str, number) -string.join(", ",str, number.tostring())system.out.println(result);
虽然元组使用起来很方便,但它们不应该被滥用,尤其是当它们具有 3 个以上的元素时。 当元组中的元素过多时,可能很难明确地记住每个元素的位置和含义,从而降低其可读性。 此时,最好使用 j**a 类。

在 J**A 8 中,只提供一个接受一个参数的函数和一个接受两个参数的双函数。 v**r 提供函数接口 function0、function1 到 function8,可以描述最多接受 8 个参数的函数。 这些接口的方法apply不能引发异常。 如果需要抛出异常,可以使用相应的 API:checkedfunction0、checkedfunction1 和 checkedfunction8。

v**r 的函数支持一些常见的特征。

函数组合是指利用一个函数的执行结果作为参数调用另一个函数而得到的新函数。 例如,f 是从 x 到 y 的函数,g 是从 y 到 z 的函数,那么g(f(x))是一个从 x 到 z 的函数。 v**r 的功能接口提供了默认方法andthen将当前函数与另一个函数表示的函数组合在一起。 v**r 的 function1 还提供了一个默认方法 compose,用于在执行当前函数之前执行由另一个函数表示的函数。

在清单 2 中,第一个函数 3 执行简单的数学运算,然后使用 然后将函数 3 的结果乘以 100。 第二个 function1 来自字符串touppercase创建和使用的方法compose带有对象的方法tostring首先结合方法。 首先为任何对象调用生成的方法tostring然后再次致电touppercase

清单 2功能组合。

function3< integer, integer, integer, integer> function3 = (v1, v2, v3)->v1 + v2) *v3;function3< integer, integer, integer, integer> composed =function3.andthen(v ->v * 100);int result = composed.apply(1, 2, 3);system.out.println(result);输出 900function1< string, string> function1 = string::touppercase; function1< object, string> touppercase = function1.compose(object::tostring);string str = touppercase.apply(list.of("a", "b"));system.out.println(str);输出 [a, b]。
在 v**r 中,函数的 apply 方法可以应用不同数量的参数。 如果提供的参数数小于函数声明的参数数(passarity()method),则结果为另一个函数,所需的参数数是剩余未指定值的参数数。在清单 3 中,function4 接受 4 个参数,在apply调用时仅提供 2 个参数,结果为 function2 对象。

清单 3部分功能应用。

function4< integer, integer, integer, integer, integer> function4 =(v1, v2, v3, v4) -v1 + v2) *v3 + v4);function2< integer, integer, integer> function2 = function4.apply(1, 2);int result = function2.apply(4, 5);system.out.println(result);输出 27
curried方法获取当前函数的咖喱化版本。 由于 curryized 函数只有一个参数,curried都是 function1 对象。 在清单 4 中,对于 function3,在第一个curried方法调用获取 function1 后,它通过apply将值应用于第一个参数。 以此类推,通过 3 次curriedapply调用,将值应用于所有 3 个参数。

清单 4函数的咖喱化。

function3 function3 = (v1, v2, v3)->v1 + v2) *v3;int result =function3.curried().apply(1).curried().apply(2).curried().apply(3);system.out.println(result);
使用记忆函数会根据参数值缓存先前计算的结果。 对于相同的参数值,另一个调用将返回缓存的值,而无需再次计算。 这是一个经典的时空策略。 可以使用记忆的前提是该函数具有引用透明度。

在清单 5 中,使用了原始函数实现bigintegerpow计算功率的方法。 用memoized方法获取函数的记忆版本。 然后调用相同的参数两次并记录时间。 从结果中可以看出,第二个函数调用需要很短的时间,因为结果是直接从缓存中获取的。

清单 5功能记忆。

function2 pow = biginteger::pow;function2 memoized = pow.memoized();long start = system.currenttimemillis();memoized.apply(biginteger.valueof(1024), 1024);long end1 = system.currenttimemillis();memoized.apply(biginteger.valueof(1024), 1024);long end2 = system.currenttimemillis();system.out.printf("%d ms ->d ms", end1 - start, end2 - end1);
注意memoized该方法只是将原始函数视为黑盒,并且不修改函数的内部实现。 因此memoized它不适用于本系列第二篇文章中递归计算斐波那契数列的函数的直接封装。 这是因为在函数的内部实现中,函数仍然是在没有内存的情况下调用的。 v**r 中提供了几种不同类型的值。

v**roption带 J**A 8optional是相似的。 但是,v**r 的选项是具有两个实现类的接口option.someoption.none,分别对应两种情况,有值和无值。 用option.some可以创建包含给定值的方法some对象,而option.none是的none对象的实例。 option还支持常用的mapflatmapfilter依此类推,如清单 6 所示。

清单 6使用选项的示例。

option str = option.of("hello");str.map(string::length);str.flatmap(v ->option.of(v.length())
either指示可能有两种不同类型的值,称为左值或右值。 它只能是其中之一。 either它通常用于表示成功或失败。 惯例是将成功的值作为右值,将失败的值作为左值。 是的, 你可以的either添加适用于左值或右值的计算。 适用于右值的计算仅在either它仅在包含右值时生效,左值也是如此。

在清单 7 中,基于随机布尔值创建一个 LOR LVALUEeither对象。 eithermapmapleft右值和左值是分开计算的。

清单 7使用两者之一的示例。

import io.v**r.control.either;import j**a.util.concurrent.threadlocalrandom;public class eithers private static either compute()
try用于表示可能产生异常的计算。 try该接口有两个实现类try.successtry.failure,分别代表成功和失败。 try.success在计算成功时封装返回值,而try.failure然后封装计算失败throwable对象。 可以从接口获取 try 的实例checkedfunction0callablerunnablesuppliertry还提供mapfilter等方法。 值得一提的是tryrecover在发生错误时从异常中恢复的方法。

在清单 8 中,第一个try是的结果显然是一种反常现象。 用recover返回 1。 第二个try它表示读取文件的结果。 由于该文件不存在,try这也是一种反常现象。

清单 8使用 try 的示例。

try result = try.of(()1 / 0).recover(e ->1);system.out.println(result);try lines = try.of(()files.readalllines(paths.get("1.txt")))map(list ->string.join(",", list)) andthen((consumer) system.out::println);system.out.println(lines);
lazy表示延迟计算的值。 评估操作在第一次访问时发生,并且仅评估一次值。 后续访问操作获取缓存值。 在清单 9 中,lazy.of从站接口supplierlazy对象。 方法:isevaluated可以判断lazy对象是否已被评估。

清单 9使用懒惰的示例。

lazy lazy = lazy.of(()biginteger.valueof(1024).pow(1024));system.out.println(lazy.isevaluated())system.out.println(lazy.get())system.out.println(lazy.isevaluated())
v**r 在 Iterable 之上重新实现自己的收集框架。 v**r 的收集框架侧重于不变性。 v**r 的集合类比 j**a 流更简洁易用。

V**r 的流比 j**a 中的流提供更多的操作。 可以使用stream.ofall从可迭代对象创建 v**r 流。 以下是 v**r 中添加的一些有用操作:

groupby:使用功能对元素进行分组。 结果是一个映射,其中映射的键是组函数的结果,值是包含同一组中所有元素的流。 partition:使用谓词对元素进行分组。 结果是具有 2 个流的 tuple2。 tuple2 的第一个流的元素满足谓词指定的条件,第二个流的元素不满足谓词指定的条件。 scanleftscanright:分别按从左到右或从右到左的顺序对元素调用函数,并累加结果。 zip:将流与可迭代对象合并,生成的流包含 tuple2 对象。 tuple2 对象的两个元素分别来自流对象和可迭代对象。 在清单 10 中,第一个groupby该操作将流分为奇数组和偶数组; 第二个partition该操作将流分为两组:大于 2 且不大于 2; 第三scanleft包含字符串的流根据字符串的长度进行累积; 最后一个zip该操作合并两个流,结果是与长度最小的输入流具有相同元素数的流。

清单 10Stream的使用示例。

map> booleanlistmap = stream.ofall(1, 2, 3, 4, 5) .groupby(v ->v % 2 == 0) .mapvalues(value::tolist);system.out.println(booleanlistmap);输出 linkedhashmap((false, list(1, 3, 5)),true, list(2, 4))))tuple2, list>listtuple2 = 流ofall(1, 2, 3, 4) .partition(v ->v > 2) .map(value::tolist, value::tolist);system.out.println(listtuple2);输出 (list(3, 4), list(1, 2))list integers = stream.ofall(list.of("hello", "world", "a")) scanleft(0, (sum, str) -sum + str.length())tolist();system.out.println(integers);输出 list(0, 5, 10, 11)list> tuple2list = 流ofall(1, 2, 3) .zip(list.of("a", "b")) tolist();system.out.println(tuple2list);输出列表((1, a), 2, b)))。
v**r 提供了常见数据结构的实现,包括 list、set、map、seq、queue、tree 和 treemap。 这些数据结构的用法类似于 J**a 标准库的用法,但提供了更多操作并且更易于使用。 在 j**a 中,如果需要映射列表的某个元素,则需要使用 stream 方法将其转换为流,然后使用 map 操作,最后通过收集器collectors.tolist转换回列表。 在 v**r 中,列表本身提供映射操作。 这两种用途的区别如清单 11 所示。

清单 11v**r 中数据结构的用法。

list.of(1, 2, 3).map(v ->v + 10); 2, 3).stream().map(v ->v + 10).collect(collectors.tolist()) j**a
在 j**a 中,我们可以根据 Value 使用 switch 和 case 来执行不同的逻辑。 但是,开关和外壳提供的功能非常薄弱,只能平分秋色。 V**R 提供了一个模式匹配 API,可以在各种情况下匹配和执行逻辑。 在清单 12 中,我们将 j**a 中的开关和 case 替换为 v**r 中的匹配和大小写。 match 的参数是需要匹配的值。 case 的第一个参数是匹配条件,用谓语表示; 第二个参数是满足匹配项时的值。 $(value)表示值的相等匹配它表示默认匹配,相当于交换机中的默认值。

清单 12模式匹配示例。

string input = "g";string result = match(input).of( case($("g"), "good"), case($("b"), "bad"), case($("unknown"));system.out.println(result);输出良好
在清单 13 中,我们使用:$(v ->v > 0)将创建一个值大于 0 的谓词。 在这里,匹配的结果不是特定值,而是传递run制作方法 ***

清单 13使用模式匹配产生***

int value = -1;match(value).of( case($(v ->v > 0), o ->run(()system.out.println("> 0")))case($(0), o ->run(()system.out.println("0")))case($(o ->run(()system.out.println("< 0"输出< 0
当 J**a 平台上需要复杂的函数式编程时,J**a 标准库提供的支持已不再足够。 v**r 作为 J**a 平台上流行的函数式编程库,可以满足不同的需求。 本文详细介绍了 v**r 提供的元组、函数、值、数据结构和模式匹配。 下一篇文章将介绍单子在函数式编程中的重要概念。

相似文章

    如何使用excel中的rank函数进行排名,解释rank函数参数和公式的使用

    Excel 中的 rank 函数专用于数据排名场景,在本节中,我们将详细了解 rank 函数的完整三个参数及其公式应用。我们来看一个例子,数据表包含产品和销售额,现在我们想按照销售额进行排名,排名方法从大到小依次降序,即最大值排,最小值排在最后。如何使用rank函数来表达排名公式,我们先看一下它的语...

    如何使用工作日功能

    weekday该函数用于返回指定日期所在的星期几,是Excel中常用的日期和时间函数。在这里 weekday功能基本用法 weekday serial number,return type serial number 必需,表示要确定日期的星期几。它可以是包含日期的单元格引用,也可以是直接输入的日期...

    如何使用 INDEX 函数

    索引函数介绍 索引函数是Excel中常用的搜索函数之一,用于返回表或区域中的值或对值的引用,并用于查找指定区域中行或列中的数据。函数一般有两种形式 数组 通常返回一个数值或一个数值数组引文 通常返回引用。.索引函数的句法结构 index array,row num,column num .第一个参数...

    如何使用间接函数

    间接函数是在电子软件 如 Microsoft Excel 中用于引用其他单元格内容的函数。它的主要作用是通过给定的单元格引用获取单元格的内容,而无需直接使用单元格引用。以下是使用间接函数的方法 .打开 Excel,创建新工作簿或打开现有工作簿。.在一个单元格中输入间接函数。例如,在单元格 A 中输入...

    如何使用间接函数

    间接函数概述 在数学和工程领域,间接函数是一个重要的工具,它使我们能够描述一个函数如何被另一个函数转换。间接函数的应用范围很广,如物理 化学 生物学 金融等领域。了解如何使用间接函数可以帮助我们更好地理解和解决各种问题。.间接功能的定义 间接函数,也称为隐式函数,是一种不能简单地用代数表示的函数。它...