==[思維]函數(shù)式思維(描述做什么/而不是怎么做,非常簡(jiǎn)練)

什么是函數(shù)式編程思維勿她? - 面向?qū)ο缶幊?- 知乎
https://www.zhihu.com/question/28292740

//摘要
從這個(gè)例子可以看出挤渐,函數(shù)式程序非常簡(jiǎn)練,描述做什么院领,而不是怎么做。

函數(shù)式編程與命令式編程最大的不同其實(shí)在于:函數(shù)式編程關(guān)心數(shù)據(jù)的映射够吩,命令式編程關(guān)心解決問題的步驟

//
一般來說比然,遞歸這種方式于循環(huán)相比被認(rèn)為是更符合人的思維的,即告訴機(jī)器做什么周循,而不是告訴機(jī)器怎么做强法。遞歸還是有很強(qiáng)大的表現(xiàn)力的,比如換零錢問題湾笛。

從這個(gè)例子可以看出饮怯,函數(shù)式程序非常簡(jiǎn)練,描述做什么嚎研,而不是怎么做蓖墅。

函數(shù)式語言當(dāng)然還少不了以下特性:高階函數(shù)(Higher-order function)
偏應(yīng)用函數(shù)(Partially Applied Functions)
柯里化(Currying)
閉包(Closure)

高階函數(shù)就是參數(shù)為函數(shù)或返回值為函數(shù)的函數(shù)。有了高階函數(shù),就可以將復(fù)用的粒度降低到函數(shù)級(jí)別论矾,相對(duì)于面向?qū)ο笳Z言教翩,復(fù)用的粒度更低。
高階函數(shù)提供了一種函數(shù)級(jí)別上的依賴注入(或反轉(zhuǎn)控制)機(jī)制贪壳,在上面的例子里饱亿,sum函數(shù)的邏輯依賴于注入進(jìn)來的函數(shù)的邏輯。很多GoF設(shè)計(jì)模式都可以用高階函數(shù)來實(shí)現(xiàn)闰靴,如Visitor彪笼,Strategy,Decorator等蚂且。比如Visitor模式就可以用集合類的map()或foreach()高階函數(shù)來替代配猫。

總結(jié)函數(shù)式編程是給軟件開發(fā)者提供的另一套工具箱,為我們提供了另外一種抽象和思考的方式膘掰。函數(shù)式編程也有不太擅長的場(chǎng)合章姓,比如處理可變狀態(tài)和處理IO,要么引入可變變量识埋,要么通過Monad來進(jìn)行封裝(如State Monad和IO Monad)

//
函數(shù)式編程與命令式編程最大的不同其實(shí)在于:函數(shù)式編程關(guān)心數(shù)據(jù)的映射凡伊,命令式編程關(guān)心解決問題的步驟這里的映射就是數(shù)學(xué)上“函數(shù)”的概念——一種東西和另一種東西之間的對(duì)應(yīng)關(guān)系。這也是為什么“函數(shù)式編程”叫做“函數(shù)式編程”窒舟。

這就是命令式編程——你要做什么事情系忙,你得把達(dá)到目的的步驟詳細(xì)的描述出來,然后交給機(jī)器去運(yùn)行惠豺。這也正是命令式編程的理論模型——圖靈機(jī)的特點(diǎn)银还。一條寫滿數(shù)據(jù)的紙帶,一條根據(jù)紙帶內(nèi)容運(yùn)動(dòng)的機(jī)器洁墙,機(jī)器每動(dòng)一步都需要紙帶上寫著如何達(dá)到蛹疯。那么,不用這種方式热监,如何翻轉(zhuǎn)二叉樹呢捺弦?函數(shù)式思維提供了另一種思維的途徑——所謂“翻轉(zhuǎn)二叉樹”,可以看做是要得到一顆和原來二叉樹對(duì)稱的新二叉樹孝扛。這顆新二叉樹的特點(diǎn)是每一個(gè)節(jié)點(diǎn)都遞歸地和原樹相反列吼。用 haskell 代碼表達(dá)出來就是:data Tree a = Node a (Maybe (Tree a)) (Maybe (Tree a)) deriving (Show, Eq)invert :: Maybe (Tree a) -> Maybe (Tree a)invert Nothing = Nothinginvert (Just Node v l r) = Just (Node v (invert r) (invert l))

