重复性管理--从泛值到泛型以及泛函(中)--泛函是什么及为什么

news2024/10/5 21:21:44

在前面, 我们探讨了泛型范式在解决重复性问题上的应用, 在这里, 将继续探讨泛函范式在解决重复性问题上的作用.

注: 关于"泛函(functional)“这一名称, 前面说了, 泛型的本质是"参数化类型”, 那么, 按照这一思路, 泛函的意思也可以理解为"函数的参数化"或者现在时髦的所谓"函数式编程(functional programming)"吧!

当然, 你可以有自己的看法, 这里用这种比较概括性的说法可以使得标题等比较简短, 我也承认, 很多时候, 想取一个简短又准确的名字是不容易的.

从高斯的求和故事说起

据说高斯(Gauss, 德国数学家)同学小时候, 有一次老师让大家求从 1 加到 100 的和, 当其它小朋友还在埋头苦算时, 我们的小高斯同学却很快给出了结果: 5050!

image

老师和其它小伙伴都惊呆了:

原来聪明的高斯同学注意到了一个事实, 那就是: 1+100=101,2+99=101, … 50+51=101, 总共有 50 组, 所以 50 * 101 = 5050, Done!

现在我们用程序来解决这一问题, 我们就不用那些奇淫技巧了, 简单粗暴一个 for 循环求和, 以计算机速度之飞快, 妥妥秒杀我们的高斯同学:

/** 求普通和 */
public static int sum() {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += i;
	}
	return sum;
}

更多的求和

现在, 让我们来看更多的求和问题, 除了普通的求和, 我们还可能想求比如平方和, 那么可以这样写:

/** 求平方和 */	
public static int sum() {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += i * i;
	}
	return sum;
}

如果想求立方和, 可以这样写:

/** 求立方和 */
public static int sum() {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += i * i * i;
	}
	return sum;
}

自然, 我们的计算机在做起这些反复的类似的工作来是毫无怨言而且是又快又好的, 可是一再类似的重复工作却会让我们人类心生厌倦.

一再重复的模式

让我们具体看看, 重复的 bad smell 坏味道很容易就能嗅到, 请看下面的对比:

求和代码的唯一区别

不难注意到, 除了 += 右边存在差异外, 代码的其它地方都是一样的!

从字面看, 也不难发现重复:

求和中的重复

从求普通和, 到求平方和, 再到求立方和, 自然, 我们是不能忍受这种一再重复的. 我们的语言能否表达出"求和"本身这一抽象概念, 而不是限于求具体的某种和? 如何去消除这种模式的重复呢?

去重的初步设想

按照我们之前在泛函范式中的谈论, 很容易就能想到: 能否把这些差异参数化, 外部化呢? 比如这样:

public static void main(String[] args) {
	sum(i);
	sum(i * i);
	sum(i * i * i);
}

public static int sum(Object exp) {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += exp;
	}
	return sum;
}

当然, 以上代码在 Java 下是不能编译通过的, 但它的确清晰的表达出了我们的意图. 再仔细想想, 我们想要的效果大概是这样:

public static int sum(Function f) {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += f(i);
	}
	return sum;
}

public static int identity(int i) {
	return i;
}

public static int square(int i) {
	return i * i;
}

public static int cube(int i) {
	return i * i * i;
}

我们想要的是传递一个函数(或者说方法)进来, 然后在我们的求和函数中调用它.

public static void main(String[] args) {
	sum(identity);
	sum(square);
	sum(cube);
}

很遗憾, 以上代码在 Java 中依然是不能编译通过的.

如果是使用 javascript 这样的语言, 这样写已经差不多了. 不过这里不打算列举具体的代码实现.

不过, 再做些调整, 就能达到我们的意图了.

传统的解决方案

自然, 我们也可以一下子跳到函数式的解决方案上去, 这在 Java 1.8 支持了 lambda 方式之后也并不是什么问题了;或者你直接使用一个原生就支持函数式的语言那也 OK, 比如 javascript.

不过, 这里还是打算一步一步的来, 这样有助于我们理清事情的来龙去脉, 更加清晰的体会到函数式的好处.

如果你没有耐心, 可以直接直接跳过此章节. 我也承认, 有时这种技术文章不好写, 写得详细, 基础好的同学可能觉得啰嗦;写得简略, 读者可能又觉得跳跃性太大, 不好理解. 这里做个折中, 写得是尽量详细, 但也分成了不同的章节, 你可以根据需要取舍.

if-else, naive 的方式

