函数式思维的小例子

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

  • 随机调用服务
  • 打印服务结果
  • 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的函数式思路还不太了解)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容