(防止看不懂,翻譯成等價(jià)的 python )def invert(node): if node is None: return None else return Tree(node.value, invert(node.right), invert(node.left))

這段代碼體現(xiàn)的思維苦始,就是舊樹到新樹的映射——對(duì)一顆二叉樹而言寞钥,它的鏡像樹就是左右節(jié)點(diǎn)遞歸鏡像的樹。這段代碼最終達(dá)到的目的同樣是翻轉(zhuǎn)二叉樹陌选,但是它得到結(jié)果的方式和 python 代碼有著本質(zhì)的差別:通過描述一個(gè) 舊樹->新樹 的映射理郑,而不是描述“從舊樹得到新樹應(yīng)該怎樣做”來達(dá)到目的蹄溉。那么這樣有什么好處呢?首先香浩,最直觀的角度來說类缤,函數(shù)式風(fēng)格的代碼可以寫得很精簡(jiǎn),大大減少了鍵盤的損耗(其次邻吭,函數(shù)式的代碼是“對(duì)映射的描述”餐弱,它不僅可以描述二叉樹這樣的數(shù)據(jù)結(jié)構(gòu)之間的對(duì)應(yīng)關(guān)系,任何能在計(jì)算機(jī)中體現(xiàn)的東西之間的對(duì)應(yīng)關(guān)系都可以描述——比如函數(shù)和函數(shù)之間的映射(比如 functor**)囱晴;比如外部操作到 GUI 之間的映射(就是現(xiàn)在前端熱炒的所謂 FRP)膏蚓。它的抽象程度可以很高,這就意味著函數(shù)式的代碼可以更方便的復(fù)用畸写。另外還有其他答主提到的驮瞧,可以方便的并行。同時(shí)枯芬,將代碼寫成這種樣子可以方便用數(shù)學(xué)的方法進(jìn)行研究(這就是為什么可以扯上范疇上的這種數(shù)學(xué)上的高深概念)
【至于什么科里化论笔、什么數(shù)據(jù)不可變,都只是外延體現(xiàn)而已】千所。


函數(shù)式思維的小例子 - IvanEye - 博客園
http://www.cnblogs.com/ivaneye/p/5824675.html

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

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


最近寫了一個(gè)簡(jiǎn)單的客戶端最楷,用來模擬服務(wù)化框架的客戶端調(diào)用,功能如下:
隨機(jī)調(diào)用服務(wù)
打印服務(wù)結(jié)果
10%的幾率較少訪問量(假設(shè)1個(gè)并發(fā))待错,10%幾率高訪問量(假設(shè)100個(gè)并發(fā))籽孙,80%幾率正常訪問量(假設(shè)10個(gè)并發(fā))
打印各個(gè)訪問量情況下的服務(wù)調(diào)用總時(shí)間

分別嘗試了Java和Clojure實(shí)現(xiàn),在實(shí)現(xiàn)過程中火俄,兩者的思路完全不同犯建!
面向?qū)ο?面向過程語言思路
邏輯很簡(jiǎn)單,基本不涉及面向?qū)ο蟾拍罟峡停饕€是面向過程語言的思路胎挎!
如果使用Java來實(shí)現(xiàn),那么大致的思路是這樣的:
首先需要一個(gè)隨機(jī)數(shù)生成器忆家,基于這個(gè)隨機(jī)數(shù)生成器來構(gòu)建隨機(jī)調(diào)用邏輯
隨機(jī)調(diào)用服務(wù)就是判斷隨機(jī)數(shù)大小,例如:0~1的隨機(jī)數(shù)范圍德迹,大于0.5訪問服務(wù)A,否則訪問服務(wù)B
并發(fā)量判定則可以依據(jù)0~10的隨機(jī)數(shù)范圍芽卿,小于等于1時(shí)并發(fā)為1,大于等于9時(shí)并發(fā)為100胳搞,否則并發(fā)為10
在每個(gè)服務(wù)調(diào)用完成后卸例,統(tǒng)計(jì)執(zhí)行時(shí)間称杨,然后匯總就可以了