最简单也最容易想到的方式就是用 if-else 来判断不同情况, 这种方式的代码如下:

public static void main(String[] args) {
	int idSum = sum("identity");
	int sqSum = sum("square");
	int cbSum = sum("cube");
	
	System.out.print(idSum + " " + sqSum + " " + cbSum);
}

public static int sum(String type) {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		int temp = 0;
		if ("identity".equals(type)) {
			temp = i;
		} else if ("square".equals(type)) {
			temp = i * i;
		} else if ("cube".equals(type)){
			temp = i * i * i;
		} else {
			// TODO error
		}
		sum += temp;
	}
	return sum;
}

很简单, 就是通过一个 String 的类别参数, 然后用 if-else 的方式来判断, 它在一定程度上解决了重复, 比如循环的代码只出现了一遍, 但其弊端也是很明显的.

首先, 尽管参数传递进来后就不会再变了, 可是循环中还是每次都会去判断, 影响了性能, 某种程度上看也是一种重复.

如果我们把判断放在 for 循环外面, 那又不得不重复 for 循环那些代码, 跟之前差不多.

其次是一旦有新的求和方式要添加, 又不得不修改这些代码.

它违反了所谓的开闭原则(OCP: Open Closed Principle), 软件中的对象(类, 模块, 函数等等)应该对于扩展是开放的, 但是对于修改是封闭的. (open for extension, but closed for modification)

通常会建议使用多态来代替这些条件判断, 参见 Martin Fowler 的这篇文章: https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html

多态策略(Polymorphism)

if-else 的方式很容易想到, 但弊端也很明显, 我们需要更好的解决方案.

实际上前面的初步设想已经很接近满足需求了, 只不过传统的 Java 语言坚持"一切都是对象", 对象在 Java 中是**第一级(first-class)**的, 可以做参数, 可以放在变量中, 可以作为返回值等等.

关于第一级(first-class)的概念, 后面还会具体介绍.

但它不能支持或者说不直接支持传递函数或方法的引用. 为此, 我们不得不引入一个叫 MyFunction 的接口, 里面有一个简单的 apply 方法, 接受 int 参数, 返回一个 int 结果:

public interface MyFunction {
	public int apply(int i);
}

public static int sum(MyFunction f) {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += f.apply(i);
	}
	return sum;
}

然后, 弄几个类实现这一接口:

class Identity implements MyFunction {
	@Override
	public int apply(int i) {
		return i;
	}
}

class Square implements MyFunction {
	@Override
	public int apply(int i) {
		return i * i;
	}
}

class Cube implements MyFunction {
	@Override
	public int apply(int i) {
		return i * i * i;
	}
}

这样, 想进行不同的求和时, new 出具体的类即可:

public static void main(String[] args) {
	int idSum = sum(new Identity());
	int sqSum = sum(new Square());
	int cbSum = sum(new Cube());
	
	System.out.println(idSum + " " + sqSum + " " + cbSum);
}

同时, 它也具有良好的可扩展性, 想进行新的求和, 可以创建出新的类并实现接口即可.

泛型是参数化多态, 接口和继承则是子类型多态, 不过这里不打算去探讨它们的细节.

这种方式大概是 GoF 说的"策略模式"(strategy).

GoF: gang of four, 就是写<<设计模式>>一书的四个家伙(四人帮)

不过, 由于不少模式有些相似, 我也记不清了这到底是策略模式还是模板方法, 还是其他, 亦或都不是, 如果你比较清楚, 欢迎留言.

不过, 它的缺陷在这种简单需求中也体现得很明显, 有许多的类要定义, 大量重复的**脚手架(scaffold)**的代码.

应该说, 借助于现代的 IDE, 书写这些代码也不是很难了, 不过有些人可能还是会觉得不爽.

毕竟, 反复地写那些样板代码某种程度也是一种重复性的问题.

匿名内部类(Anonymous Inner Class)

如果对于简单的需求不想定义太多的类, 可以使用匿名类的方式:

public static void main(String[] args) {
	// 匿名类方式
	int idSum = sum(new MyFunction() {
		@Override
		public int apply(int i) {
			return i;
		}
	});
	
	int sqSum = sum(new MyFunction() {
		@Override
		public int apply(int i) {
			return i * i;
		}
	});
	
	int cbSum = sum(new MyFunction() {
		@Override
		public int apply(int i) {
			return i * i * i;
		}
	});
	
	System.out.println(idSum + " " + sqSum + " " + cbSum);
}

这种方式一定程度上减轻了某些重复繁琐的工作, 但依旧还是有不少的样板代码, 不够简洁, 重点也不突出.

