你真的理解函數語言程式設計嗎?

GitChat的部落格發表於2018-04-12

大資料以及人工智慧越來越流程,你是否可以輕鬆適應大資料程式設計,函數語言程式設計在其中起著重要作用,如何從物件導向程式設計跳槽到函數語言程式設計?你是否覺得函式式各種概念難於理解?本場 Chat 將為你解答。我將為你分享親身學習和理解關於函數語言程式設計的經驗:

  • 高階函式、閉包、匿名函式等
  • 高階函式和閉包是啥關係?
  • 柯里化
  • 函數語言程式設計思維

適合人群:

  • 如果你想轉人工智慧領域,請關注此 Chat
  • 如果你想了解機器學習,請關注此 Chat
  • 如果你正在學習一下機器學習,請購關注 Chat

實錄提要:

  • 函數語言程式設計在哪一方面用得比較多?
  • 學習演算法有什麼建議及過程分享,從 0 如何開始學習 Python?
  • 做過 Python,邏輯很強,但是演算法不是很懂,寫的時候不知如何下手?
  • 不同語言中函式的重要性有哪些?
  • 函數語言程式設計演算法和傳統演算法不太一樣,是否要重新學習,有沒有推薦的資料?
  • 對於大型專案,函數語言程式設計有什麼好的應用場景?
  • 想把已有的一個專案,用函數語言程式設計的思想重新實現,有哪些需要注意的事項?
  • 函數語言程式設計與物件導向程式設計那個實現的效能會比較好?
  • 函數語言程式設計具體是在哪個資料分析過程中應用,是清洗還是算統計量的過程中?
  • 函數語言程式設計跟目前物件導向/過程程式設計的主要思維方式上的區別是什麼?
  • 對於前端工程師來說,函數語言程式設計如何入門學習?
  • 函數語言程式設計的概念是什麼?
  • 函數語言程式設計在什麼情況下使用?
  • 如何看待函數語言程式設計的實際應用的?
  • 函數語言程式設計的學習的網上教程有什麼好的推薦嗎?或者書籍?

前言

現在機器學習、人工智慧的發展趨勢如火如萘,很多培訓班也在引入大資料、機器學習的課程,受到眾多IT從業者的追捧,有一種勢必與傳統模式一決高下,分享半壁江山之勢,如果人工智慧走向企業、社會,它帶來的經濟效益以及影響是巨大的,我們每個人不管從事什麼行業都有必要了解一下人工智慧的發展趨勢,機器學習背後隱藏著什麼不可告人的祕密,讓我們來揭開層層面紗,一窺究竟。

在面嚮物件語言滿天飛的現在,很多人認為,函數語言程式設計是一種僅僅存在於某些偏門語言中,然而,縱觀現代主流語言都在引進函式式特性,不同語言可能引進的程度不同,如我們最熟悉的jdk,jdk8已經引入了函數語言程式設計的一些特性,可見大公司在不斷引進新技術,唯有跟著技術前沿發展,我們才能立足於不敗之地。

本篇文章的目錄結構如下:

enter image description here

為什麼函數語言程式設計會在這個時期流行起來?這種思想出現至少10年以上了,那一定是它自身的某些特性符合了這個時代的特性,解決了企業中設計、開發中遇到的各種問題,這是一個流程技術的內在驅動力,在函數語言程式設計裡面函式的地位很高,如物件導向中的物件一樣常見,有三個俗稱三板斧的利器,map、filter、reduce,它將在以後我們的程式設計中隨手不離,函數語言程式設計為我們開啟了另一個思考維度。

思想轉化

電腦科學的進步經常是間歇式的發展,先形成好的思路、然後出來這種思路下面的語言,之後便會成為某一時代的主流,例如第一種物件導向的語言simula 1967年發明,直到1983年的物件導向的C++才流行起來,早年java總被認為太慢,程式設計遲鈍,記憶體消耗太高不適合高效能的應用,如今計算機硬體已經不再是制約條件,硬體的變遷把它變為了極具吸引力的語言,從國內大部分公司應用即可看出來。

那麼大家想一下,如今硬體仍然在飛速發展,那麼更高階的面向函數語言程式設計、語言會不會更適應這個時代?

函式式思想

什麼是函數語言程式設計?

百科定義:

函數語言程式設計是種程式設計方式,它將電腦運算視為函式的計算。函式程式語言最重要的基礎是λ演算(lambda calculus),而且λ演算的函式可以接受函式當作輸入(引數)和輸出(返回值)。

