==[思维]函数式思维(描述做什么/而不是怎么做,非常简练)

什么是函数式编程思维? - 面向对象编程 - 知乎
https://www.zhihu.com/question/28292740

//摘要
从这个例子可以看出,函数式程序非常简练,描述做什么,而不是怎么做。

函数式编程与命令式编程最大的不同其实在于:函数式编程关心数据的映射,命令式编程关心解决问题的步骤

//
一般来说,递归这种方式于循环相比被认为是更符合人的思维的,即告诉机器做什么,而不是告诉机器怎么做。递归还是有很强大的表现力的,比如换零钱问题。

从这个例子可以看出,函数式程序非常简练,描述做什么,而不是怎么做。

函数式语言当然还少不了以下特性:高阶函数(Higher-order function)
偏应用函数(Partially Applied Functions)
柯里化(Currying)
闭包(Closure)

高阶函数就是参数为函数或返回值为函数的函数。有了高阶函数,就可以将复用的粒度降低到函数级别,相对于面向对象语言,复用的粒度更低。
高阶函数提供了一种函数级别上的依赖注入(或反转控制)机制,在上面的例子里,sum函数的逻辑依赖于注入进来的函数的逻辑。很多GoF设计模式都可以用高阶函数来实现,如Visitor,Strategy,Decorator等。比如Visitor模式就可以用集合类的map()或foreach()高阶函数来替代。

总结函数式编程是给软件开发者提供的另一套工具箱,为我们提供了另外一种抽象和思考的方式。函数式编程也有不太擅长的场合,比如处理可变状态和处理IO,要么引入可变变量,要么通过Monad来进行封装(如State Monad和IO Monad)

//
函数式编程与命令式编程最大的不同其实在于:函数式编程关心数据的映射,命令式编程关心解决问题的步骤这里的映射就是数学上“函数”的概念——一种东西和另一种东西之间的对应关系。这也是为什么“函数式编程”叫做“函数式编程”。

这就是命令式编程——你要做什么事情,你得把达到目的的步骤详细的描述出来,然后交给机器去运行。这也正是命令式编程的理论模型——图灵机的特点。一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。那么,不用这种方式,如何翻转二叉树呢?函数式思维提供了另一种思维的途径——所谓“翻转二叉树”,可以看做是要得到一颗和原来二叉树对称的新二叉树。这颗新二叉树的特点是每一个节点都递归地和原树相反。用 haskell 代码表达出来就是: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))

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

这段代码体现的思维,就是旧树到新树的映射——对一颗二叉树而言,它的镜像树就是左右节点递归镜像的树。这段代码最终达到的目的同样是翻转二叉树,但是它得到结果的方式和 python 代码有着本质的差别:通过描述一个 旧树->新树 的映射,而不是描述“从旧树得到新树应该怎样做”来达到目的。那么这样有什么好处呢?首先,最直观的角度来说,函数式风格的代码可以写得很精简,大大减少了键盘的损耗(其次,函数式的代码是“对映射的描述”,它不仅可以描述二叉树这样的数据结构之间的对应关系,任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射(比如 functor**);比如外部操作到 GUI 之间的映射(就是现在前端热炒的所谓 FRP)。它的抽象程度可以很高,这就意味着函数式的代码可以更方便的复用。另外还有其他答主提到的,可以方便的并行。同时,将代码写成这种样子可以方便用数学的方法进行研究(这就是为什么可以扯上范畴上的这种数学上的高深概念)
【至于什么科里化、什么数据不可变,都只是外延体现而已】。


函数式思维的小例子 - IvanEye - 博客园
http://www.cnblogs.com/ivaneye/p/5824675.html

函数式语言通过提供大量的函数来操作少量的数据结构来完成逻辑。

所以其大致思路就是构建相应的数据结构,然后使用提供的操作函数来对其进行操作。


最近写了一个简单的客户端,用来模拟服务化框架的客户端调用,功能如下:
随机调用服务
打印服务结果
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阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容