函數式編程很難,所以你要學習它

2020-12-01 開源中國

本文是從 Functional Programming Is Hard,That's Why It's Good 這篇文章翻譯而來。


很 奇怪不是,很少有人每天都使用函數式程式語言。如果你用Scala,Haskell,Erlang,F#或某個Lisp方言來編程,很可能沒有公司會花錢 聘你。這個行業裡的絕大部分人都是使用像Python,Ruby,Java或C#等面向對象的程式語言——它們用起來很順手。不錯,你也許會偶然用到一兩 個「函數式語言特徵」,例如「block」,但人們不會去做函數式編程。

然而,很多年來,我們一直被教導說函數式程式語言很好很棒。我仍然記得當我第一次閱讀ESR的著名的關於學習Lisp語言的論文時的困惑。也許大多數的人對Paul Graham 的《Beating The Averages》這篇文章更加熟悉:

使用Lisp開發使我們的開發周期迭代的如此之快,以至於有時當競爭對手在新聞發布會上推出他們的新功能一兩天後,我們就能複製出同樣的功能。當報導產品發布的新聞記者打電話給我們時,我們的產品已經擁有了同樣的功能特徵。

那些皈依函數式編程的人中,一直常見的考慮是:學習這種新的、函數式的語言「對你有好處」;就像是某些人建議說每天30分鐘的健身房活動會「讓你的 身體健康」一樣。但這也同時暗示了這樣做的難度和需要的付出。Lisp語言跟Haskell、Ocaml和Scala語言不同,被認為是出了名的難學,可 以說是臭名昭著。文雅的人說這是Lisp語言的「深度&廣度」的體現。不文雅的人說這是「意淫」或「玩弄學術」或簡單的「沒必要」。我認為,它的 難度跟你對它熟不熟悉有關,而且,這種難度是一種重要指標顯示:學習這樣的一種語言會讓你編程更有效率、能力更強。

它給你的初次印象不友善

我7歲時就開始編程,在漫長無聊的郊區夏季裡,在我祖父的計算機上瞎搞一氣。我學了BASIC,用它在屏幕上畫一個蹦跳的球。我學了Pascal, 用它寫了一個能通過PC喇叭放音樂的程序。大概10歲時我學了C語言,但遇到了一堵越不過去的牆,直到我上了高中。那就是:指針。即使不算這些該死的指 針,我寫、讀、學習、練習中,同樣遭遇無數的失敗。我把祖父的硬碟給毀掉了兩次(一次屬意外),最後弄得不少次要自己重裝作業系統。我失敗,一遍遍的失 敗。

也許你也有跟我相似的故事,也許是完全不同的一個。但我想,差不多所有學過編程的人都有過遇到困難的經歷。我們在學了一些基本知識後,必然會遇到一 些公認的概念上的關口,比如「指針」。很多計算機科學教授會把指針描述為他們課程上的過濾網。如果你想成為一名優秀的程式設計師,你必須要能理解指針。很少人 能輕鬆的掌握它們。大多數人,包括我,則需要不斷的練習和參考例子來理解什麼是指針、為什麼它們很重要。

這種艱難的努力過程不是偶然的,是一種幾乎普遍的現象。指針是一種非常強大和基礎功能的概念。學會它能讓你成為一名更好的程式設計師,能讓你的思考更加形象化。即使你使用的語言並不提供指針這樣的特徵,但跟指針類似的數據結構和概念卻隨處可見。

新奇事物

一旦你學會了幾種語言後,所有的語言都開始看起來都很相似。知道Python的人學習Ruby可能不會遇到太多的問題,知道Java的人學習C#會 感到很熟悉。不錯,也有意外的地方。Ruby愛好者在學習Python時會對它的comprehension感到吃驚,Java用戶會對C#裡的委派摸不著頭腦。還是那句話,如果你只瞟一眼,它們都很相似。我可以打保票的說,如果你還不曾有過這樣的認識,一旦你學了一種Lisp語言,你會發現所有的Lisp變種都很相似。