個人理解就是我們的程式設計是以函式作為單元來處理各個業務邏輯,函式既可以當做引數傳來傳去,也可以作為返回值,可以把函式理解一個值到另一個值得對映關係,由於函數語言程式設計方式更適合於資料處理,隨著儲存器容量升高、計算機處理能力大幅提高,它的優勢更加明顯,最近支援函數語言程式設計的語言也逐漸流行,比如python、scale等都因它們對函數語言程式設計的支援被人們重視,從被遺忘的角落重新拾起。

函數語言程式設計因為其特點更適用於統計分析資料、科學計算、大資料處理等方面工作,當然並不限於這些,在web開發、伺服器指令碼等其它方面也很不錯,而物件導向程式設計更適合於開發和處理業務性強、功能模組完備的大型業務系統。

優勢特點

程式碼簡潔、開發快速

函式式程式碼同命令式相比程式碼量要少很多,一行頂十行,所以實現一些功能也比較簡潔,作為開發者的我們,還是對程式碼更親切一些,來看一個具體例子,從中體會其特點。功能描述:統計文字或網頁中單詞的頻率TF(term frequency),詞頻在計算網頁質量、資訊檢索中是一個重要概念,下面來看一下簡化程式碼:

命令式實現

/** * Created by lilongsheng on 2017/11/5. */public class Words {    /**     * 虛詞過濾     */    private static Set<String> NON_WORDS = new HashSet<String>(){        {add("the");add("and");add("of");add("to");add("a");         add("i");add("it");add("in");add("or");add("is");add("d");         add("s");add("as");add("so");add("but");add("be");}};    /**     * 命令式方式過濾實現      * @param words     * @return     */    public static Map wordFreq1(String words){        TreeMap<String,Integer> wordMap = new TreeMap<>();        Matcher m = Pattern.compile("\\w+").matcher(words);        while (m.find()){            String word = m.group().toLowerCase();            if (! NON_WORDS.contains(word)){                if (wordMap.get(word) == null){                    wordMap.put(word,1);                }else {                    wordMap.put(word,wordMap.get(word)+1);                }            }        }        return wordMap;    }

函式式實現

    /**     * 將待處理物件轉為列表集合     * @param words     * @param regex     * @return     */    private static List<String> regexToList(String words,String regex){        List wordList  = new ArrayList<>();        Matcher m = Pattern.compile(regex).matcher(words);        while (m.find())            wordList.add(m.group());        return wordList;    }    /**     * 對集合統一處理     * @param words     * @return     */    public static Map wordFreq2(String words){        TreeMap<String,Integer> wordMap = new TreeMap<>();        regexToList(words,"\\w+").stream()                .map(w -> w.toLowerCase())                .filter(w -> !NON_WORDS.contains(w))                .forEach(w -> wordMap.put(w,wordMap.getOrDefault(w,0)+1));        return wordMap;    }    public static void main(String[] args) {        String test = "Before the popularity of network communicative tools, such as Weibo and WeChat, people get used to know a person by face and face talk, but now the situation has changed, the young generation tend to know a person by network social communicative tools. If they are interested in making friends, then they will be friends online first and then keep trace with the former information on the record. It seems that we can learn a person so fast and convenient. There is a short video about a girl dated a guy, but the guy did not have any account on the Internet, then the girl felt not comfortable and started to question if the guy was a criminal. The video satirizes people to rely on the Internet too much. They rather to communicate with a person by the Internet instead of face and face talk, while the latter is much trustworthy";        System.out.println(wordFreq1(test).toString());        System.out.println(wordFreq2(test).toString());    }}

函數語言程式設計思維是對集合統一處理、統一操作,而指令式程式設計需要取出來每個單詞單獨處理,單獨計數,而函式式只需要傳入待處理物件集合、處理規則,我們不需要關注於具體細節,這樣程式設計不僅僅減少了出現bug的概率而且提高了IT人員開發效率,何樂而不為呢。

易於理解,抽象度高

讓我們再來看一個在開發中,我們經常遇到場景,例如我們有一個List<User>列表,我們要把user的某個屬性提取出來生成一個新的List,如下:

import lombok.Data;/** * Created by lilongsheng on 2017/11/8. * 使用者資料實體物件 */@Datapublic class User {    /**     * 主鍵     */    private Integer id;    /**     * 使用者名稱     */    private String userName;    /**     * 使用者密碼     */    private String userPassword;    /**     * 年齡     */    private Integer age;    /**     * 電話     */    private String phone;    /*其它屬性*/}public static void main(String[] args) {        //初始化資料        List<User> userList = new ArrayList<>();        for (int i = 0; i < 10; i++) {            User user = new User();            user.setUserName("lilongsheng" + i);            userList.add(user);        }        //命令式方式實現        List<String> newUserNameList1 = new ArrayList<>();        for (int i=0;i<userList.size();i++){            User user = userList.get(i);            newUserNameList1.add(user.getUserName());        }        //函式式方式實現 java 8 funcational        List<String> newUserNameList2 = userList.stream()                                                .map(p -> p.getUserName())                                                .collect(Collectors.toList());    }

假設你已經瞭解了函式式語言的語法,你可能會覺得函式式寫法很簡潔,函數語言程式設計並不需要你關注細節實現,我們在獲取使用者名稱作為一個新List時並沒有對單獨user物件操作,而是是告訴集合物件,我們要做什麼,思維重心和關注點從“怎麼做”轉移到了“做什麼”,通過map這個高階函式把集合以及怎麼做的規則傳入,它幫我們處理集合元素,並返回一個結果集。

沒有副作用,變數無狀態

如果一個函式內外有依賴於外部變數或者環境時,常常我們稱之為其有副作用,如果我們僅通過函式簽名不開啟內部程式碼檢查並不能知道該函式在幹什麼,作為一個獨立函式我們期望有明確的輸入和輸出,副作用是bug的發源地,作為程式設計師開發者應儘量少的開發有副作用的函式或方法,副作用也使得方法通用性下降不適合擴充套件和可重用性。

函數語言程式設計語言強烈要求使用者編寫沒有副作用的函式,它的函式式數學意義上的函式,給定一個輸入就會有一個輸出,而且每次相同的輸入輸出也肯定一樣,函式式中的變數所代表的意義並不是記憶體中的一塊儲存單元,因此多個函式同時操作一個變數或者一個函式呼叫多次一個變數都不會改變該變數的值,函式每次對於給定的輸入都是作為一個新值來處理,就是因為它這種沒有狀態、沒有副作用的理念,很適合大資料計算和處理,它只接受固定輸入既可以得到預定計算結果,完全不依賴於外部環境,由這個想到了呂迅說過“我們要做一個純粹的人、做一個大寫的人”,不能因為環境、外部壓力所屈服,以後,就讓我們來編寫無副作用的函式吧。

函數語言程式設計其中的“函式”並不是我們計算機中理解的方法或函式,而是純數學領域函式的概念,看一下百科中對數學函式的定義:

函式的定義:

給定一個數集A,對A施加對應法則f,記作f(A),得到另一數集B,也就是B=f(A)。那麼這個關係式就叫函式關係式,簡稱函式。函式概念含有三個要素:定義域A、值域C和對應法則f。其中核心是對應法則f,它是函式關係的本質特徵。

命令式程式是基於馮諾依曼計算機結構為基礎,一條一條的執行操作指令,程式執行效率為每條命令執行的總和,我們是面向命令程式設計,一般我們使用的語言都是命令式語言,比如c c++ java 等等,還有一種程式設計方式叫做邏輯式程式設計,程式碼風格和命令式一樣,只是在思考過程的時候,更偏重於邏輯思想部分,對於複雜的問題可能會更有優勢。

指令式程式設計是計算機硬體的抽象,可以這樣理解,我們把預先寫好的一條條程式碼翻譯成機器語言就是01指令,告訴計算機先執行哪個命令後執行哪個命令,最終是去改變了計算機硬體的穩定狀態 1 穩定 0 不穩定 所以,可以說指令式程式設計是計算機硬體的抽象。

函數語言程式設計是對於數學的抽象,理論基礎來源於數學基礎。

概念分析

閉包

定義:

一種特殊的函式,繫結了函式內部引用的所有變數,把它引用的東西都放在一個上下文中“包”了起來。百科定義:包含兩層含義要執行的程式碼塊(自由變數以及自由變數引用的物件)和自由變數的作用域。

閉包的概念在很多不同內容中都有提到,如資料庫原理中函式依賴有閉包、JavaScript也有閉包、好多語言裡面也會提到閉包的概念,它們得意思是否一樣呢?答案是一樣的,只是他們的表現形式不一樣,資料庫中的閉包是描述列是否可達、資料庫正規化的依據,把需要的東西放在了“上下文中”包了起來,解釋閉包時先讓我們來回顧一下前提條件:

函數語言程式設計以函式作為一等公民,以函式為單元完成各種操作,函式是自變數到因變數的一一對映,即一個值到另一個值得對映,接收一個值返回另一個值,好了前提條件已經有了讓我們來完成一個函式,計算兩個數的相加操作,如下:

def plus(senior):    return senior + 100if  __name__ == '__main__':    result = plus(3)    print result

由於函式只能接收一個引數,該函式只能計算加100,這一種型別加法,想一想怎麼才能讓它再接收一個引數不變的情況下,計算出結果呢,也許你會想到先把值儲存起來,不過函數語言程式設計是不儲存計算中間結果的,即變數是不可變的,我們可以把這個函式處理結果傳遞給另一個函式(同樣接收一個引數),這樣一來即可計算兩個數加法,如下:

def plus(senior):    def pluaA(second):        return senior + second    return pluaAif  __name__ == '__main__':    pluaA = plus(3)    result = pluaA(2)    print result

上面程式碼中plus即是一個閉包,閉包不僅僅是一個函式,從定義可以看出其包含幾個部分,引用變數、程式碼塊、作用域,可以結合上述程式碼加以理解。

閉包函式裡面的函式雖然有函式名字但是並沒有意義,只是一個名稱而已從外面不能直接訪問,屬於匿名函式,個人理解它屬於一種特殊的函式以及該函式包含的作用域裡面包含的東西,閉包是一種看不見摸不著的東西,因此不好理解,通過閉包可以將n個函式相互連線起來,可以無限相互之間結果程式對映,如果沒有閉包那麼數學中的函式將會是一個個死的函式式子,閉包是函數語言程式設計的靈活、是函數語言程式設計的核心。

把程式碼繫結到閉包後,可以推遲到適當的時機再執行閉包,是一種對行為的建模手段,讓我們把程式碼和上下文同時封裝在單一結構,也就是閉包裡面,以後執行。

高階函式

高階函式從字面上面理解除了普通函式功能還是高階內容,即可以把函式作為引數或者返回值的函式,從這個角度來說增強了函式處理能力,書上定義為可以接收函式為引數或者返回一個函式作為引數的函式。大家思考一下,高階函式為什麼可以這麼設計,閉包在裡面起到了什麼作用?

匿名函式

匿名函式即lambda表示式函式,經常作為函式引數或者生成表示式的場景中,閉包中裡面的函式可以看成是匿名函式來理解,上面計算兩個數的加法還可以改為這樣:

def plus2(senior):    return lambda second:senior+secondif  __name__ == '__main__':    pluaA2 = plus2(3)    result = pluaA2(2)    print result

這兩種寫法意思和作用是一樣的,其實上面的PluaA也是匿名函式,只不過這樣寫會易於閱讀和理解,匿名函式常用在比較簡單的參數列達式中,使得程式碼簡化、簡潔,但是不要濫用匿名函式,如果程式碼中到處是匿名函式那麼看起來會很不好理解。

柯里化

通俗的理解是將函式的引數變為一個引數的形式,函數語言程式設計提倡柯里化程式設計,儘量編寫一個引數的函式,優化方便,簡化程式碼。

語法糖

指程式的可閱讀性更高、可以給我們帶來方便,更簡潔的寫法,提高開發編碼效率某種語言中新增了某種語法,這種語法對功能沒有影響,但可以提高可閱讀性、間接性等,則稱這種語法為該語言的語法糖

效能優化

緩求值

是函數語言程式設計語言的一種特性,這個特性也比較好理解,儘可能的推遲函式或表示式的計算過程,等到真正用到的時候才載入資料,類俗語hibernate框架中的懶載入,利用緩求值的方式來使用對映,可以產生更高效率的程式碼。

尾遞迴、尾呼叫

尾呼叫是在函式的尾部,呼叫另一個函式,因為函式是語言沒有迴圈,遞迴很重要,對於遞迴需要來不斷優化,一般採用尾呼叫或尾遞迴來優化。顧名思義,尾遞迴就是從最後開始計算, 每遞迴一次就算出相應的結果, 也就是說, 函式呼叫出現在呼叫者函式的尾部 ,尾遞迴就是把當前的運算結果(或路徑)放在引數裡傳給下層函式和普通遞迴區別在於記憶體佔用。

總結

函數語言程式設計其特性非常適合於大資料處理、科學計算、資料統計等業務,隨著人工智慧、機器學習、深度學習的流行正在變得重要起來,從面相物件思維到函式式思維的轉變,是我們更好的面對人工智慧領域問題的催化劑,它會使我們從細節中解救出來以數學的思維考慮問題,不管你是從事哪個領域工作,都有必要了解一下智慧時代發展的方向,因為未來你從事的職業很可能被智慧化取代。


本文首發於GitChat,未經授權不得轉載,轉載需與GitChat聯絡。

閱讀全文: http://gitbook.cn/gitchat/activity/59fe7550d343ff71a3c8620f

一場場看太麻煩?成為 GitChat 會員,暢享 1000+ 場 Chat !點選檢視

相關文章