反射方式(Reflection)

假如我们的代码中已经存在诸如求平方, 求立方等工具类的代码,

public class MathUtil {
	public static int identity(int i) {
		return i;
	}

	public static int square(int i) {
		return i * i;
	}

	public static int cube(int i) {
		return i * i * i;
	}
}

而且我们也不想再定义什么接口及子类型, 尽管这在一定程度也解决了我们的问题, 但回到我们最初的意图, 我们就想传入一个方法, 然后调用一下它而已.

这大概类似于 C++ 等语言中的函数指针.

Java 并不直接支持传递函数引用, 但通过反射的方式, 也还是能够间接得做到这一点的. 我们来看下:

public static void main(String[] args) throws Exception {
	// int.class 表示方法参数的类型
	int idSum = sum(MathUtil.class.getMethod("identity", int.class));
	int sqSum = sum(MathUtil.class.getMethod("square", int.class));
	int cbSum = sum(MathUtil.class.getMethod("cube", int.class));
	
	System.out.print(idSum + " " + sqSum + " " + cbSum);
}

public static int sum(Method m) throws Exception {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		// 第一个参数为 null 表示为静态方法, 没有对象与之关联
		// 返回值为 Object 类型, 所以需要强制类型转换
		sum += (int)m.invoke(null, i);
	}
	return sum;
}

可以看到, 通过反射, 方法也能被参数化了, 这样直接就解决了我们的问题.

当然, 弊端也不少, 比如很多异常要处理:

为求简洁, 示例代码中直接抛出了所有异常, 但真实应用中, 这样做是很草率的.

其次, 直接使用字符串参数, 也没有编译期的检查, 写错了不到运行时也发现不了.

再次, 大量反射的运用也有潜在的性能开销.

总体而言, 至少在这个问题上, 反射方案还是不够简洁优雅, 虽然已经很接近我们最终的意图了. 从根源上讲, 问题出在 Java 不能直接支持所谓的"函数第一级(first-class function)"上.

JCP 社区的大佬们似乎也听到了群众的呼声, 推出的 JDK 8.0 总算是在这个问题上有了交待.

在进一步讲解之前, 我们先简单了解下"函数第一级"的概念.

函数第一级(First-class Function)

一般而言, 程序设计语言总会对计算元素的可能使用方式强加上某些限制. 带有最少限制的元素被称为具有 第一级(first-class) 的状态. 第一级元素的某些"权利或者特权"包括:

  • 可以用变量命名;
  • 可以提供给过程作为参数;
  • 可以由过程作为结果返回;
  • 可以包含在数据结构中.

注: 以上说法直接来自<< SICP >>一书中, 这里所谓的"过程", 可以认为就是"方法"或者"函数".

程序设计语言元素的第一级状态的概念应归功于英国计算机科学家 Christopher Strachey.

简单地讲, 函数第一级就是函数可以做参数, 可以作为返回值等等.

高阶函数(Higher Order Function)

有了函数第一级, 一些函数就可以接受函数作为参数, 也可以把函数作为返回值返回, 这样的函数, 我们称之为"高阶函数(higher order function)", 高阶函数可以为我们提供强大的抽象能力, 从而消除一些我们用普通方式不能或者很难消除的重复.

简单讲, 可以认为它们是函数的函数, 用我们前面的话讲, 它们是代码的代码, 抽象之抽象, 模板的模板, 等等.

泛函的解决方案(lambda 式)

有了 JDK 1.8, 有了函数第一级, 我们就可以把 sum 函数定义为一个高阶函数, 它接受一个函数作为参数, 这里用 java.uitl 包下的 Function 类型表示这样一个泛函参数:

public static int sum(Function<Integer, Integer> f) {
	int sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += f.apply(i);
	}
	return sum;
}

它有个 apply 方法, 但并不需要我们去实现, 传递给它的方法就是它的实现, 所以直接传递一个方法引用给它即可:

public static void main(String[] args) {
	int idSum = sum(MathUtil::identity);
	int sqSum = sum(MathUtil::square);
	int cbSum = sum(MathUtil::cube);
}

注意这里的写法, 类后面跟着两个冒号( :: ), 然后是方法名.

这里并没有在调用这个方法, 没有括号, 也没有参数, 实际上它就是我们一开始所设想的那种意图, 仅仅是传递一个方法引用而已.

跟反射的方式比较的话, 它不是一个 String, 而更像是一个符号类型(Symbol ), 支持编译器检查, 也支持 IDE 的代码提示, 如果你写错了, IDE 会提示你出错了, 不用像反射那样到运行期才能知道.

