zoukankan      html  css  js  c++  java
  • 函数式思维的小例子

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

    • 随机调用服务
    • 打印服务结果
    • 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的函数式思路还不太了解)

  • 相关阅读:
    SQL执行计划之sql_trace
    Pycharm,出现Invalid VCS root mapping The directory 解决方法
    npm安装cnpm时候报错code EINTEGRITY
    Linux 常用命令汇总
    vue 父子组件传值
    vue 钩子函数的使用
    sql 语句中 order by 的用法
    sql查询的常用语句
    vue 甘特图简单制作
    Node.js安装及环境配置
  • 原文地址:https://www.cnblogs.com/ivaneye/p/5824675.html
Copyright © 2011-2022 走看看