函數(shù)式思維的小例子

最近寫了一個簡單的客戶端康嘉,用來模擬服務(wù)化框架的客戶端調(diào)用,功能如下:

  • 隨機調(diào)用服務(wù)
  • 打印服務(wù)結(jié)果
  • 10%的幾率較少訪問量(假設(shè)1個并發(fā))财松,10%幾率高訪問量(假設(shè)100個并發(fā))菜秦,80%幾率正常訪問量(假設(shè)10個并發(fā))
  • 打印各個訪問量情況下的服務(wù)調(diào)用總時間

分別嘗試了Java和Clojure實現(xiàn)鲫售,在實現(xiàn)過程中,兩者的思路完全不同衷模!

面向?qū)ο?面向過程語言思路

邏輯很簡單,基本不涉及面向?qū)ο蟾拍钭叽ィ饕€是面向過程語言的思路晦譬!

如果使用Java來實現(xiàn),那么大致的思路是這樣的:

  • 首先需要一個隨機數(shù)生成器互广,基于這個隨機數(shù)生成器來構(gòu)建隨機調(diào)用邏輯
  • 隨機調(diào)用服務(wù)就是判斷隨機數(shù)大小敛腌,例如:0~1的隨機數(shù)范圍,大于0.5訪問服務(wù)A,否則訪問服務(wù)B
  • 并發(fā)量判定則可以依據(jù)0~10的隨機數(shù)范圍惫皱,小于等于1時并發(fā)為1像樊,大于等于9時并發(fā)為100,否則并發(fā)為10
  • 在每個服務(wù)調(diào)用完成后旅敷,統(tǒng)計執(zhí)行時間生棍,然后匯總就可以了

下面是Java實現(xiàn)的代碼:

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媳谁。

函數(shù)式語言思路

函數(shù)式語言的思路和上面的思路差異很大涂滴!

函數(shù)式語言通過提供大量的函數(shù)來操作少量的數(shù)據(jù)結(jié)構(gòu)來完成邏輯。

所以其大致思路就是構(gòu)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)晴音,然后使用提供的操作函數(shù)來對其進行操作柔纵。

對于“隨機調(diào)用服務(wù)”這個需求,我們可以把它看成是有一個序列段多,隨機排列著需要調(diào)用的服務(wù)首量!

