函数式思维的小例子

简介: 最近写了一个简单的客户端,用来模拟服务化框架的客户端调用,功能如下:随机调用服务打印服务结果10%的几率较少访问量(假设1个并发),10%几率高访问量(假设100个并发),80%几率正常访问量(假设10个并发)打印各个访问量情况下的服务调用总时间分别尝试了Java和Clojure实现,...

最近写了一个简单的客户端,用来模拟服务化框架的客户端调用,功能如下:

  • 随机调用服务
  • 打印服务结果
  • 10%的几率较少访问量(假设1个并发),10%几率高访问量(假设100个并发),80%几率正常访问量(假设10个并发)
  • 打印各个访问量情况下的服务调用总时间

分别尝试了Java和Clojure实现,在实现过程中,两者的思路完全不同!

面向对象/面向过程语言思路

逻辑很简单,基本不涉及面向对象概念,主要还是面向过程语言的思路!

如果使用Java来实现,那么大致的思路是这样的:

  • 首先需要一个随机数生成器,基于这个随机数生成器来构建随机调用逻辑
  • 随机调用服务就是判断随机数大小,例如:0~1的随机数范围,大于0.5访问服务A,否则访问服务B
  • 并发量判定则可以依据0~10的随机数范围,小于等于1时并发为1,大于等于9时并发为100,否则并发为10
  • 在每个服务调用完成后,统计执行时间,然后汇总就可以了

下面是Java实现的代码:

public class RandomCall {

    private static ExecutorService executorService = Executors.newFixedThreadPool(4);

    public static void main(String[] args) throws Exception {
        while (true) {
            int rand = (int)(Math.random() * 10);
            if (rand >= 9) {
                call(100);
            } else if (rand <= 1) {
                call(1);
            } else {
                call(10);
            }
            Thread.sleep(1000L);
        }
    }

    private static void call(int n) throws Exception {
        final AtomicLong total = new AtomicLong(0);
        final CountDownLatch latch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    long start = System.currentTimeMillis();
                    if ((int)(Math.random() * 2) > 1) {
                        System.out.println(callServiceA());
                    } else {
                        System.out.println(callServiceB());
                    }

                    total.addAndGet(System.currentTimeMillis() - start);

                    latch.countDown();
                }
            });
        }

        latch.await();

        System.out.println("Invoke " + n + ":" + total + " ms");
    }
}

代码没什么好说的,对ExecutorService,Executors以及CountDownLatch不熟悉的请自行Google。

函数式语言思路

函数式语言的思路和上面的思路差异很大!

函数式语言通过提供大量的函数来操作少量的数据结构来完成逻辑。

所以其大致思路就是构建相应的数据结构,然后使用提供的操作函数来对其进行操作。

对于“随机调用服务”这个需求,我们可以把它看成是有一个序列,随机排列着需要调用的服务!

;定义一个包含了所调用服务的Vector
(def fns [call-serviceA call-serviceB])
;依据上面的Vector构建一个随机排列的服务序列
(def rand-infinite-fns (repeatedly #(get fns (->> fns count rand int))))

简单解释下最后一行代码:

;;->>是个宏,是为了方便代码阅读
(->> fns count rand int)
;;等价于
(int (rand (count fns)))
;;从最里面那个括号往外读:
;;1 获取fns这个Vector的长度
;;2 以这个长度为随机数范围(0,length)产生随机数
;;3 并取整

#(get fns ...)
;#(...)表示匿名函数,是个语法糖,等价于
(fn [] (get fns ...))

(get fns ...)
;就是依据上面的随机数,从fns这个Vector中获取元素

(repeatedly ...)
;就是字面意思,不停的重复,结果构建成一个LazySeq
;LazySeq就是在需要时才执行

理解了上面的代码,我们是不是可以以同样的逻辑,构建一个随机1、10、100的LazySeq来实现随机并发的逻辑呢?

(defn arr [1 10 100])
(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))

很简单吧?那么问题来了:

  • 目前这个LazySeq是平均概率的分布着1、10、100,和需求不符合
  • 第二行代码和前面的逻辑一模一样,是不是可以重构成函数

第一个问题有思路吗?可以先想想。

其实很简单

(def arr [1 10 10 10 10 10 10 10 10 100])
(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))

