浅谈函数式编程

43次阅读

共计 1676 个字符,预计需要花费 5 分钟才能阅读完成。

1. 前言

最近开始忙起来了, 写东西的时间越来越少了。这几天开始在 Java 开发中尝试函数式编程风格。所以就写点小东西来分享一下。

2. 什么是函数式编程?

在我看来函数式编程就是把函数作为一等公民来使用。平常我们开发都是在处理数据。面向对象中都在处理类。而函数式编程不是在写函数就是在写函数的路上。我们来看看 Java 中函数式编程的演进之路。我小时候玩过一种廉价的黑白屏游戏机,只能玩俄罗斯方块。这种是不可扩展的。

  public void playGame(){//  只能玩俄罗斯方块}

这种机器给童年带来了不少的乐趣。后来小伙伴有了一台 gameboy,这种掌机的好处在于它可以插卡。一张卡一个游戏,最有名的莫过于《超级马里奥》和《打砖块》了。这时候机器是完全可扩展的了。

      public void  playGameboy(GameboyCard card){card.getGame().run()}

虽然可以扩展但是卡的价格非常贵,当时买卡的渠道还很少。那时候其实就想如果能自己造 gameboy 游戏就好了(然后我就来搞编程?)。理想中的游戏机是我们不关心你什么风格的游戏,只要你能放入符合接口的游戏卡中并且在我这个游戏机中跑就行。

所以我们定义了一个固定的游戏卡接口:

 /**
 * @author Felordcn
 * @since 2019/10/31 22:13
 */
@FunctionalInterface
public interface Card {Game  apply();

}

只要符合这种接口的游戏卡都能插到机器中玩:

    /**
     * Fun.
     *
     * @param card the card
     */
    void fun(Card card) {Game game = card.apply();
        game.run();}

熟练面向对象的同学们会说这不就是面向接口编程吗?是的你说的没有问题。但是这里 Card 接口只干一件事就是提供游戏。我们的重心是游戏卡吗?显然不是!有趣好玩的游戏才是我们的目的所在。于是我们不管他是卡还是光碟甚至网路,只要能提供游戏给我们娱乐都符合我们的需要。

  // 玩插卡游戏机
  fun(() -> new CardGame());
   // 玩 PSP
   fun(() -> new PSPGame());
   // more

作为一名码农,平常我们都在写 SQL。无论大厂小厂,不管单体还是分布式。SQL 总能帮我们解决很多业务关系处理。SELECTINSERTUPDATEDELETE 每一种命令只要是 SQL 规范数据库,不管是什么表都会是一致的操作。你声明了什么命令就执行什么操作。这时数据与函数是松耦合的。正是这样的特点让我们实现了“万变不离其宗”。这也是一种另类的函数式编程。

3. 面向对象和函数式编程冲突吗?

面向对象一直处于我能操作什么数据、这种数据我该怎么操作的范式中。而函数式编程一直沉浸于给我操作数据的方法中。面向对象最大优点是多态性和封装;函数式编程优势是抽象化和声明式命令风格,两者其实是正交,可互补的,可在同一程序中共存。争论是面向对象好还是面向函数好跟争论哪门语言好一样都是非常极端的。对于面向对象来讲:存在的并不一定都是对象,函数就是对象;对于函数式编程来说:存在的并不总是纯粹的,副作用总是真实存在的。总之,面向对象侧重于分解,函数编程侧重于组合。

4. 函数式编程特点

函数风格的编程拥有自己的一些特点:

  • 函数作为一等公民。可以作为参数传递、从函数里返回、可以赋值给变量。
  • 带有闭包的 Lambda 表达式和匿名函数,这是广泛的多态。
  • 不变性,大部分无态处理,在函数式程序中,变量是通过外部传入或者申明获得值的。变量不能被改变
  • 基于不可变进而可以无副作用的调用。
  • 通过 tail call 实现递归的性能优化。
  • 提供动态的、可组合的开发思路。

5. 总结

今天简单表达了我对函数式编程的一些理解,对于习惯了面向对象的 Java 开发者来说,理解函数式编程并不容易。它不仅仅有 Lambda 和匿名函数!更多的是一种思想。这里推荐一个很好的 Java 函数式编程库 vavr。有兴趣的同学可以学习下。

关注公众号:Felordcn 获取更多资讯

个人博客:https://felord.cn

正文完
 0

浅谈函数式编程

43次阅读

共计 5356 个字符,预计需要花费 14 分钟才能阅读完成。

本文首发于泊浮目的简书:https://www.jianshu.com/u/204…

1. 前言

一个风和日丽的下午, 我看着日常看代码做重构迁移, 看到这么段代码:

突然, 我看到了这样的代码:

    private void getTopicsDiskSizeForSomeBroker(int brokerID, AdminClient admin, Map<String, Long> topicsSizeMap) throws ExecutionException, InterruptedException {DescribeLogDirsResult ret = admin.describeLogDirs(Collections.singletonList(brokerID));
        Map<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>> tmp = ret.all().get();
        for (Map.Entry<Integer, Map<String, DescribeLogDirsResponse.LogDirInfo>> entry : tmp.entrySet()) {Map<String, DescribeLogDirsResponse.LogDirInfo> tmp1 = entry.getValue();
            for (Map.Entry<String, DescribeLogDirsResponse.LogDirInfo> entry1 : tmp1.entrySet()) {DescribeLogDirsResponse.LogDirInfo info = entry1.getValue();
                Map<TopicPartition, DescribeLogDirsResponse.ReplicaInfo> replicaInfoMap = info.replicaInfos;
                for (Map.Entry<TopicPartition, DescribeLogDirsResponse.ReplicaInfo> replicas : replicaInfoMap.entrySet()) {String topic = replicas.getKey().topic();
                    Long topicSize = topicsSizeMap.get(topic);
                    if (topicSize == null) {topicsSizeMap.put(topic, replicas.getValue().size);
                    } else {topicsSizeMap.put(topic, replicas.getValue().size + topicSize);
                    }
                }
            }
        }
    }

看了这段代码我整个人都不好了!

首先是那火箭式的三个嵌套 for 循环, 再者就是那些变量声明语句. 为了迭代他们, 我们不得不声明它一遍 …

2. 使用 Stream

    public List<KafkaTopicInfoBO> getTopicDiskSize() {return getTopicPartitionReplicaInfo().entrySet().stream()
                .map(e -> new KafkaTopicInfoBO(e.getKey().topic(), e.getValue().size))
                .collect(Collectors.toList());

    }

    protected Map<TopicPartition, DescribeLogDirsResponse.ReplicaInfo> getTopicPartitionReplicaInfo() {Properties globalConf = zkConfigService.getProperties(ZkPathUtils.GLOBAL_CONFIG);
        Properties adminConfig = new Properties();
        adminConfig.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, globalConf.getProperty((ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG)));
        AdminClient adminClient = AdminClient.create(adminConfig);
        List<String> brokerIds = zkConfigService.getChildByPath(kafkaIdsPath);
        return  brokerIds.stream()
                .map(Integer::valueOf)
                .map(Collections::singletonList)
                .map(adminClient::describeLogDirs)
                .map(DescribeLogDirsResult::all)
                .map(mapKafkaFuture -> {
                    try {return mapKafkaFuture.get();
                    } catch (Exception e) {throw new RuntimeException(e);
                    }
                })
                .map(Map::values)
                .flatMap(Collection::stream)
                .map(Map::values)
                .flatMap(Collection::stream)
                .map(e -> e.replicaInfos)
                .map(Map::entrySet)
                .flatMap(Collection::stream)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

这样看起来似乎好了一点. 但是对于不熟悉 函数式编程 的同学来说, 理解以上代码还是有点困难的.

接下来, 先来简单讲一讲函数式编程.

3. 什么是函数式编程

3.1 一句话搞懂

就像来自数学中的代数

f(x)=5x^2+4x+3
g(x)=2f(x)+5=10x^2+8x+11
h(x)=f(x)+g(x)=15x^2+12x+14

函数式编程定义输入数据和输出数据相关的关系——数学表达式里面其实是在做一种映射(Mapping), 输入的数据和输出的数据关系是什么样的, 就是用来函数定义的.

3.2 直观感受: 用代码举例

public class Quint{public static void main (String args[]){for (int i=0; i<25; i++){System.out.println(i*i);
        }
    }
}
(println (take 25 (map (fn [x] (*x x) (range)))))

简单解释一下上段 Lisp 代码:

  1. range 函数回返回一个从 0 开始的整数无穷列表
  2. 然后该列表会被传入 map, 针对列表中的每个元素, 调用平方值的匿名函数, 产生了一个无穷多的, 包含平方值的列表
  3. 将列表传入 take 函数, 仅仅返回前 25 个
  4. println 将接入的参数输出

4. 使用对函数式编程支持更好的 Kotlin


    protected fun getTopicPartitionReplicaInfo(): Map<TopicPartition, DescribeLogDirsResponse.ReplicaInfo> {val globalConf = zkConfigService.getProperties(ZkPathUtils.GLOBAL_CONFIG)
        val adminConfig = Properties()
        adminConfig.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, globalConf.getProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG))
        val adminClient = AdminClient.create(adminConfig)
        val brokerIds = zkConfigService.getChildByPath(kafkaIdsPath)
        return brokerIds.stream()
                .mapToInt {Integer.valueOf(it)
                }.let { intStream ->
                    adminClient.describeLogDirs(intStream.boxed().collect(Collectors.toList()))
                }.let { describeLogDirsResult ->
                    describeLogDirsResult.all()}.let { mapKafkaFutrue ->
                    mapKafkaFutrue.get()}.let { mapStream ->
                    mapStream.values
                }.let {it.stream().map {e -> e.values}.flatMap {e -> e.stream() }.collect(Collectors.toList())
                }.flatMap {it.replicaInfos.entries.toList()
                }.let { it ->
                    it.associateBy({it.key}, {it.value})
                }
    }

