最近在看函数式编程相关的内容,并且结合公司一些人用函数式变成的处理,产生一个疑问,从算法复杂度和速度来说,很多时候一个循环能做到的事情,会用 lambda 循环多次处理,这样做的根据是什么?
1
golangLover 2022-10-01 12:12:19 +08:00
其实不要盲目使用 parallelStream, 很多数据量不大的情况下适得其反。
另外就是函数式的目的是为了写法,而不是为了速度 |
2
golangLover 2022-10-01 12:13:27 +08:00
现在的人这么卷吗?国庆还写代码
|
3
lerefe OP @golangLover 哈哈哈,没买到票回不去,看点东西
|
4
TWorldIsNButThis 2022-10-01 12:17:04 +08:00 via iPhone 18
好处是第二种我看一遍代码的函数调用就知道在干嘛最后产生了什么结果
第一种不去跟着他的代码在脑子里模拟运行一下的话不知道最后得到了个什么东西,看了跟没看一样 |
5
lerefe OP @TWorldIsNButThis 这个角度很有说服力
|
6
Nasei 2022-10-01 12:35:15 +08:00 via iPhone
性能方面,如果你的数据量十分巨大,后面那种可以很方便的改成集群式并发处理
|
7
TWorldIsNButThis 2022-10-01 12:41:23 +08:00 via iPhone 1
grouping 的第二个参数可以直接写对 group 内元素的处理,collectors.maxby(comparator.comparing(Book::getPrice)),
然后.values().stream().flatMap(Optional::stream) 代码可以更简短一些 |
8
rabbbit 2022-10-01 12:43:57 +08:00
个人理解就是更易读、易维护?缺点是某些场景下存在性能问题。
挺适合前端的(数据量小,需求、接口、数据格式总是变来变去) |
9
lerefe OP @TWorldIsNButThis 感谢指教
|
11
L4Linux 2022-10-01 13:04:37 +08:00
而且第二种可重用性高一些,注释里也写了改一行就可以 XXX 。
|
12
Nasei 2022-10-01 13:05:22 +08:00 via iPhone
@lerefe 我说的是分布式计算的一种模式,你可以了解下 data parallel model 和 mapreduce
|
13
agagega 2022-10-01 13:07:40 +08:00 via iPhone
不可变能提高抽象层次(有助于并行或者向量化等优化),同时增强可读性
|
14
v2eb 2022-10-01 13:21:13 +08:00 via Android 1
|
15
yayiji 2022-10-01 13:25:30 +08:00 via Android 6
后者可读性强
结构固若金汤 可以参考马丁对编程范式的论述 结构化编程 我们限制了 goto 的使用 面向对象编程 我们限制了指针的能力 函数式编程 我们限制了可变变量的使用 The unavoidable price of reliability is simplicity. |
17
zhouyg 2022-10-01 14:34:25 +08:00
函数式是声明式编程的范畴,而上面的代码是典型的指令式编程。所以这里的优势就是声明式编程相对于指令式编程的优势,也就是 readability, usability
|
18
QKgf555H87Fp0cth 2022-10-01 14:54:29 +08:00
停止使用 var
|
20
Huelse 2022-10-01 15:57:29 +08:00
很明显的区别之一就是第一种使用了变量,而第二种没有,或者说系统帮你维护了变量,
这样的写法用老外的话来说就是代码很健壮 |
21
nightwitch 2022-10-01 16:07:02 +08:00
无状态的函数在并行编程时可以避免引入锁
|
22
july1995 2022-10-01 16:51:06 +08:00 via iPhone
@zxCoder var 因为作用域的问题,现在很少使用,一般用 let const 。var 还有重复声明的问题。
|
23
yigecaiji 2022-10-01 18:12:40 +08:00 via Android
@july1995 这是 Java ,不是 JavaScript ,还是说 Java 现在引入 let 了?
|
24
wdwwtzy 2022-10-01 18:20:22 +08:00 4
不得不说,这 java 的 stream 太难用了,也太难看了。
学学 C#的 linq 吧,大概是这样 books.GroupBy(b=> b.CategoryId) .SelectMany(g => g.OrderBy(b=> b.Price).Take(1)) .ToList(); |
25
cp19890714 2022-10-01 18:28:59 +08:00 via Android
第二种,代码直接表明了意图,想做什么事就调用对应的函数,这更接近人的思维。
第一种,往往要把代码看完,才能猜出意图,而且使用很多无实际意义的临时变量,增加心智负担。 |
26
july1995 2022-10-01 18:39:48 +08:00 via iPhone
@yigecaiji 哈哈,抱歉,看错了看错了,图片加载不出来,又看到有人提到 var ,下意识认为这是 JavaScript 。
|
27
paopjian 2022-10-01 19:00:51 +08:00
看第一个还能理解,第二个感觉对函数的功能理解需要非常全面,忘了一个函数就得找半天相关方法了,感觉调用超过三层不用 ide 自动提示就不知道怎么写了
|
28
FrankFang128 2022-10-01 19:03:09 +08:00
函数式一直就不以执行速度为优先
|
31
ghui 2022-10-01 19:20:28 +08:00 via iPhone
新的思路提高抽象层次,你想干啥告诉我(声明式)就行了,具体怎么实现的调用者不需要关心
|
32
chihiro2014 2022-10-01 19:47:58 +08:00
普通的写法只是让我们编写逻辑不混乱。函数式是抽象了过程,只关心入参和结果
|
33
xuanbg 2022-10-01 20:37:01 +08:00
并不会有什么优势!
计算机可不会管你是不是函数式,编译完都是机器码。CPU 可不会因为你的机器码由函数式的代码编译而来就执行得快一些。 |
34
wupher 2022-10-01 20:37:10 +08:00
浅见:
1. 实际执行时多次 map / collect / filter 会被合并至一起,提升效率 2. 没有中间变量并发时更可靠和安全 3. 于 Actor/flow/Reactor 模式下,每一步都可拆解为一个事件 /信号,可以更好的利用多核进行并发处理。 |
35
mxT52CRuqR6o5 2022-10-01 20:40:46 +08:00
有一些约定俗称的函数能让你快速明白代码干了些什么
比较限制副作用类型代码的书写,减少阅读时的心智负担 |
36
mxT52CRuqR6o5 2022-10-01 20:46:15 +08:00 1
像你举的那个函数式的例子,逻辑写成了一条一条的,阅读的时候只需要从上往下阅读,只需要依次关注每一条的功能和作用,记住上一条代码的返回值就不需要再关心上一条代码具体做了些什么
像非函数式的那个例子,阅读的时候就需要整体都做了些什么,他在第一行声明了一个 map ,接下来的阅读中就需要时刻关心 map 具体发生了些什么,当前 map 的状态是什么 |
37
ChefIsAwesome 2022-10-01 21:13:42 +08:00
链,或者叫 flow ,因为函数是 pure 的,随便抽一段连续的,就得到了一个新的函数,就能拿到其它地方复用。这不就是程序员梦寐以求的最简单、也是最高级的模块化。
|
38
lmshl 2022-10-01 21:18:57 +08:00 1
|
39
loveyu 2022-10-01 21:32:07 +08:00 via Android
说句不好听的,见过大量在复杂业务逻辑盲目使用方式二导致的性能和阅读困难
|
40
Anarchy 2022-10-01 21:42:53 +08:00
对我而言就两点好处:
1. 提供操作符减少了部分操作代码编写 2. 整个代码结构从上到下就是对应逻辑链条,熟悉操作符很快就能看懂逻辑了 支撑使用的原因就是第二条了 |
41
Mogeko 2022-10-01 22:04:04 +08:00 via iPhone
因为 Java 的函数式本来就是残废的。
像 Haskell 这类正统的函数式语言,默认珂里化,甚至参数都不用写全。灵活又高效。 另外函数式编程最大的爽点是无副作用所带来的心智负担的降低;以及超高的鲁棒性。在这两点好处面前,性能的些微下降不值一提。 |
42
iseki 2022-10-01 22:27:12 +08:00
第一种我必须人脑运行一次才能理解发生了什么;第二种虽然能一眼看明白在干啥,但是写的有点恶心,但我认为这是 Java 的问题,用 Kotlin 就好了
|
43
zmal 2022-10-01 22:49:33 +08:00
函数式写法很大程度上是为了增加可读性,但 op 你发的这个写法 2 个人认为挺拉的。
可读性没降低多少,时间复杂度从 n 升级到 n * log n 。 |
44
lmshl 2022-10-01 23:23:02 +08:00
写法 2 性能差不是 fp 的原因,而是楼主没能等价改写。
实际上这里应该用 foldLeft 而不是 sorted/findFirst 在 java stream api 中应该 reduce 是可以用的 这样两段代码复杂度就一样了 |
45
zmal 2022-10-01 23:26:44 +08:00
|
46
zmal 2022-10-01 23:46:16 +08:00
|
47
abc612008 2022-10-01 23:56:07 +08:00 2
来个 kotlin 版本,比 java stream 舒服很多。(吹爆 kotlin
```kotlin data class Book(val category: String, val price: Double) fun mostExpensiveByCategory(books: List<Book>): Map<String, Book> { return books.groupBy { it.category } .mapValues { (_, books) -> books.maxBy { it.price } } } ``` |
48
msg7086 2022-10-02 00:53:14 +08:00
函数式一般可以把算法轻松地拆分成多个步骤。
第一种写法的坏处就是在任何一个时间点的数据都缺乏一致性,即一半数据处理了,一半数据没处理。 如果数据出错,你打断点拿到的也是这样一半一半的数据。 函数式这样每次跑一步出来都是完整的数据集,一眼就能找到问题点。 |
49
zjp 2022-10-02 01:35:31 +08:00
一次遍历就可以了,感觉楼上代码都被楼主带偏
每个 key 对应单值而不是集合的优先考虑 Collectors.toMap() dataSource.stream().collect(Collectors.toMap(Book::getCategory, Function.identity(), (o1, o2) -> o1.getPrice().compareTo(o2.getPrice()) > 0 ? o1 : o2)).values(); 理论上总能用 lambda 写出一样的复杂度,只是可能有 API 的限制 |
50
GeruzoniAnsasu 2022-10-02 03:18:38 +08:00
> 从算法复杂度和速度来说
从这些角度来说函数式不一定有优势。 函数式编程的本质是定义 A 到 B 的变换映射,当映射比流程更明确时,函数式更容易写对;反之若步骤和流程比映射更明确,那么强行使用函数式风格则是下策。 |
51
zhuweiyou 2022-10-02 10:48:24 +08:00
这点数据 循环一次和循环几次没啥差别, 我的原则是能一行代码解决的 不写七八行.
|
52
zddwj 2022-10-02 20:52:32 +08:00 via Android
链式调用并不是函数式编程的专利,函数式编程的核心特征是不对变量进行重复赋值
|
53
Envov 2022-10-03 00:10:21 +08:00
函数式编程很多时候都会牺牲一定的性能,但是获得了可读性上的增强。
我个人很喜欢在 javascript 里面使用函数式,比如说组合函数,纯函数,hof 等等,但是同事都不是很认可,后来放弃了。只在自己的项目里面用。 |
54
AllenHua 2022-10-03 10:55:51 +08:00 via iPhone
Java 可真残废 (doge )
|
55
optional 2022-10-03 12:11:26 +08:00
可读性强,可测试性高,可并行化改造,可以从组合的维度思考问题。同时流程清晰,可以很方便的进行优化,包括语法编译层面的优化。
|
56
QKgf555H87Fp0cth 2022-10-03 15:02:17 +08:00
|
57
git00ll 2022-10-03 22:51:58 +08:00
简单场景下,虽然第二种方式可读性更高一些。
但是因为是简单场景,完全可以给这个方法加一行注释 “按照 xxx 分组,获取每个分组内 xxx 最小的元素” 来解决可读性差的问题。 但是第二种写法的扩展性就不如第一种了,如取分组内最小的和第二小的,改起来逻辑就没这么顺了。 |
59
acapla 2022-10-04 04:13:32 +08:00
楼主在看哪本书啊? 可以推荐一下学习资料吗?
|
60
lmshl 2022-10-04 10:49:53 +08:00 1
@WispZhan
akka-stream 、zstream 、fs2 都有上生产环境,目前用下来总体感觉 zio 的模型是上手最快,最容易写的。 akka-stream 的错误处理建模会很恶心,fs2 和 zstream 差不多但是 zstream 的类型没有 fs2 那么高理解成本。 |
62
yayiji 2022-10-04 12:55:21 +08:00 via Android 1
@Mistwave
来自 架构整洁之道 「如你所见,我在介绍三个编程范式的时候,有意采用了上面这种格式,目的是凸显每个编程范式的实际含义——它们都从某一方面限制和规范了程序员的能力。没有一个范式是增加新能力的。也就是说,每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。 另外,我们应该认识到,这三个编程范式分别限制了 goto 语句、函数指针和赋值语句的使用。那么除此之外,还有什么可以去除的吗? 没有了。因此这三个编程范式可能是仅有的三个了——如果单论去除能力的编程范式的话。支撑这一结论的另外一个证据是,三个编程范式都是在 1958 年到 1968 年这 10 年间被提出来的,后续再也没有新的编程范式出现过。」 |
63
xavierchow 2022-10-05 00:35:38 +08:00
仅从题主的例子来说,
第 1 点不同是可读性,imperative VS declarative , 第 2 点关于复杂度的担忧(循环),同样是函数式的写法可以用 fold / reduce, 另外尽量用 transducer ,看上去很多的 transformer 实际上也只是循环一次。(不清楚 java 是否有类似的东西,https://clojure.org/reference/transducers 是 clojure 在性能方面的考量和努力) |
64
amlee 2022-10-06 15:18:31 +08:00
@TWorldIsNButThis 换一种说法,好像就是把代码从命令式变成声明式
|