;定義一個包含了所調(diào)用服務(wù)的Vector
(def fns [call-serviceA call-serviceB])
;依據(jù)上面的Vector構(gòu)建一個隨機排列的服務(wù)序列
(def rand-infinite-fns (repeatedly #(get fns (->> fns count rand int))))

簡單解釋下最后一行代碼:

;;->>是個宏,是為了方便代碼閱讀
(->> fns count rand int)
;;等價于
(int (rand (count fns)))
;;從最里面那個括號往外讀:
;;1 獲取fns這個Vector的長度
;;2 以這個長度為隨機數(shù)范圍(0,length)產(chǎn)生隨機數(shù)
;;3 并取整

#(get fns ...)
;#(...)表示匿名函數(shù)进苍,是個語法糖加缘,等價于
(fn [] (get fns ...))

(get fns ...)
;就是依據(jù)上面的隨機數(shù),從fns這個Vector中獲取元素

(repeatedly ...)
;就是字面意思觉啊,不停的重復(fù)拣宏,結(jié)果構(gòu)建成一個LazySeq
;LazySeq就是在需要時才執(zhí)行

理解了上面的代碼,我們是不是可以以同樣的邏輯杠人,構(gòu)建一個隨機1勋乾、10、100的LazySeq來實現(xiàn)隨機并發(fā)的邏輯呢嗡善?

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

很簡單吧辑莫?那么問題來了:

  • 目前這個LazySeq是平均概率的分布著1、10罩引、100各吨,和需求不符合
  • 第二行代碼和前面的邏輯一模一樣,是不是可以重構(gòu)成函數(shù)

第一個問題有思路嗎袁铐?可以先想想揭蜒。

其實很簡單

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

這樣是不是就符合要求了横浑?

再進一步對第二行代碼重構(gòu)為函數(shù):

(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中是隨機調(diào)用的函數(shù)
  • rand-infinite-arr中是隨機的并發(fā)數(shù)

現(xiàn)在我們只要從rand-infinite-arr中依次取出元素,然后根據(jù)元素的值來構(gòu)建相同數(shù)量的線程來進行調(diào)用就可以了屉更!由于是無限序列徙融,所以間接實現(xiàn)了死循環(huán)!

舉個例子:

;;假設(shè)現(xiàn)在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個元素瑰谜,構(gòu)建10個線程去調(diào)用
;;rand-infinite-arr的第二個元素是1,
;;則從rand-infinite-fns的第11個函數(shù)開始欺冀,去一個函數(shù)去調(diào)用
;;以此類推

第一印象是遞歸,Clojure代碼實現(xiàn)如下:

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

最后就是構(gòu)建線程進行函數(shù)調(diào)用

(time (println (pmap #(%) (take (first nums) rand-fns)))
;;pmap就是將#(% obj)這個函數(shù)依次應(yīng)用到后面的序列上,并且是并發(fā)的
;;time函數(shù)打印出執(zhí)行所需要的時間
;;(take (first nums) rand-fns)就是依據(jù)nums元素的大小似舵,獲取相應(yīng)數(shù)量的rand-fns的元素
;;rand-fns中的元素是函數(shù)脚猾,直接放在括號里的第一個元素就可以執(zhí)行了,這里是替換了那個%

實際上可以更進一步砚哗,上面的流程龙助,相當(dāng)于遍歷下面這個鏈表:

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

所以只需要構(gòu)建類似上面的鏈表結(jié)構(gòu)就可以了,Clojure里很簡單:

(let [rand-fns (rand-infinite fns)
       rand-arr (rand-infinite arr)
       group-rand-fns (map #(take % rand-fns) rand-arr)]
       ...))
;;group-rand-fns就是我們需要的鏈表結(jié)構(gòu)

最后只要遍歷這個鏈表就可以了:

(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表示立即執(zhí)行蛛芥,因為map出來的鏈表是lazySeq,這里的map相當(dāng)于外層循環(huán)提鸟,對每個內(nèi)部鏈表應(yīng)用invoke函數(shù)
;;invoke內(nèi)部是內(nèi)層循環(huán),每隔1秒就并發(fā)調(diào)用鏈表中的函數(shù)

完整代碼如下:

(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實現(xiàn)

Java8提供了lambda表達式等功能仅淑,支持函數(shù)式編程称勋,下面使用Java8實現(xiàn),直接貼代碼:

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());
              });
    }
}

總結(jié)

  • 最終代碼涯竟,Clojure明顯少于Java赡鲜、略少于Java8。在代碼表現(xiàn)力上Clojure > Java8 > Java
  • 函數(shù)式思路和過程式思路差異很大
  • 在編寫Clojure代碼時庐船,明顯是偏腦力的勞動银酬。而在編寫Java的時候,明顯是偏體力的勞動
  • 編寫Clojure代碼筐钟,如果不多思考揩瞪,則寫出來的代碼將比Java要難讀得多
  • Java8代碼比Clojure代碼可讀性上感覺更差(可能自己對Java8的函數(shù)式思路還不太了解)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市篓冲,隨后出現(xiàn)的幾起案子李破,更是在濱河造成了極大的恐慌,老刑警劉巖壹将,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗤攻,死亡現(xiàn)場離奇詭異,居然都是意外死亡诽俯,警方通過查閱死者的電腦和手機屯曹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恶耽,你說我怎么就攤上這事⊙掌簦” “怎么了偷俭?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缰盏。 經(jīng)常有香客問我涌萤,道長,這世上最難降的妖魔是什么口猜? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任负溪,我火速辦了婚禮,結(jié)果婚禮上济炎,老公的妹妹穿的比我還像新娘川抡。我一直安慰自己,他們只是感情好须尚,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布崖堤。 她就那樣靜靜地躺著,像睡著了一般耐床。 火紅的嫁衣襯著肌膚如雪密幔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天撩轰,我揣著相機與錄音胯甩,去河邊找鬼。 笑死堪嫂,一個胖子當(dāng)著我的面吹牛偎箫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溉苛,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼镜廉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了愚战?” 一聲冷哼從身側(cè)響起娇唯,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寂玲,沒想到半個月后塔插,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拓哟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年想许,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡流纹,死狀恐怖糜烹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漱凝,我是刑警寧澤疮蹦,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站茸炒,受9級特大地震影響愕乎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜壁公,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一感论、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧紊册,春花似錦比肄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至关斜,卻和暖如春示括,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痢畜。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工垛膝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丁稀。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓吼拥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親线衫。 傳聞我的和親對象是個殘疾皇子凿可,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容