有人說,大部分人第一次使用Haskell或Ocaml時都完全的不知所措。見鬼了,在Haskell裡,連分號都跟別人不一樣。這並不是語法的問題;Haskell和ML語言完全基於一種不同的概念、一種新的語言範式。你需要用不同的方式開發應用,不同的方式組織應用,不同的方式擴展應用。

很多這樣的新概念都具有不可思議的強大力量。Haskell裡的Monads 是跟指針一樣基礎且強大的概念(你很可能在不知道它叫什麼的情況下就已經使用過它們了)。所以,跟學了Java後再學C#不一樣,有志向學習函數式語言的 人需要往回走的更遠,去學習更加基礎的概念後才能接下去學習。就像是完全再學習一次指針。並且,就像是當年我們剛開始學習編程一樣,一些很大的概念看起來 會讓人迷惑茫然,讓人沮喪,直到你去攻克(以及失敗)它們。

吃下你的藥丸,找到你的藥劑師

儘管不好學,但我堅信,學習這些函數式程式語言會在職業上對你有好處。我相信有些人讀到這點時會眼睛翻起來向天看,很難想像出這些monoids 或 monad 會對他們在使用Java或C#時有用處。對我而言,我已經不驚奇於由於這樣的思維而阻止他們學習函數式語言的現象;他們需要學習一種跟指針和遞歸一樣基礎 的新概念。他們需要有一種只有專業人員在完成清晰的商業目標時才具有的耐心和鬥志。很少人能在過了可塑的年齡後還受得了挫折——一次又一次的挫折——否則 我們現在都早成專家了,不是嗎?

還有更複雜的東西,有大量的語言和算法研究都是用函數式語言實施的(尤其是Haskell)。你很容易會被這些不熟悉的概念——例如分類學理論, half-finished abstractions,一些失敗的研究——弄的迷失方向。沒有一個清晰的指導(比如由一個實用主義的作者寫的一本好書),本來已經很困難的學習任務變的更加可怕。

這些疊加起來的複雜因素導致了不出意外的結果:很多人不情願在函數式編程學習中投入時間。很容易理解這種不情願,「我幹嘛不把花在學習這些東西的時 間用在實現什麼東西上呢?」但這種思路也表明了你永遠不願意在任何新技術上浪費時間(只用自己熟悉的)。在一個像軟體技術這樣日新月異的產業裡,我不認為 這是正確的判斷。

眼見為實

學習一種函數式程式語言最顯而易見的好處是,你能學會這種類型語言中的函數式概念。它能幫助你的大腦,讓它具有能非常清晰的思考和處理一些驚人的重 大概念的能力。這並不是函數式編程具有魔法;各種語言和範式的出現都是為了應對某一特定類別的問題。函數式編程的殺手鐧正是應對了當今世界上日益增長的並 行性編程和元數據編程趨勢。

例如,我們研究一個簡化的、本地版本化的Google著名的MapReduce範例。用函數式方式描述這種範例是不可思議的清晰簡潔:

mapReducer data partitioner mapper reducer = let partitions = partitioner data in reduce reducer (map mapper partitions)

讓這樣的代碼支持並行計算或分布式並行計算是輕而易舉的(對於本地並行計算,很多的功能包都支持「pmap」和「preduce「——只需要利用函 數式語言的一些簡單特性)。像maps, partitions, generators, streams, reductions, folds, 已以及 function chaining等概念在各種的函數式程式語言中都大同小異,所以,任何對Lisp,Haskell,OCaml,甚至帶點函數式語言特徵的語言—— Python和Ruby熟悉的人,都會很容易的理解這裡面的思想精華。

讓我們花點時間考慮一下,如何用一種面向對象的語言,以一種常見的面向對象的模式來清楚的描述這種架構。至少你需要做的事情是定義用來描述 mapper和reducer的聲明。如果你有好奇心,請試著用你喜歡的面向對象語言描述一個最小化的「面向對象」的MapReduce。我發現那是非常 羅嗦的。如果使用Java風格的語言,它會像這樣:

interface Mapper { B map(A input); } interface Reducer { Y reduce(X a, X b); } abstract class MapReduce { private Mapper mapper; private Reducer reducer; public MapReduce(Mapper map, Reducer reduce) { // ... } public run(SeqenceType data) { // ... } }

即使是沒有加入循環邏輯,這種缺乏函數式模式中常見的名詞和動詞的使用,使得MapReduce這種技術很難被定義。這種定義方式幾乎是滑稽可笑的,但它能讓你想到函數式概念。另外一個好例子是Scala語言如何利用完備的Java Fork/Join 類庫,把它輕鬆的集成的自己的自有語法中。

各有所求

所以,我鼓勵任何想進步的程式設計師:請考慮學習一種函數式語言。Haskell和OCaml都是極好的選擇,F#和Erlang也相當的不錯。它們都 不好學,但也許這是個好事。努力弄清楚你遇到的複雜的概念,看看是否有其他人正在利用這些概念;經常的,你會在尋找這些不熟悉的概念的真正用意的時候實現 思想上的突破。

當你開始學的時候,請注意,不要過於在意。就像其他任何需要你花時間和精力的事情一樣,過度的在函數式編程上進行精力上的投資是很危險的。掉進了認知能力的陷阱後你的投資會血本無歸。你很容易會忘掉世界上還有無數種計算模型,你更容易忘掉有多少種優秀的軟體根本沒有使用任何的函數式概念。

學習的道路會越來越難走,但從另一方面說,在你日常的編程中,你會發現有越來越多的可以使用的重要概念和模型。對於這樣緊湊的編程風格你會越來越適應,必然,你也會對如何成為一名更好的軟體工程師有了新的認識。

補充

有不少校對這篇文章的人在看完文章後都問了我一個同樣的問題:「聽起來不錯,大衛,可是我應該學習那種語言呢?」當然,這是他們給我出的難題。

我想,如果你是一個很有經驗的程式設計師,這最能「應付」這個問題的答案是:「選一種符合你的需求的」。如果你需要在JVM上工作,選擇Scala或 Clojure。如果你想能快速的開發大型分布式軟體系統,選擇Erlang。如果你想要一種具有超強編譯器的超能幹活的語言,請選擇Haskell或 RCaml。如果你想要一種比Ruby或Python更有能力的原型工具,選擇Scheme。

請記住,我們在這裡要做的這些目的是為了實際的技能和自我進步。如果你能騰出時間學這些,就走出你的安逸環境,挑戰自己。

因為我已經學習了Lisp和Erlang,而且使用OCaml做專業工作,我決定研究一下Haskell,這完全是另外一個世界。我發現唯一能幫助我參透這種語言的途徑是依賴Learn You A Haskell 和 Real World Haskell 這兩本有用的指導材料。這些書寫的非常好,很有價值,而且可以免費在網上找到。如果你想試一下Haskell,這些書可以當作你的尋寶圖。

相關焦點