下面是Java實(shí)現(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"); }}

代碼沒什么好說的,對(duì)ExecutorService,Executors以及CountDownLatch不熟悉的請(qǐng)自行Google筷转。
函數(shù)式語言思路
函數(shù)式語言的思路和上面的思路差異很大姑原!
函數(shù)式語言通過提供大量的函數(shù)來操作少量的數(shù)據(jù)結(jié)構(gòu)來完成邏輯。
所以其大致思路就是構(gòu)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)呜舒,然后使用提供的操作函數(shù)來對(duì)其進(jìn)行操作锭汛。
對(duì)于“隨機(jī)調(diào)用服務(wù)”這個(gè)需求,我們可以把它看成是有一個(gè)序列袭蝗,隨機(jī)排列著需要調(diào)用的服務(wù)唤殴!
;定義一個(gè)包含了所調(diào)用服務(wù)的Vector(def fns [call-serviceA call-serviceB]);依據(jù)上面的Vector構(gòu)建一個(gè)隨機(jī)排列的服務(wù)序列(def rand-infinite-fns (repeatedly #(get fns (->> fns count rand int))))

簡(jiǎn)單解釋下最后一行代碼:
;;->>是個(gè)宏,是為了方便代碼閱讀(->> fns count rand int);;等價(jià)于(int (rand (count fns)));;從最里面那個(gè)括號(hào)往外讀:;;1 獲取fns這個(gè)Vector的長度;;2 以這個(gè)長度為隨機(jī)數(shù)范圍(0,length)產(chǎn)生隨機(jī)數(shù);;3 并取整#(get fns ...);#(...)表示匿名函數(shù)到腥,是個(gè)語法糖朵逝,等價(jià)于(fn [] (get fns ...))(get fns ...);就是依據(jù)上面的隨機(jī)數(shù),從fns這個(gè)Vector中獲取元素(repeatedly ...);就是字面意思乡范,不停的重復(fù)配名,結(jié)果構(gòu)建成一個(gè)LazySeq;LazySeq就是在需要時(shí)才執(zhí)行

理解了上面的代碼,我們是不是可以以同樣的邏輯晋辆,構(gòu)建一個(gè)隨機(jī)1渠脉、10、100的LazySeq來實(shí)現(xiàn)隨機(jī)并發(fā)的邏輯呢栈拖?
(defn arr [1 10 100])(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))

很簡(jiǎn)單吧连舍?那么問題來了:
目前這個(gè)LazySeq是平均概率的分布著1、10涩哟、100索赏,和需求不符合
第二行代碼和前面的邏輯一模一樣,是不是可以重構(gòu)成函數(shù)

第一個(gè)問題有思路嗎贴彼?可以先想想潜腻。
其實(shí)很簡(jiǎn)單
(def arr [1 10 10 10 10 10 10 10 10 100])(def rand-infinite-arr (repeatedly #(get arr (->> arr count rand int))))

這樣是不是就符合要求了?
再進(jìn)一步對(duì)第二行代碼重構(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中是隨機(jī)調(diào)用的函數(shù)
rand-infinite-arr中是隨機(jī)的并發(fā)數(shù)

現(xiàn)在我們只要從rand-infinite-arr中依次取出元素器仗,然后根據(jù)元素的值來構(gòu)建相同數(shù)量的線程來進(jìn)行調(diào)用就可以了融涣!由于是無限序列,所以間接實(shí)現(xiàn)了死循環(huán)精钮!
舉個(gè)例子:
;;假設(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的第一個(gè)元素是10,;;則從rand-infinite-fns中取10個(gè)元素威鹿,構(gòu)建10個(gè)線程去調(diào)用;;rand-infinite-arr的第二個(gè)元素是1,;;則從rand-infinite-fns的第11個(gè)函數(shù)開始,去一個(gè)函數(shù)去調(diào)用;;以此類推

第一印象是遞歸,Clojure代碼實(shí)現(xiàn)如下:
(loop [rand-fns (rand-infinite fns) nums (rand-infinite arr)] ... (recur (drop (first nums) rand-fns) (drop 1 nums)))

最后就是構(gòu)建線程進(jìn)行函數(shù)調(diào)用
(time (println (pmap #(%) (take (first nums) rand-fns)));;pmap就是將#(% obj)這個(gè)函數(shù)依次應(yīng)用到后面的序列上轨香,并且是并發(fā)的;;time函數(shù)打印出執(zhí)行所需要的時(shí)間;;(take (first nums) rand-fns)就是依據(jù)nums元素的大小忽你,獲取相應(yīng)數(shù)量的rand-fns的元素;;rand-fns中的元素是函數(shù),直接放在括號(hào)里的第一個(gè)元素就可以執(zhí)行了臂容,這里是替換了那個(gè)%

實(shí)際上可以更進(jìn)一步科雳,上面的流程根蟹,相當(dāng)于遍歷下面這個(gè)鏈表:
[[call-serviceA] [call-serviceA call-serviceB ...] [call-serviceB] [call-serviceB call-serviceA ...] ...]

所以只需要構(gòu)建類似上面的鏈表結(jié)構(gòu)就可以了,Clojure里很簡(jiǎn)單:
(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)

最后只要遍歷這個(gè)鏈表就可以了:
(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í)行糟秘,因?yàn)閙ap出來的鏈表是lazySeq,這里的map相當(dāng)于外層循環(huán)简逮,對(duì)每個(gè)內(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實(shí)現(xiàn)
Java8提供了lambda表達(dá)式等功能尿赚,支持函數(shù)式編程散庶,下面使用Java8實(shí)現(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代碼時(shí)泻蚊,明顯是偏腦力的勞動(dòng)躲舌。而在編寫Java的時(shí)候,明顯是偏體力的勞動(dòng)
編寫Clojure代碼性雄,如果不多思考没卸,則寫出來的代碼將比Java要難讀得多
Java8代碼比Clojure代碼可讀性上感覺更差(可能自己對(duì)Java8的函數(shù)式思路還不太了解)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市秒旋,隨后出現(xiàn)的幾起案子约计,更是在濱河造成了極大的恐慌,老刑警劉巖迁筛,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件煤蚌,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡细卧,警方通過查閱死者的電腦和手機(jī)尉桩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贪庙,“玉大人蜘犁,你說我怎么就攤上這事≈褂剩” “怎么了这橙?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長导披。 經(jīng)常有香客問我屈扎,道長,這世上最難降的妖魔是什么撩匕? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任助隧,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘并村。我一直安慰自己,他們只是感情好滓技,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布哩牍。 她就那樣靜靜地躺著,像睡著了一般令漂。 火紅的嫁衣襯著肌膚如雪膝昆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天叠必,我揣著相機(jī)與錄音荚孵,去河邊找鬼。 笑死纬朝,一個(gè)胖子當(dāng)著我的面吹牛收叶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播共苛,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼判没,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了隅茎?” 一聲冷哼從身側(cè)響起澄峰,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辟犀,沒想到半個(gè)月后俏竞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡堂竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年魂毁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跃捣。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漱牵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疚漆,到底是詐尸還是另有隱情酣胀,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布娶聘,位于F島的核電站闻镶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丸升。R本人自食惡果不足惜铆农,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狡耻。 院中可真熱鬧墩剖,春花似錦猴凹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至爷绘,卻和暖如春书劝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背土至。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工购对, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陶因。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓骡苞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坑赡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子烙如,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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