代码看起来大差不差. 但 Kotlin 的这些关键字写起来更方便. 我们看下 Java 中 map 函数和 Kotlin 中 let 函数的签名:

     * Returns a stream consisting of the results of applying the given
     * function to the elements of this stream.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param <R> The element type of the new stream
     * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *               <a href="package-summary.html#Statelessness">stateless</a>
     *               function to apply to each element
     * @return the new stream
     */
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 *
 * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let).
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

我们可以看到 Java 中的 map 是被限制在 Stream API 中的, 而 Kotlin 的 let 并没有这种限制.

同时, 我们也可以感受到, 对于函数式编程的支持, 明显是 Kotlin 更好一些——在 Kotlin 中, 我们用一个 () 就可以表示函数, 而 Java 则需要 Interface 来表示(在 Java 中, 对象是一等公民).

如果读者有兴趣的话, 可以尝试一下 HaskellLisp(JVM 上叫 Clojure). 这些都是纯函数式语言.

类似,Kotlin 还有很多这种函数, 被称为作用域函数, 在这里罗列一下常用的函数:

  • let
  • run
  • also
  • apply
  • takeIf
  • takeUnless
  • repeat

5. 小结

在《架构整洁之道》中, 有这么一个总结:

  • 结构化编程是对程序控制权的直接转移的限制
  • 面向对象编程是对程序控制权的间接转移的限制
  • 函数式编程是对程序赋值操作的限制

如果说面向对象编程是对数据进行抽象, 那么函数式编程则是对行为进行抽象.

5.2 函数式编程的三件套:

  1. Map
  2. Reduce
  3. Filter

举个例子, 面包和蔬菜 map 到切碎的操作上, 再 reduce 成汉堡.

我们可以看到 map 和 reduce 不关心输入数据, 它们只控制, 并不是业务. 控制是描述怎么干, 而业务描述要干什么.

在本文中, 我们只看到了 map 的身影——上面提到了,map 对流中的每一个元素进行操作.

可能会有读者问 let 是啥, 在本文的代码例子中,let针对整个流进行操作.

简单来说, Map && Reduce 对应了我们日常中用的 循环 , 而Filter 对应了If

5.3 优势 && 劣势

  • 优势

    1. 无状态
    2. 并发无伤害
    3. 函数执行没有顺序上的问题
  • 劣势

    1. 数据复制严重

5.4 应用场景

  • Python 的装饰器模式
  • 事件溯源: 不记录最终状态, 而是记录每一个事件. 需要时, 通过追溯 (重新计算) 事件来得出当前的状态. 如:

    1. 数据库事务日志
    2. 版本控制器
    3. 比特币

正文完
 0