函数式思维的小例子
最近写了一个简单的客户端,用来模拟服务化框架的客户端调用,功能如下:
- 随机调用服务
- 打印服务结果
- 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的函数式思路还不太了解)
公众号:一瑜一琂