这样是不是就符合要求了?

再进一步对第二行代码重构为函数:

(defn rand-infinite [vec]
    (repeatedly #(get vec (->> vec count rand int))))

(def fns [call-serviceA call-serviceB])
(def arr [1 10 10 10 10 10 10 10 10 100])

(def rand-infinite-fns (rand-infinite fns))
(def rand-infinite-arr (rand-infinite arr))
  • rand-infinite-fns中是随机调用的函数
  • rand-infinite-arr中是随机的并发数

现在我们只要从rand-infinite-arr中依次取出元素,然后根据元素的值来构建相同数量的线程来进行调用就可以了!由于是无限序列,所以间接实现了死循环!

举个例子:

;;假设现在rand-infinite-fns中元素如下
[call-serviceA call-serviceA call-serviceB call-serviceA ...]
;;rand-infinite-arr中元素如下
[10 1 10 100 10 10 1 ...]
;;rand-infinite-arr的第一个元素是10,
;;则从rand-infinite-fns中取10个元素,构建10个线程去调用
;;rand-infinite-arr的第二个元素是1,
;;则从rand-infinite-fns的第11个函数开始,去一个函数去调用
;;以此类推

第一印象是递归,Clojure代码实现如下:

(loop [rand-fns (rand-infinite fns)
       nums (rand-infinite arr)]
       ...
       (recur (drop (first nums) rand-fns)
              (drop 1 nums)))

最后就是构建线程进行函数调用

(time (println (pmap #(%) (take (first nums) rand-fns)))
;;pmap就是将#(% obj)这个函数依次应用到后面的序列上,并且是并发的
;;time函数打印出执行所需要的时间
;;(take (first nums) rand-fns)就是依据nums元素的大小,获取相应数量的rand-fns的元素
;;rand-fns中的元素是函数,直接放在括号里的第一个元素就可以执行了,这里是替换了那个%

实际上可以更进一步,上面的流程,相当于遍历下面这个链表:

[[call-serviceA] [call-serviceA call-serviceB ...] [call-serviceB] [call-serviceB call-serviceA ...] ...]

所以只需要构建类似上面的链表结构就可以了,Clojure里很简单:

(let [rand-fns (rand-infinite fns)
       rand-arr (rand-infinite arr)
       group-rand-fns (map #(take % rand-fns) rand-arr)]
       ...))
;;group-rand-fns就是我们需要的链表结构

最后只要遍历这个链表就可以了:

(defn invoke [fns]
    (Thread/sleep 1000)
    (time (println (pmap #(%) fns))))

(defn -main [& args]
    (let [rand-fns (rand-infinite fns)
           rand-arr (rand-infinite arr)
           group-rand-fns (map #(take % rand-fns) rand-arr)]
           (doall (map invoke group-rand-fns))))
;;doall表示立即执行,因为map出来的链表是lazySeq,这里的map相当于外层循环,对每个内部链表应用invoke函数
;;invoke内部是内层循环,每隔1秒就并发调用链表中的函数

完整代码如下:

(defn rand-infinite [vec]
    (repeatedly #(get vec (->> vec count rand int))))

(def fns [call-serviceA call-serviceB])
(def arr [1 10 10 10 10 10 10 10 10 100])

(defn invoke [fns]
    (Thread/sleep 1000)
    (time (println (pmap #(%) fns))))

(defn -main [& args]
    (let [rand-fns (rand-infinite fns)
           rand-arr (rand-infinite arr)
           group-rand-fns (map #(take % rand-fns) rand-arr)]
           (doall (map invoke group-rand-fns))))

Java8实现

Java8提供了lambda表达式等功能,支持函数式编程,下面使用Java8实现,直接贴代码:

public class RandomCall {

    public static void main(String[] args) throws Exception {
        Function<Supplier<String>,Long> func = sup -> {
            long start = System.currentTimeMillis();
            sup.get();
            return System.currentTimeMillis() - start;
        };

        Supplier<String> serviceASup = () -> callServiceA();
        Supplier<String> serviceBSup = () -> callServiceB();

        List<Supplier<String>> fns = Arrays.asList(serviceASup,serviceBSup);
        List<Integer> arr = Arrays.asList(1,10,10,10,10,10,10,10,10,100);

        Stream.generate(() -> (int) (Math.random() * 10)).map(arr::get)
              .forEach(n -> {
                    Thread.sleep(1000L);

                    System.out.println(Stream.generate(() -> (int) (Math.random() * 2)).map(fns::get).limit(n)
                    .parallel().mapToLong(func::apply).sum());
              });
    }
}

总结

  • 最终代码,Clojure明显少于Java、略少于Java8。在代码表现力上Clojure > Java8 > Java
  • 函数式思路和过程式思路差异很大
  • 在编写Clojure代码时,明显是偏脑力的劳动。而在编写Java的时候,明显是偏体力的劳动
  • 编写Clojure代码,如果不多思考,则写出来的代码将比Java要难读得多
  • Java8代码比Clojure代码可读性上感觉更差(可能自己对Java8的函数式思路还不太了解)

目录
相关文章
|
分布式计算 前端开发 JavaScript
程范式解析:面向对象、函数式与声明式编程
程范式解析:面向对象、函数式与声明式编程
137 0
|
5月前
|
设计模式 数据可视化 测试技术
实践中的面向对象的例子
【7月更文挑战第1天】本文介绍面向对象编程注重代码的可理解性、重用和维护。例如,设计一个显示时间、温度等的设备,用户无需关心内部工作,这就是封装;如果需要多个设备,可通过多态创建不同实例;而继承则允许共享通用功能,如所有时钟都继承自计时器基类。
116 0
实践中的面向对象的例子
|
5月前
|
编译器 程序员 C++
【C++高阶】掌握C++多态:探索代码的动态之美
【C++高阶】掌握C++多态:探索代码的动态之美
47 0
|
存储 算法 Python
Python函数编程的艺术:创造简洁优雅的代码
函数是一种重要的编程概念,它可以将一段代码封装起来,实现特定的功能,并且可以被多次调用和复用。函数在Python中具有广泛的应用,可以用于模块化程序、提高代码的可读性和可维护性。本文将引导您从函数的基础知识到高级应用,全面了解Python中函数的使用方法。
123 1
|
7月前
|
人工智能 算法
【算法】深入理解 Prolog:逻辑编程的奇妙世界
【算法】深入理解 Prolog:逻辑编程的奇妙世界
174 0
|
7月前
|
并行计算 数据处理 开发者
Python函数式编程:探索优雅的编程范式
传统的编程范式中,命令式编程和面向对象编程占据主导地位。然而,Python函数式编程作为一种新颖而强大的范式,通过引入函数作为一等公民和不可变性等特性,为开发者提供了更加优雅和灵活的编码方式。本文将深入探讨Python函数式编程的概念与应用,包括高阶函数、纯函数、惰性计算以及函数式编程在并行处理和数据处理方面的实际应用。
|
7月前
|
大数据 开发者
探索编程范式:面向对象与函数式的抉择
在当今快速发展的软件开发领域,面向对象编程(OOP)和函数式编程(FP)是两种重要的编程范式。本文将深入比较这两种范式的特点、应用场景和优劣势,为读者提供选择时的参考,并探讨如何在实际项目中灵活运用它们。
|
7月前
|
Serverless 开发者 Python
Python函数式编程:从概念到应用的完整指南
在 Python 中,函数式编程是一种流行且强大的编程范式,它不仅可以使代码更加简洁、优雅,而且还能提高程序的可读性和可维护性。本文将从基础概念入手,详细讲解 Python 函数式编程的核心思想、常用函数和实际应用。无论你是 Python 新手还是经验丰富的开发者,本文都能为你提供全面的参考和指导。
|
7月前
|
分布式计算 Java API
谈谈代码:函数式编程
一个风和日丽的下午,我看着日常看代码做重构迁移,突然看到这么段代码...
76 1
|
7月前
|
缓存 JavaScript 前端开发
精通JavaScript修饰器:超越传统编程范式的进阶技巧
在JavaScript中,修饰器(Decorator)是一种特殊的语法,用于修改类、方法或属性的行为。修饰器提供了一种简洁而灵活的方式来扩展和定制代码功能。本文将详细介绍JavaScript修饰器的概念、语法和应用场景,并提供相关的代码示例。