函数式思维的小例子

  1. 云栖社区>
  2. 博客>
  3. 正文

函数式思维的小例子

ivaneye 2016-08-31 21:31:00 浏览241
展开阅读全文

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

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

qrcode258.jpg

网友评论

登录后评论
0/500
评论
ivaneye
+ 关注