这里甚至可以使用所谓的 lambda 表达式, 进行所谓"函数式编程":

int idSum = sum(i -> i);// 求普通和
int sqSum = sum(i -> i * i);// 求平方和
int cbSum = sum(i -> i * i * i);// 求立方和

int dbSum = sum(i -> 2 * i);// 求两倍和
int qrSum = sum(i -> i * i * i * i);// 求四次方和

这里的箭头表达式就是所谓的 lambda 表达式了, 可以看到, 我们可以很轻松地写出求普通和, 平方和, 立方和, 乃至四次方和等等, 几乎消除了所有的脚手架式的代码, 非常简洁优雅.

也可以直接复用 Math 类中的方法:

double sinSum = sumf(Math::sin);

因为 sin 需要 double 的参数, 这里需要调整 sum 的参数为 double:

public static double sumf(Function<Double, Double> f) {
	double sum = 0;
	for (int i = 1; i <= 100; i++) {
		sum += f.apply((double) i);
	}
	return sum;
}

然后还可以直接复用 Math 里的 pow 方法来做平方和立方等等:

double sqSum2 = sumf(i -> Math.pow(i, 2));
double cbSum2 = sumf(i -> Math.pow(i, 3));

总结

可以看出, 引入了函数式编程后, 代码显得直接, 简洁, 优雅. 利用高阶函数的抽象, 我们去除了重复, 消除了耦合.

由于篇幅的关系, 关于泛型与泛函的一个综合总结, 留待下篇再分析.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127243.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jvm垃圾回收算法有哪些及原理

目录 垃圾回收器1 Serial收集器2 Parallel收集器3 ParNew收集器4 CMS收集器5 G1回收器三色标记算法标记算法的过程三色标记算法缺陷多标漏标 垃圾回收器 垃圾回收机制&#xff0c;我们已经知道什么样的对象会成为垃圾。对象回收经历了什么——垃圾回收算法。那么谁来负责回收垃…

电子宣传册制作攻略,打造完美视觉效果

随着互联网的普及&#xff0c;电子宣传册已成为企业宣传的重要手段之一。但是你知道如何制作一份吸引人的电子宣传册&#xff0c;打造完美的视觉效果呢&#xff1f; 我们只需利用FLBOOK这个在线电子期刊制作平台&#xff0c;就可以打造出完美视觉效果的电子宣传册&#xff0c;操…

postgres和postgis下载链接

PostGIS Index of /postgis/windows/ postgres PostgreSQL 10.9 (64-bit) Download arcgis支持的版本 适用于 PostgreSQL 的 ArcGIS 11.1 和 ArcGIS Pro 3.1 要求 适用于—ArcGIS Enterprise | ArcGIS Enterprise 文档 arcgis如何连接 ArcMap 中的数据库连接—ArcMap | 文…

亚马逊添加购物车和收藏有什么区别

亚马逊的添加购物车和收藏是两个不同的功能&#xff0c;它们在用户行为和用途上有明显的区别&#xff1a; 1、添加购物车&#xff08;Add to Cart&#xff09;&#xff1a; 当用户点击"添加到购物车"按钮时&#xff0c;所选商品将被放入他们的购物车&#xff0c;而…

Power BI 傻瓜入门 7. 清理、转换和加载数据

本章内容包括 根据异常、属性和数据质量问题确定清理需求解决数据类型、值、键、结构和查询之间的不一致在数据加载之前根据查询和命名约定对数据进行流化处理 要进行任何数据清理和转换&#xff0c;您的组织需要分析师、工程师和侦探。这里的想法是&#xff0c;在进入系统之…

严重内卷的电商直播还有机会吗?教你如何在电商直播中脱颖而出!

用行业内的一句话来讲&#xff1a;如今的电商直播带货&#xff0c;没有最卷只有更卷&#xff01; 过去&#xff0c;电商直播带货只是中规中矩的“叫卖式”直播。随着“全民直播带货”的潮流兴起&#xff0c;电商直播带货行业的竞争变得越来越激烈&#xff1a;同质化的内容不断…

ADC架构种类说明_笔记