  • go 學習筆記之學習函數式編程前不要忘了函數基礎
    標準的函數式編程具有濃厚的數學色彩,幸運的是,Go 並不是函數式語言,所以也不必受限於近乎苛責般的條條框框.簡單來說,函數式編程具有以下特點:不可變性: 不用狀態變量和可變對象函數只能有一個參數純函數沒有副作用摘自維基百科中關於函數式編程中有這麼一段話:
  • 反對函數式編程的政治正確
    關於它的好處我們已經聽到太多了:結果可預期、利於測試、利於復用、利於並發……一切聽起來都這麼理想,所以我們能夠自底到上地用純函數來編寫出一個完整的系統了嗎?這裡的荒誕之處在於,當你越接近一臺計算機 under the hood 的原貌時,你離純函數與無狀態越遠。
  • 寫Python 代碼不可不知的函數式編程技術
    選自 Medium作者:Raivat Shah參與:魔王、Jamin本文對 Python 中的函數式編程技術進行了簡單的入門介紹。近來,越來越多人使用函數式編程(functional programming)。因此,很多傳統的命令式語言(如 Java 和 Python)開始支持函數式編程技術。
  • 「深度學習被可微分編程取代?」展望30年後的神經網絡和函數編程
    他從深度學習三大觀點之一的表示(representation)角度出發,認為深度學習研究的是優化和函數編程之間的聯繫,而可微分編程則是函數編程和優化的自然交集,十分優雅而簡潔,值得進一步研究。目前,深度學習是一個非常成功的工具。但這個工具是我們偶然發現的,作為一個領域,還沒有統一的看法或共同的理解。事實上,這個領域還存在幾種相互競爭的表述!
  • 可微分式編程:深度學習發展的新趨勢?
    以長期以來被人工智慧純化論者所鄙視的途徑——從海量數據中學習概率的方式——解決了。從此你無需把問題編碼成可運行的格式,更不依賴人自身解決問題的技巧——從目標分類和語音識別到圖片標註和合成特定藝術家風格的圖像,甚至指導機器人執行尚未被編程的任務,都被一一解決。這個佔主導地位的新進展最初被冠以「神經網絡」之名,如今被稱作「深度學習」——用來定性地強調相比以前的長足進展。
  • Python基礎教程(一) - 函數和函數式編程
    什麼是函數函數是對程序邏輯進行結構化或過程化的一種編程方法。能將整塊代碼巧妙地隔離成易於管理的小塊,把重複的代碼放在函數中而不是進行大量的copy。前面使用過很多print()來進行列印,這是python提供的內建函數,你也可以自己創建函數,這叫做用戶自定義函數。創建函數你可以定義一個由自己想要功能的函數,用def語句來創建,標題行由def關鍵字,函數的名字,以及參數的集合(如果有的話)組成。
  • 如何從零學習一門程式語言,並由此拿到高薪?
    十一、編程模式比較流行的編程模式大概有:面向對象編程,主要是封裝、繼承、多態;函數式編程,主要是應用Lambda;過程式編程,可以理解為實現需求功能的特定步驟。十六、回調機制每種語言實現回調的方式有所不同,如.Net的delegate (大量被用於WinForm程序);Javascript中函數天然支持回調:Javascript函數允許傳入另一個函數作為入參,然後在方法中調用它。其它語言的回調方式不一一列舉。
  • 函數式語言庫模式:框架是魔鬼?
    框架定義了一個結構,你不得不將其填充好;而庫則需要你圍繞其提供的結構進行編碼。為什麼不建議使用框架?框架最大最顯著的弱點是不可組合。如果你正在使用兩個框架,這兩者之間往往是很難兼容的;誰包含誰,誰是誰的外延也是不清晰的。如果是庫,情況則有所不同。
  • 讓你徹底明白yield語法糖的用法和原理及在C 函數式編程中的作用
    骨架代碼猜想骨架代碼其實很簡單,方法的返回值是IEnumerable,然後return被yield開了光,讓人困惑的地方就是既然方法的返回值是IEnumerable卻在方法體內沒有看到任何實現這個接口的子類,所以第一感覺就是這個yield不簡單,既然代碼可以跑,那底層肯定幫你實現了一個繼承IEnumerable接口的子類,你說對吧?
  • C語言編程:以實例教你學指向函數的指針
    指針是C語言的精髓,對於初學者來講,指針是C語言語法學習中比較難的知識點,而這裡面指向函數的指針更是不太容易理解。下面給大家講下怎樣學習理解C語言中指向函數的指針及編程方法和使用例子。注意:這是一篇關於C語言編程的基礎語法內容,C語言大神請繞過。
  • Yann LeCun:深度學習已死,可微分編程萬歲!
    深度學習已死,可微分編程萬歲!事情要回溯到前天。1月4日,AAAI前主席Thomas Dietterich連發10條Twitter,駁斥紐約大學心理學家Gary Marcus對深度學習的批評。動態網絡變得越來越流行(尤其是對於NLP而言),這要歸功於PyTorch和Chainer等深度學習框架(注意:早在1994年,以前的深度學習框架Lush,就能處理一種稱為Graph Transformer Networks的特殊動態網絡,用於文本識別)。現在人們正在積極從事命令式可微分程式語言編譯器的工作。
  • VBA編程如何輸入輸出字符集,兩函數要掌握
    字符函數Chr():返回一個字符串,其中包含與指定的字符代碼關聯的字符。如何輸入字符,在vba編程過程中是一個基本常識。本節主要介紹一下,字符集對應的數值,以及如何輸入字符。很顯然,函數Chr即可輸出相應數值的字符。
  • 面向對象編程的災難:是時候考慮更新換代了!
    函數編程什麼是函數編程?有些人認為它是一個高度複雜的編程範式,只適用於學術界,不適用於「現實世界」。這與事實相去甚遠!函數式編程有很強的數學基礎,並且植根於λ微積分。然而,它的大部分想法都是針對主流程式語言的弱點而提出的。函數是函數編程的核心抽象。
  • 少兒編程中的取整函數和生活應用
    想起,Scratch少兒編程裡有一個取整函數代碼,很多同學常常會把向上取整和向下取整的概念混淆,還有的同學感覺這個數學知識點好像並不怎麼實用。今天我們就用生活中的實際應用來談談取整函數。就如剛才提到的時間問題,如果2.5個小時,按照3小時算,那麼你就要支付3元停車費。如果按照2小時算,那麼你只要支付2元停車費。
  • 你覺得自己很懂程式語言?隨口能說出幾個?但是你真的有理解它嗎
    因此,為了與計算機通信,我們必須以二進位語言給出指令,但這幾乎是不可能的,所以我們需要程式語言來拯救我們。我們按照程式語言的一些一般語法規則用英語編寫了指令,然後通過某種過程將其轉化為機器代碼,並告訴計算機進行一些特定的操作。
  • 有人要為ML定製程式語言,你的Python白學了?
    誠然,重新開創一種語言成本很高,但是考慮到機器學習在未來的廣闊應用前景,為其在程式語言上花些功夫,磨刀不誤砍柴工,未嘗不是一個好的辦法。如果這種語言真的被創建出來,你辛辛苦苦學的 Python 不是白學了? 隨著機器學習(ML)的發展,作為程式語言(PL)學習者和從業者,我們非常關注 ML 模型的複雜性,以及其建模框架的複雜性。
  • 現代程式語言終極測評:五星篇
    ,Elixir ReasonMLReasonML是一門最終會被編譯成JS語言的函數式語言,它主要應用在web前端開發中。學習時需要付出的代價因為ReasonML不準備成為JS的超集,所以這門語言比JS簡單很多。任何有過用JS進行函數式編程經驗的人,都可以在差不多一周之內上手ReasonML。ReasonML真的是這裡提到的最簡單的程式語言之一了。
  • 【第1162期】2018 要學習的優秀 JavaScript 庫與知識
    所以在你學習 React 或者 Angular 之後再學習它吧。Vue.js 在 2017 年表現出色,贏得了很多新聞頭條和人們的興趣。像我所預測的那樣,它沒有趕上 React, 同時我也肯定的說在 2018 年也不會發生。
  • 一文教你 「量子編程」入門式
    只要你認真閱讀下面的這篇文章,思考文末提出的問題,嚴格按照 互動:你的答案 格式在評論區留言,就有機會獲得獎品!量子計算機量子計算機被發現之後,量子編程也在不斷發展。本文將帶你入門量子編程,介紹量子計算機與傳統電腦的區別,解釋量子編程的基本概念,最後教你如何在一個當今免費的量子計算機上運行程序。
  • Python編程數組怎麼創建?4張圖講盡Numpy包所有數組創建函數
    在Python編程和學習中,特別是在科學計算中,不可避免地要創建大量數組。為了將文本中的數據讀取數組中,你還在編寫讀取程序嗎?Numpy中數組創建函數一條命令就能幫你解決。Numpy包集成了大量的函數用來創建各種數組,功能很強大,而我們往往忽略了這些函數。由於我們不知道Numpy的函數有哪些功能,而使用額外的代碼來實現,浪費了時間和精力。函數太多,我們很難完全掌握,也沒有必要掌握,但創建函數的組成和功能將對我們使用Numpy具有一定的幫助。本文使用4張圖介紹Numpy中創建數組的函數組成和功能。