V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lerefe
V2EX  ›  Java

请教关于函数式编程的优势

  •  
  •   lerefe · 2022-10-01 12:08:30 +08:00 · 8646 次点击
    这是一个创建于 792 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在看函数式编程相关的内容,并且结合公司一些人用函数式变成的处理,产生一个疑问,从算法复杂度和速度来说,很多时候一个循环能做到的事情,会用 lambda 循环多次处理,这样做的根据是什么? MJVis.png

    65 条回复    2022-12-03 11:27:48 +08:00
    golangLover
        1
    golangLover  
       2022-10-01 12:12:19 +08:00
    其实不要盲目使用 parallelStream, 很多数据量不大的情况下适得其反。
    另外就是函数式的目的是为了写法,而不是为了速度
    golangLover
        2
    golangLover  
       2022-10-01 12:13:27 +08:00
    现在的人这么卷吗?国庆还写代码
    lerefe
        3
    lerefe  
    OP
       2022-10-01 12:15:38 +08:00 via iPhone
    @golangLover 哈哈哈,没买到票回不去,看点东西
    TWorldIsNButThis
        4
    TWorldIsNButThis  
       2022-10-01 12:17:04 +08:00 via iPhone   ❤️ 18
    好处是第二种我看一遍代码的函数调用就知道在干嘛最后产生了什么结果

    第一种不去跟着他的代码在脑子里模拟运行一下的话不知道最后得到了个什么东西,看了跟没看一样
    lerefe
        5
    lerefe  
    OP
       2022-10-01 12:18:46 +08:00 via iPhone
    @TWorldIsNButThis 这个角度很有说服力
    Nasei
        6
    Nasei  
       2022-10-01 12:35:15 +08:00 via iPhone
    性能方面,如果你的数据量十分巨大,后面那种可以很方便的改成集群式并发处理
    TWorldIsNButThis
        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)
    代码可以更简短一些
    rabbbit
        8
    rabbbit  
       2022-10-01 12:43:57 +08:00
    个人理解就是更易读、易维护?缺点是某些场景下存在性能问题。
    挺适合前端的(数据量小,需求、接口、数据格式总是变来变去)
    lerefe
        9
    lerefe  
    OP
       2022-10-01 12:44:24 +08:00 via iPhone
    @TWorldIsNButThis 感谢指教
    lerefe
        10
    lerefe  
    OP
       2022-10-01 12:45:43 +08:00 via iPhone
    @Nasei 你说的是 parallelStream 吗
    L4Linux
        11
    L4Linux  
       2022-10-01 13:04:37 +08:00
    而且第二种可重用性高一些,注释里也写了改一行就可以 XXX 。
    Nasei
        12
    Nasei  
       2022-10-01 13:05:22 +08:00 via iPhone
    @lerefe 我说的是分布式计算的一种模式,你可以了解下 data parallel model 和 mapreduce
    agagega
        13
    agagega  
       2022-10-01 13:07:40 +08:00 via iPhone
    不可变能提高抽象层次(有助于并行或者向量化等优化),同时增强可读性
    v2eb
        14
    v2eb  
       2022-10-01 13:21:13 +08:00 via Android   ❤️ 1
    yayiji
        15
    yayiji  
       2022-10-01 13:25:30 +08:00 via Android   ❤️ 6
    后者可读性强
    结构固若金汤

    可以参考马丁对编程范式的论述
    结构化编程 我们限制了 goto 的使用
    面向对象编程 我们限制了指针的能力
    函数式编程 我们限制了可变变量的使用

    The unavoidable price of reliability is simplicity.
    lerefe
        16
    lerefe  
    OP
       2022-10-01 13:26:15 +08:00
    @v2eb 你也在看程序袁老爸的视频吗
    zhouyg
        17
    zhouyg  
       2022-10-01 14:34:25 +08:00
    函数式是声明式编程的范畴,而上面的代码是典型的指令式编程。所以这里的优势就是声明式编程相对于指令式编程的优势,也就是 readability, usability
    likunyan
        18
    likunyan  
       2022-10-01 14:54:29 +08:00
    停止使用 var
    zxCoder
        19
    zxCoder  
       2022-10-01 15:20:58 +08:00
    @likunyan 请问为何
    Huelse
        20
    Huelse  
       2022-10-01 15:57:29 +08:00
    很明显的区别之一就是第一种使用了变量,而第二种没有,或者说系统帮你维护了变量,
    这样的写法用老外的话来说就是代码很健壮
    nightwitch
        21
    nightwitch  
       2022-10-01 16:07:02 +08:00
    无状态的函数在并行编程时可以避免引入锁
    july1995
        22
    july1995  
       2022-10-01 16:51:06 +08:00 via iPhone
    @zxCoder var 因为作用域的问题,现在很少使用,一般用 let const 。var 还有重复声明的问题。
    yigecaiji
        23
    yigecaiji  
       2022-10-01 18:12:40 +08:00 via Android
    @july1995 这是 Java ,不是 JavaScript ,还是说 Java 现在引入 let 了?
    wdwwtzy
        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();
    cp19890714
        25
    cp19890714  
       2022-10-01 18:28:59 +08:00 via Android
    第二种,代码直接表明了意图,想做什么事就调用对应的函数,这更接近人的思维。
    第一种,往往要把代码看完,才能猜出意图,而且使用很多无实际意义的临时变量,增加心智负担。
    july1995
        26
    july1995  
       2022-10-01 18:39:48 +08:00 via iPhone
    @yigecaiji 哈哈,抱歉,看错了看错了,图片加载不出来,又看到有人提到 var ,下意识认为这是 JavaScript 。
    paopjian
        27
    paopjian  
       2022-10-01 19:00:51 +08:00
    看第一个还能理解,第二个感觉对函数的功能理解需要非常全面,忘了一个函数就得找半天相关方法了,感觉调用超过三层不用 ide 自动提示就不知道怎么写了
    FrankFang128
        28
    FrankFang128  
       2022-10-01 19:03:09 +08:00
    函数式一直就不以执行速度为优先
    Chad0000
        29
    Chad0000  
       2022-10-01 19:09:31 +08:00 via iPhone
    @wdwwtzy
    你就静静的看他们捧 Java 多好。
    Rache1
        30
    Rache1  
       2022-10-01 19:18:20 +08:00
    @wdwwtzy Laravel 的 Collections 也不错 😏
    ghui
        31
    ghui  
       2022-10-01 19:20:28 +08:00 via iPhone
    新的思路提高抽象层次,你想干啥告诉我(声明式)就行了,具体怎么实现的调用者不需要关心
    chihiro2014
        32
    chihiro2014  
       2022-10-01 19:47:58 +08:00
    普通的写法只是让我们编写逻辑不混乱。函数式是抽象了过程,只关心入参和结果
    xuanbg
        33
    xuanbg  
       2022-10-01 20:37:01 +08:00
    并不会有什么优势!
    计算机可不会管你是不是函数式,编译完都是机器码。CPU 可不会因为你的机器码由函数式的代码编译而来就执行得快一些。
    wupher
        34
    wupher  
       2022-10-01 20:37:10 +08:00
    浅见:

    1. 实际执行时多次 map / collect / filter 会被合并至一起,提升效率
    2. 没有中间变量并发时更可靠和安全
    3. 于 Actor/flow/Reactor 模式下,每一步都可拆解为一个事件 /信号,可以更好的利用多核进行并发处理。
    mxT52CRuqR6o5
        35
    mxT52CRuqR6o5  
       2022-10-01 20:40:46 +08:00
    有一些约定俗称的函数能让你快速明白代码干了些什么
    比较限制副作用类型代码的书写,减少阅读时的心智负担
    mxT52CRuqR6o5
        36
    mxT52CRuqR6o5  
       2022-10-01 20:46:15 +08:00   ❤️ 1
    像你举的那个函数式的例子,逻辑写成了一条一条的,阅读的时候只需要从上往下阅读,只需要依次关注每一条的功能和作用,记住上一条代码的返回值就不需要再关心上一条代码具体做了些什么
    像非函数式的那个例子,阅读的时候就需要整体都做了些什么,他在第一行声明了一个 map ,接下来的阅读中就需要时刻关心 map 具体发生了些什么,当前 map 的状态是什么
    ChefIsAwesome
        37
    ChefIsAwesome  
       2022-10-01 21:13:42 +08:00
    链,或者叫 flow ,因为函数是 pure 的,随便抽一段连续的,就得到了一个新的函数,就能拿到其它地方复用。这不就是程序员梦寐以求的最简单、也是最高级的模块化。
    lmshl
        38
    lmshl  
       2022-10-01 21:18:57 +08:00   ❤️ 1
    资深函数式码农(自封)来扯两句:
    于我而言函数式最大的优势在于,容易写对,且容易分析,容易理解。同时附带了容易并行的优势
    而执行速度,函数式写法确实慢于专家优化过的指令式,但比普通 CRUD 农写的指令式代码更快是基本操作了。
    loveyu
        39
    loveyu  
       2022-10-01 21:32:07 +08:00 via Android
    说句不好听的,见过大量在复杂业务逻辑盲目使用方式二导致的性能和阅读困难
    Anarchy
        40
    Anarchy  
       2022-10-01 21:42:53 +08:00
    对我而言就两点好处:
    1. 提供操作符减少了部分操作代码编写
    2. 整个代码结构从上到下就是对应逻辑链条,熟悉操作符很快就能看懂逻辑了
    支撑使用的原因就是第二条了
    Mogeko
        41
    Mogeko  
       2022-10-01 22:04:04 +08:00 via iPhone
    因为 Java 的函数式本来就是残废的。

    像 Haskell 这类正统的函数式语言,默认珂里化,甚至参数都不用写全。灵活又高效。

    另外函数式编程最大的爽点是无副作用所带来的心智负担的降低;以及超高的鲁棒性。在这两点好处面前,性能的些微下降不值一提。
    iseki
        42
    iseki  
       2022-10-01 22:27:12 +08:00
    第一种我必须人脑运行一次才能理解发生了什么;第二种虽然能一眼看明白在干啥,但是写的有点恶心,但我认为这是 Java 的问题,用 Kotlin 就好了
    zmal
        43
    zmal  
       2022-10-01 22:49:33 +08:00
    函数式写法很大程度上是为了增加可读性,但 op 你发的这个写法 2 个人认为挺拉的。
    可读性没降低多少,时间复杂度从 n 升级到 n * log n 。
    lmshl
        44
    lmshl  
       2022-10-01 23:23:02 +08:00
    写法 2 性能差不是 fp 的原因,而是楼主没能等价改写。
    实际上这里应该用 foldLeft 而不是 sorted/findFirst
    在 java stream api 中应该 reduce 是可以用的
    这样两段代码复杂度就一样了
    zmal
        45
    zmal  
       2022-10-01 23:26:44 +08:00
    如果是我的话可能会这样写:

    https://imgur.com/YBBKeIT
    zmal
        46
    zmal  
       2022-10-01 23:46:16 +08:00
    abc612008
        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 } }
    }
    ```
    msg7086
        48
    msg7086  
       2022-10-02 00:53:14 +08:00
    函数式一般可以把算法轻松地拆分成多个步骤。
    第一种写法的坏处就是在任何一个时间点的数据都缺乏一致性,即一半数据处理了,一半数据没处理。
    如果数据出错,你打断点拿到的也是这样一半一半的数据。
    函数式这样每次跑一步出来都是完整的数据集,一眼就能找到问题点。
    zjp
        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 的限制
    GeruzoniAnsasu
        50
    GeruzoniAnsasu  
       2022-10-02 03:18:38 +08:00
    > 从算法复杂度和速度来说

    从这些角度来说函数式不一定有优势。




    函数式编程的本质是定义 A 到 B 的变换映射,当映射比流程更明确时,函数式更容易写对;反之若步骤和流程比映射更明确,那么强行使用函数式风格则是下策。
    zhuweiyou
        51
    zhuweiyou  
       2022-10-02 10:48:24 +08:00
    这点数据 循环一次和循环几次没啥差别, 我的原则是能一行代码解决的 不写七八行.
    zddwj
        52
    zddwj  
       2022-10-02 20:52:32 +08:00 via Android
    链式调用并不是函数式编程的专利,函数式编程的核心特征是不对变量进行重复赋值
    Envov
        53
    Envov  
       2022-10-03 00:10:21 +08:00
    函数式编程很多时候都会牺牲一定的性能,但是获得了可读性上的增强。
    我个人很喜欢在 javascript 里面使用函数式,比如说组合函数,纯函数,hof 等等,但是同事都不是很认可,后来放弃了。只在自己的项目里面用。
    AllenHua
        54
    AllenHua  
       2022-10-03 10:55:51 +08:00 via iPhone
    Java 可真残废 (doge )
    optional
        55
    optional  
       2022-10-03 12:11:26 +08:00
    可读性强,可测试性高,可并行化改造,可以从组合的维度思考问题。同时流程清晰,可以很方便的进行优化,包括语法编译层面的优化。
    likunyan
        56
    likunyan  
       2022-10-03 15:02:17 +08:00
    @lerefe @july1995 不好意思,看错了 :->
    git00ll
        57
    git00ll  
       2022-10-03 22:51:58 +08:00
    简单场景下,虽然第二种方式可读性更高一些。
    但是因为是简单场景,完全可以给这个方法加一行注释 “按照 xxx 分组,获取每个分组内 xxx 最小的元素”
    来解决可读性差的问题。

    但是第二种写法的扩展性就不如第一种了,如取分组内最小的和第二小的,改起来逻辑就没这么顺了。
    WispZhan
        58
    WispZhan  
       2022-10-03 23:09:02 +08:00 via Android
    @lmshl zio 好用吗?
    acapla
        59
    acapla  
       2022-10-04 04:13:32 +08:00
    楼主在看哪本书啊? 可以推荐一下学习资料吗?
    lmshl
        60
    lmshl  
       2022-10-04 10:49:53 +08:00   ❤️ 1
    @WispZhan
    akka-stream 、zstream 、fs2 都有上生产环境,目前用下来总体感觉 zio 的模型是上手最快,最容易写的。

    akka-stream 的错误处理建模会很恶心,fs2 和 zstream 差不多但是 zstream 的类型没有 fs2 那么高理解成本。
    Mistwave
        61
    Mistwave  
       2022-10-04 12:26:59 +08:00 via iPhone
    @yayiji 请问这个原文哪里有?
    yayiji
        62
    yayiji  
       2022-10-04 12:55:21 +08:00 via Android   ❤️ 1
    @Mistwave
    来自 架构整洁之道

    「如你所见,我在介绍三个编程范式的时候,有意采用了上面这种格式,目的是凸显每个编程范式的实际含义——它们都从某一方面限制和规范了程序员的能力。没有一个范式是增加新能力的。也就是说,每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。

    另外,我们应该认识到,这三个编程范式分别限制了 goto 语句、函数指针和赋值语句的使用。那么除此之外,还有什么可以去除的吗?

    没有了。因此这三个编程范式可能是仅有的三个了——如果单论去除能力的编程范式的话。支撑这一结论的另外一个证据是,三个编程范式都是在 1958 年到 1968 年这 10 年间被提出来的,后续再也没有新的编程范式出现过。」
    xavierchow
        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 在性能方面的考量和努力)
    amlee
        64
    amlee  
       2022-10-06 15:18:31 +08:00
    @TWorldIsNButThis 换一种说法,好像就是把代码从命令式变成声明式
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2597 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:40 · PVG 18:40 · LAX 02:40 · JFK 05:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.