ADC架构种类说明 FLASH_架构ADC 可达GHz级别Pipeline_ADC架构SAR ADC架构![在这里插入图片描述](https://img-blog.csdnimg.cn/d42b65568b6648ec92b04e7b6c53fa0d.png?x-oss-processimage/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6Ieq5bCP5ZCD5aSa,size_20,col…

怎么下载微信视频号视频?

你是否曾经在浏览视频号时看到了一些精彩的视频号&#xff0c;希望能够保存下来&#xff0c;但却不知道如何下载&#xff1f;别担心&#xff01;本篇文章将为你介绍一个方便易用的视频号下载工具&#xff0c;让你轻松保存喜欢的视频号视频&#xff01;犀牛下载是一款专门为微信…

drf-分页,coreapi自动生成接口文档

目录 分页 视图类继承ListAPIView的分页具体使用方法 # 第一种PageNumberPagination页码分页 # 第二种LimitOffsetPagination偏移分页 # 第三种CursorPagination游标分页 视图类的使用 视图类继承APIView或GenericAPIView的分页使用 coreapi 如何写好接口文档 自动生…

众和策略可靠吗?A股构筑“市场底” 卫星互联网和5.5G逆势走强

可靠 周一A股三大指数低开低走&#xff0c;午后加快下探并创出今年以来新低&#xff0c;尾盘跌幅略有收窄。截至收盘&#xff0c;上证综指收于2939.29点&#xff0c;跌落1.47%&#xff1b;深证成指收于9425.98点&#xff0c;跌落1.51%&#xff1b;创业板指收于1864.91点&#…

AtCoder Beginner Contest 325 题解 A-E

目录 A - Takahashi sanB - World MeetingC - SensorsD - Printing MachineE - Our clients, please wait a moment A - Takahashi san 原题链接 题目描述 给你两个字符串&#xff0c;将第二个字符串改为san后与第一个字符串一起输出 public static void solve() throws IOExc…

spring懒加载

简介 Spring默认会在容器初始化的过程中&#xff0c;解析xml或注解&#xff0c;创建配置为单例的bean并保存到一个map中&#xff0c;这样的机制在bean比较少时问题不大&#xff0c;但一旦bean非常多时&#xff0c;spring需要在启动的过程中花费大量的时间来创建bean &#xff0…

Postman笔记

文章目录 1.安装2.简介和使用流程3 postman使用3.1 测试集与HTTP请求发送HTTP请求和分析响应数据 3.2 发送HTTP请求和分析响应数据3.3 Postman中请求体提交方式3.4 Postman使用之接口测试3.5 使用Postman新建一个mock服务3.6 请求数据的参数化3.7 断言与脚本导出 1.安装 官网地…

外汇天眼:过度交易是大忌,交易不是越多越好!

过度交易是交易中的大忌&#xff0c;因为交易并不是越多越好。为什么我们倾向于将交易失败归因于心态呢&#xff1f;这可能是因为我们认为一笔交易成功和失败的概率都是50%&#xff0c;从而让人们误以为他们具备盈利的能力。然而&#xff0c;如果我们具备盈利的能力&#xff0c…

【Java基础学习打卡21】流程控制

目录 前言一、流程控制的重要性二、流程控制结构1.顺序结构2.分支结构3.循环结构 三、顺序结构总结 前言 无论是哪种编程语言&#xff0c;都会提供流程控制结构&#xff1a;顺序结构、分支结构和循环结构。其实计算机之所以能够完成很多自动化的任务目标&#xff0c;因为它可以…

Flutter笔记:图片的 precacheImage 函数

Flutter笔记 图片的 precacheImage 函数 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/134004572 【简…

执行 SQL 响应比较慢,你有哪些排查思路?

排查思路 如果执行 SQL 响应比较慢&#xff0c;我觉得可能有以下 4 个原因&#xff1a; 第 1 个原因&#xff1a;没有索引或者导致索引失效。 第 2 个原因&#xff1a;单表数据量数据过多&#xff0c;导致查询瓶颈第 3 个原因&#xff1a;网络原因或者机器负载过高。 第 4 个原…

JAVA-编程基础-11-02-文件流

Lison <dreamlison163.com>, v1.0.0, 2023.05.07 JAVA-编程基础-11-02-文件流 文章目录 JAVA-编程基础-11-02-文件流File 构造方法File 常用方法获取功能的方法**绝对路径和相对路径****判断功能的方法****创建、删除功能的方法**目录的遍历递归遍历 RandomAccessFile*…

LeetCode66——加一

LeetCode66——加一 题目描述&#xff1a; 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。 示例…

904. Fruit Into Baskets

904. Fruit Into Baskets 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 904. Fruit Into Baskets https://leetcode.cn/problems/fruit-into-baskets/ 完成情况&#xff1a; 解题思路&#xff1a; 连续数组 -…