广东网站推广,延安网站建设网络公司,建设学校网站需求分析,建购物网站多少钱简介#xff1a; 本文会以惰性加载为例一步步介绍函数式编程中各种概念#xff0c;所以读者不需要任何函数式编程的基础#xff0c;只需要对 Java 8 有些许了解即可。 作者 | 悬衡 来源 | 阿里技术公众号
本文会以惰性加载为例一步步介绍函数式编程中各种概念#xff0c;所…简介 本文会以惰性加载为例一步步介绍函数式编程中各种概念所以读者不需要任何函数式编程的基础只需要对 Java 8 有些许了解即可。 作者 | 悬衡 来源 | 阿里技术公众号
本文会以惰性加载为例一步步介绍函数式编程中各种概念所以读者不需要任何函数式编程的基础只需要对 Java 8 有些许了解即可。
一 抽象一定会导致代码性能降低
程序员的梦想就是能写出 “高内聚低耦合”的代码但从经验上来看越抽象的代码往往意味着越低的性能。机器可以直接执行的汇编性能最强C 语言其次Java 因为较高的抽象层次导致性能更低。业务系统也受到同样的规律制约底层的数增删改查接口性能最高上层业务接口因为增加了各种业务校验以及消息发送导致性能较低。
对性能的顾虑也制约程序员对于模块更加合理的抽象。
一起来看一个常见的系统抽象“用户” 是系统中常见的一个实体为了统一系统中的 “用户” 抽象我们定义了一个通用领域模型 User除了用户的 id 外还含有部门信息用户的主管等等这些都是常常在系统中聚合在一起使用的属性:
public class User {// 用户 idprivate Long uid;// 用户的部门为了保持示例简单这里就用普通的字符串// 需要远程调用 通讯录系统 获得private String department;// 用户的主管为了保持示例简单这里就用一个 id 表示// 需要远程调用 通讯录系统 获得private Long supervisor;// 用户所持有的权限// 需要远程调用 权限系统 获得private Set String permission;
}
这看起来非常棒“用户“常用的属性全部集中到了一个实体里只要将这个 User 作为方法的参数这个方法基本就不再需要查询其他用户信息了。但是一旦实施起来就会发现问题部门和主管信息需要远程调用通讯录系统获得权限需要远程调用权限系统获得每次构造 User 都必须付出这两次远程调用的代价即使有的信息没有用到。比如下面的方法就展示了这种情况判断一个用户是否是另一个用户的主管
public boolean isSupervisor(User u1, User u2) {return Objects.equals(u1.getSupervisor(), u2.getUid());
}
为了能在上面这个方法参数中使用通用 User 实体必须付出额外的代价远程调用获得完全用不到的权限信息如果权限系统出现了问题还会影响无关接口的稳定性。
想到这里我们可能就想要放弃通用实体的方案了让裸露的 uid 弥漫在系统中在系统各处散落用户信息查询代码。
其实稍作改进就可以继续使用上面的抽象只需要将 department, supervisor 和 permission 全部变成惰性加载的字段在需要的时候才进行外部调用获得这样做有非常多的好处
业务建模只需要考虑贴合业务而不需要考虑底层的性能问题真正实现业务层和物理层的解耦业务逻辑与外部调用分离无论外部接口如何变化我们总是有一层适配层保证核心逻辑的稳定业务逻辑看起来就是纯粹的实体操作易于编写单元测试保障核心逻辑的正确性
但是在实践的过程中常会遇到一些问题本文就结合 Java 以及函数式编程的一些技巧一起来实现一个惰性加载工具类。
二 严格与惰性Java 8 的 Supplier 的本质
Java 8 引入了全新的函数式接口 Supplier从老 Java 程序员的角度理解它不过就是一个可以获取任意值的接口而已Lambda 不过是这种接口实现类的语法糖。这是站在语言角度而不是计算角度的理解。当你了解了严格strict与惰性lazy的区别之后可能会有更加接近计算本质的看法。
因为 Java 和 C 都是严格的编程语言所以我们习惯了变量在定义的地方就完成了计算。事实上还有另外一个编程语言流派它们是在变量使用的时候才进行计算的比如函数式编程语言 Haskell。 所以 Supplier 的本质是在 Java 语言中引入了惰性计算的机制为了在 Java 中实现等价的惰性计算可以这么写
Supplier Integer a () - 10 1;
int b a.get() 1;
三 Supplier 的进一步优化Lazy
Supplier 还存在一个问题就是每次通过 get 获取值时都会重新进行计算真正的惰性计算应该在第一次 get 后把值缓存下来。只要对 Supplier 稍作包装即可
/**
* 为了方便与标准的 Java 函数式接口交互Lazy 也实现了 Supplier
*/
public class Lazy T implements Supplier T {private final Supplier ? extends T supplier;// 利用 value 属性缓存 supplier 计算后的值private T value;private Lazy(Supplier ? extends T supplier) {this.supplier supplier;}public static T Lazy T of(Supplier ? extends T supplier) {return new Lazy (supplier);}public T get() {if (value null) {T newValue supplier.get();if (newValue null) {throw new IllegalStateException(Lazy value can not be null!);}value newValue;}return value;}
}
通过 Lazy 来写之前的惰性计算代码
Lazy Integer a Lazy.of(() - 10 1);
int b a.get() 1;
// get 不会再重新计算, 直接用缓存的值
int c a.get();
通过这个惰性加载工具类来优化我们之前的通用用户实体
public class User {// 用户 idprivate Long uid;// 用户的部门为了保持示例简单这里就用普通的字符串// 需要远程调用 通讯录系统 获得private Lazy String department;// 用户的主管为了保持示例简单这里就用一个 id 表示// 需要远程调用 通讯录系统 获得private Lazy Long supervisor;// 用户所含有的权限// 需要远程调用 权限系统 获得private Lazy Set String permission;public Long getUid() {return uid;}public void setUid(Long uid) {this.uid uid;}public String getDepartment() {return department.get();}/*** 因为 department 是一个惰性加载的属性所以 set 方法必须传入计算函数而不是具体值*/public void setDepartment(Lazy String department) {this.department department;}// ... 后面类似的省略
}
一个简单的构造 User 实体的例子如下
Long uid 1L;
User user new User();
user.setUid(uid);
// departmentService 是一个rpc调用
user.setDepartment(Lazy.of(() - departmentService.getDepartment(uid)));
// ....
这看起来还不错但当你继续深入使用时会发现一些问题用户的两个属性部门和主管是有相关性需要通过 rpc 接口获得用户部门然后通过另一个 rpc 接口根据部门获得主管。代码如下
String department departmentService.getDepartment(uid);
Long supervisor SupervisorService.getSupervisor(department);
但是现在 department 不再是一个计算好的值了而是一个惰性计算的 Lazy 对象上面的代码又应该怎么写呢函子 就是用来解决这个问题的
四 Lazy 实现函子Functor
快速理解类似 Java 中的 stream api 或者 Optional 中的 map 方法。函子可以理解为一个接口而 map 可以理解为接口中的方法。
1 函子的计算对象
Java 中的 Collection TOptional T以及我们刚刚实现 Lazy T都有一个共同特点就是他们都有且仅有一个泛型参数我们在这篇文章中暂且称其为盒子记做 Box T因为他们都好像一个万能的容器可以任意类型打包进去。 2 函子的定义
函子运算可以将一个 T 映射到 S 的 function 应用到 Box T 上让其成为 Box S一个将 Box 中的数字转换为字符串的例子如下 在盒子中装的是类型而不是 1 和 1 的原因是盒子中不一定是单个值比如集合甚至是更加复杂的多值映射关系。
需要注意的是并不是随便定义一个签名满足 Box S map(Function T,S function) 就能让 Box T 成为函子的下面就是一个反例
// 反例不能成为函子因为这个方法没有在盒子中如实反映 function 的映射关系
public Box S map(Function T,S function) {return new Box (null);
}
所以函子是比 map 方法更加严格的定义他还要求 map 满足如下的定律称为 函子定律定律的本质就是保障 map 方法能如实反映参数 function 定义的映射关系
单位元律Box T 在应用了恒等函数后值不会改变即 box.equals(box.map(Function.identity()))始终成立这里的 equals 只是想表达的一个数学上相等的含义复合律假设有两个函数 f1 和 f2map(x - f2(f1(x))) 和 map(f1).map(f2) 始终等价
很显然 Lazy 是满足上面两个定律的。
3 Lazy 函子
虽然介绍了这么多理论实现却非常简单 public S Lazy S map(Function ? super T, ? extends S function) {return Lazy.of(() - function.apply(get()));}
可以很容易地证明它是满足函子定律的。
通过 map 我们很容易解决之前遇到的难题map 中传入的函数可以在假设部门信息已经获取到的情况下进行运算
Lazy String departmentLazy Lazy.of(() - departmentService.getDepartment(uid));
Lazy Long supervisorLazy departmentLazy.map(department - SupervisorService.getSupervisor(department)
);
4 遇到了更加棘手的情况
我们现在不仅可以构造惰性的值还可以用一个惰性值计算另一个惰性值看上去很完美。但是当你进一步深入使用的时候又发现了更加棘手的问题。
我现在需要部门和主管两个参数来调用权限系统来获得权限而部门和主管这两个值都是惰性的值。先用嵌套 map 来试一下
Lazy Lazy Set String permissions departmentLazy.map(department -supervisorLazy.map(supervisor - getPermissions(department, supervisor))
);
返回值的类型好像有点奇怪我们期待得到的是 Lazy Set String这里得到的却多了一层变成 Lazy Lazy Set String。而且随着你嵌套 map 层数增加Lazy 的泛型层次也会同样增加三参数的例子如下
Lazy Long param1Lazy Lazy.of(() - 2L);
Lazy Long param2Lazy Lazy.of(() - 2L);
Lazy Long param3Lazy Lazy.of(() - 2L);
Lazy Lazy Lazy Long result param1Lazy.map(param1 -param2Lazy.map(param2 -param3Lazy.map(param3 - param1 param2 param3))
);
这个就需要下面的单子运算来解决了。
五 Lazy 实现单子 Monad
快速理解和 Java stream api 以及 Optional 中的 flatmap 功能类似
1 单子的定义
单子和函子的重大区别在于接收的函数函子的函数一般返回的是原生的值而单子的函数返回却是一个盒装的值。下图中的 function 如果用 map 而不是 flatmap 的话就会导致结果变成一个俄罗斯套娃--两层盒子。 单子当然也有单子定律但是比函子定律要复杂些这里就不做阐释了他的作用和函子定律也是类似确保 flatmap 能够如实反映 function 的映射关系。
2 Lazy 单子
实现同样很简单 public S Lazy S flatMap(Function ? super T, Lazy ? extends S function) {return Lazy.of(() - function.apply(get()).get());}
利用 flatmap 解决之前遇到的问题
Lazy Set String permissions departmentLazy.flatMap(department -supervisorLazy.map(supervisor - getPermissions(department, supervisor))
);
三参数的情况
Lazy Long param1Lazy Lazy.of(() - 2L);
Lazy Long param2Lazy Lazy.of(() - 2L);
Lazy Long param3Lazy Lazy.of(() - 2L);
Lazy Long result param1Lazy.flatMap(param1 -param2Lazy.flatMap(param2 -param3Lazy.map(param3 - param1 param2 param3))
);
其中的规律就是最后一次取值用 map其他都用 flatmap。
3 题外话函数式语言中的单子语法糖
看了上面的例子你一定会觉得惰性计算好麻烦每次为了取里面的惰性值都要经历多次的 flatmap 与 map。这其实是 Java 没有原生支持函数式编程而做的妥协之举Haskell 中就支持用 do 记法简化 Monad 的运算上面三参数的例子如果用 Haskell 则写做
doparam1 - param1Lazyparam2 - param2Lazyparam3 - param3Lazy-- 注释: do 记法中 return 的含义和 Java 完全不一样-- 它表示将值打包进盒子里,-- 等价的 Java 写法是 Lazy.of(() - param1 param2 param3)return param1 param2 param3
Java 中虽然没有语法糖但是上帝关了一扇门就会打开一扇窗。在 Java 中可以清晰地看出每一步在做什么理解其中的原理如果你读过了本文之前的内容肯定能明白这个 do 记法就是不停地在做 flatmap 。
六 Lazy 的最终代码
目前为止我们写的 Lazy 代码如下
public class Lazy T implements Supplier T {private final Supplier ? extends T supplier;private T value;private Lazy(Supplier ? extends T supplier) {this.supplier supplier;}public static T Lazy T of(Supplier ? extends T supplier) {return new Lazy (supplier);}public T get() {if (value null) {T newValue supplier.get();if (newValue null) {throw new IllegalStateException(Lazy value can not be null!);}value newValue;}return value;}public S Lazy S map(Function ? super T, ? extends S function) {return Lazy.of(() - function.apply(get()));}public S Lazy S flatMap(Function ? super T, Lazy ? extends S function) {return Lazy.of(() - function.apply(get()).get());}
}
七 构造一个能够自动优化性能的实体
利用 Lazy 我们写一个构造通用 User 实体的工厂
Component
public class UserFactory {// 部门服务, rpc 接口Resourceprivate DepartmentService departmentService;// 主管服务, rpc 接口Resourceprivate SupervisorService supervisorService;// 权限服务, rpc 接口Resourceprivate PermissionService permissionService;public User buildUser(long uid) {Lazy String departmentLazy Lazy.of(() - departmentService.getDepartment(uid));// 通过部门获得主管// department - supervisorLazy Long supervisorLazy departmentLazy.map(department - SupervisorService.getSupervisor(department));// 通过部门和主管获得权限// department, supervisor - permissionLazy Set String permissionsLazy departmentLazy.flatMap(department -supervisorLazy.map(supervisor - permissionService.getPermissions(department, supervisor)));User user new User();user.setUid(uid);user.setDepartment(departmentLazy);user.setSupervisor(supervisorLazy);user.setPermissions(permissionsLazy);}
}
工厂类就是在构造一颗求值树通过工厂类可以清晰地看出 User 各个属性间的求值依赖关系同时 User 对象能够在运行时自动地优化性能一旦某个节点被求值路径上的所有属性的值都会被缓存。 八 异常处理
虽然我们通过惰性让 user.getDepartment() 仿佛是一次纯内存操作但是他实际上还是一次远程调用所以可能出现各种出乎意料的异常比如超时等等。
异常处理肯定不能交给业务逻辑这样会影响业务逻辑的纯粹性让我们前功尽弃。比较理想的方式是交给惰性值的加载逻辑 Supplier。在 Supllier 的计算逻辑中就充分考虑各种异常情况重试或者抛出异常。虽然抛出异常可能不是那么“函数式”但是比较贴近 Java 的编程习惯而且在关键的值获取不到时就应该通过异常阻断业务逻辑的运行。
九 总结
利用本文方法构造的实体可以将业务建模上需要的属性全部放置进去业务建模只需要考虑贴合业务而不需要考虑底层的性能问题真正实现业务层和物理层的解耦。
同时 UserFactory 本质上就是一个外部接口的适配层一旦外部接口发生变化只需要修改适配层即可能够保护核心业务代码的稳定。
业务核心代码因为外部调用大大减少代码更加接近纯粹的运算因而易于书写单元测试通过单元测试能够保证核心代码的稳定且不会出错。
十 题外话Java 中缺失的柯里化与应用函子Applicative
仔细想想刚刚做了这么多目的就是一个让签名为 C f(A,B) 的函数可以无需修改地应用到盒装类型 Box A和 Box B 上并且产生一个 Box C在函数式语言中有更加方便的方法那就是应用函子。
应用函子概念上非常简单就是将盒装的函数应用到盒装的值上最后得到一个盒装的值在 Lazy 中可以这么实现 // 注意这里的 function 是装在 lazy 里面的public S Lazy S apply(Lazy Function ? super T, ? extends S function) {return Lazy.of(() - function.get().apply(get()));}
不过在 Java 中实现这个并没有什么用因为 Java 不支持柯里化。
柯里化允许我们将函数的几个参数固定下来变成一个新的函数假如函数签名为 f(a,b)支持柯里化的语言允许直接 f(a) 进行调用此时返回值是一个只接收 b 的函数。
在支持柯里化的情况下只需要连续的几次应用函子就可以将普通的函数应用在盒装类型上了举个 Haskell 的例子如下 * 是 Haskell 中应用函子的语法糖, f 是个签名为 c f(a, b) 的函数语法不完全正确只是表达个意思
-- 注释: 结果为 box c
box f * box a * box b
参考资料
在 Java 函数式类库 VAVR 中提供了类似的 Lazy 实现不过如果只是为了用这个一个类的话引入整个库还是有些重可以利用本文的思路直接自己实现函数式编程进阶应用函子 前端角度的函数式编程文章本文一定程度上参考了里面盒子的类比方法掘金《Haskell函数式编程基础》《Java函数式编程》
原文链接 本文为阿里云原创内容未经允许不得转载。