你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 簡單點,理解iOS與函數式編程

簡單點,理解iOS與函數式編程

編輯:IOS開發基礎

有時候,一個關鍵字就是一扇通往新世界的大門。兩年前,身邊開始有人討論函數式編程,拿關鍵字Functional Programming一搜,全是新鮮的概念和知識,順籐摸瓜,看到的技術文章和框架也越來越多。

我有個習慣,在接收新知識的時候,我都會用已有的知識去做對比,我更關注新事物能對現有產品和知識體系帶來哪些好處。

計算機發展到今天,已經很久很久沒有理論層面的升級了,現今絕大部分新的知識都是基於已有的內核做包裝。函數式編程不是新生事物,也不是獨立的知識孤島,函數式編程核心思想離我們也沒那麼遠。

這篇文章會拋開陌生的術語和概念,只站在抽象和基礎的層面,去聊下函數式編程和我們現有編程習慣的聯系。

Functional Programming翻譯為函數式編程,初次接觸的時候會不由自主的認為,這種編程范式的核心在於對Functional的理解,或者說是對函數的理解。函數我們每天都在寫,還有什麼需要特別去理解的嗎?我個人覺得這是個誤區,相對於理解「函數」,我們更需要理解的其實是「狀態」。如果叫做Stateless Functional Programming可能會更貼切一點。

狀態

函數和狀態是人人都熟悉的概念,可越是簡單的每日可見的概念,越難理解的透徹。說到狀態,很多同學會聯想到變量,局部變量,全局變量,property,model,這些都可以成為狀態,但變量和狀態又不是一回事,要真正理解狀態,得先理解下面一行代碼:

int i = 0

簡單的一行代碼,分析起來卻有不少門道。

「i」就是我們所說的變量,一個變量可以看做是一個實體,真實存在於內存空間的實體。int是它的類型信息,是對於它的一種約束,類似於上帝對於人類性別的約束,每個人都需要有性別。0是它被賦予的一個值,值是外部信息,類似於人的職業,職業不是與生俱來的,人一生可以選擇從事不同的職業。

變量是我們要分析的目標,它的類型信息,值信息雖然會約束變量的行為,但不是我們關注的重點,真正讓變量變得危險的是中間的等號,=是個賦值操作,意味著改變i的值,原本處於靜態的i,由於一個=發生了變化,它的值發生了變化,它可以變為1,或者10000,或者其他任何值,這個看似簡單的改變可以說是我們程序的bug之源,值的變化可以像扔進湖面的石頭,層層疊疊影響其他空間和實體。

一旦一個變量開始與=打交道,一旦變量的值會發生變化,我們就可以說這個變量有了狀態。或者我們可以說,有=就有狀態。

狀態也是個相對的概念,變量都有其生命周期,一旦變量被回收,其所包含的狀態也隨之消失,所以狀態所帶來的影響是受限於變量的生命周期的。我們看下這段代碼:

- (int)doNothing
{
  int i = 0;
  return i;
}

i是函數doNothing內部的臨時變量,分配在內存的棧上,一旦return,i的生命周期也隨之結束。

站在doNothing函數內部這個空間范疇來說,i是有狀態的,i被賦予了值0,當renturn執行之後,i被內存回收了,i隨之消失,其所對應的狀態也消失了,所以一旦出了doNothing,i又變得沒有狀態了。代碼雖然執行了return i,但返回的其實是i所代表的值,i將自己的值交出來之後,就完成了自己的使命。

所以站在doNothing函數外部空間的角度來說,doNothing的使用者是感受不到i的存在的,doNothing的調用方可以認為doNothing是無狀態(stateless)的,無狀態意味著靜止,靜止的事物都是安全的,飛馳而過的火車和靜止的石塊,當然是後者感覺更安全。

我們編寫代碼的時候會經常談論狀態,函數的狀態,類的狀態,App的狀態,歸根結底,我們所討論的是:在某個空間范疇內會發生變化的變量。

函數式編程當中的函數f(x)強調無狀態,其實是強調將狀態鎖定在函數的內部,一個函數它不依賴於任何外部的狀態,只依賴於它的入參的值,一旦值確定,這個函數所返回的結果就是確定的。可能有人會覺得入參也是狀態,是外部傳入的狀態,其實不然,我前面說過變量才會有狀態,值是沒有狀態的,入參傳入的只是值,而不是變量。下面兩個函數,一個入參是傳值,一個入參是傳變量:

- (void)doNothing:(int)v  //傳值
{
  
}

- (void)doNothing:(NSMutableArray*)arr //傳變量
{
 
}

第二個版本的doNothing,不但是傳入了變量,還是可以變化的變量,是真正意義上的外部狀態。很有可能在你遍歷這個arr的時候,外部某個同時執行的線程正在嘗試改變這個arr裡的元素,是不是很危險?

所以對於下面兩種調用來說:

[self doNothing:user.userID];
[self doNothing:user.friends];

第一個調用只是傳入了userID所對應的值,第二個調用卻傳入了friends這個變量實體。第一個沒依賴,第二個有依賴,第一個沒狀態,對調用方來說是安全的,對整個app來說也是安全的,既避免了依賴外部的狀態,也不會修改外部的狀態,即:不會產生side effect,沒有副作用。

所以讓我來總結函數式編程當中的函數,可以一句話歸結為:隔絕一切外部狀態,傳入值,輸出值。

我們再來看看函數式編程當中的純函數(Pure Function)的定義:

In computer programming, a function may be considered a pure function if both of the following statements about the function hold:

  1. The function always evaluates the same result value given the same argument value(s). The function result value cannot depend on any hidden information or state that may change while program execution proceeds or between different executions of the program, nor can it depend on any external input from I/O devices (usually—see below).

  2. Evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices (usually—see below).

純函數即為函數式編程所強調的函數,上述兩點可翻譯為:

  1. 不依賴外部狀態

  2. 不改變外部狀態

所以對函數式編程當中函數的理解,最後還是落實到狀態的理解。靜止的狀態是安全的,變化的狀態是危險的,之所以危險可以從兩個維度去理解,時間和空間。

時間

變量一旦有了狀態,它就有可能隨著時間而發生變化,時間是最不可預知的因素,時間會將我們引至什麼樣的遠方不得而知,我們每創造一個變量,真正控制它的不是我們,是時間。

時間的武器是變化,是賦值,賦予變量新的值,在不可預知的未來埋下隱患。

- (void)setUserName:(NSString*)name
{
  //before assignment
  _userName = name;
  //after assignment
}

一旦有了賦值操作,時間就找到了空隙,可以對我們代碼的執行產生影響。或許是在此刻,或許是明天,或許是在appDidFinishLaunch,或許是在didReceiveMemoryWarning。每一個賦值操作都是一顆種子,可以結出新feature或者新bug。

變量會隨著時間變化,有狀態的函數也會隨著時間的流動產生不同的輸出,Pure Function卻是對時間免疫的,純函數沒有狀態,無論站在多長的時間跨度去執行一個純函數,它所輸出的結果永遠不會變,從這一角度看,純函數處於永恆的靜止狀態。

空間

如果把一個線程看成一個獨立的空間,在程序的世界當中,空間會產生交叉重疊。一個變量如果可以被兩個線程同時訪問,它的值如果可以在兩個空間發生變化,這個變量同樣變得很危險。

Pure Function同樣是對空間免疫的,無論多少個線程同時執行一個純函數,純函數總是產生相同的輸出,而且不會對外部環境產生任何干擾。

多線程的bug調試起來非常困難,因為我們的大腦並不擅長多路並發的思考方式,而函數式編程可以幫我們解決這一痛點,每一個純函數都是線程安全的。

離不開的狀態

函數式編程通過Pure Function,使得我們的代碼經得起時間和空間的考驗。

我們可以把一個App的代碼按照函數式編程的方式,打散成一個個合格的pure function,再通過某種方式串聯起來,要方便的串聯函數,需要把函數變為一等公民,需要能像使用變量一樣方便的使用函數。

一個Pure Function可以是stateless的,但我們的App可以變成stateless嗎?顯然不能。

離開了變量和狀態,我們很難完整的描述業務。用戶購物車裡的商品總是會發生變化,今天或明天,我們總是需要在一個地方接收這種變化,保存這種變化,繼而反應這種變化。所以,大多數時候,我們離不開狀態,但我們能做的是,將一定會變化的狀態,鎖定在盡可能小的時間和空間跨度之內,通過改變代碼的組織方式或架構,將必須改變的難以管教的狀態,囚禁在特定的模塊代碼之中,讓不可控變得盡量可控。

其實,即使不嚴格遵從函數式編程,我們同樣可以寫出帶有Functional Programming精髓的代碼,一切的一切,都是對於狀態(state)的理解。

在我看來,NSMutableArray的copy,也是頗具函數式編程精髓的。

一等公民(First Class)

當我們把函數改造成pure function之後,會產生一些奇妙的化學連鎖反應,這些反應甚至會改變我們的編程習慣。

一旦我們有了絕對安全的純函數,我們當然希望能盡最大可能的去發揮它的價值,增加它出現和被使用的場景。為了加大純函數的使用率,我們需要在語言層面做一些改造或者增強,以提高純函數傳遞性。怎麼增強呢?答案是將函數變為一等公民。

何謂公民?有身份證才叫公民,有身份證還能自由遷徙的就叫一等公民。

當我們的變量可以指向函數時,這個變量就有了函數的身份。當我們把這個變量當做函數的參數傳入,或者函數的返回值傳出的時候,這個變量就有了自由遷徙的能力。

一個函數A,可以接收另一個函數B作為參數,然後再返回第三個函數C作為返回值。類似下面的一段swift代碼:

func funcA(funcB: @escaping (Int) -> Int) -> (Int) -> Int {
    return { input in
        return funcB(input)
    } //funcC
}

在funcA的定義裡,funcB是作為參數傳入,funcC(匿名的)是作為返回值返回。funcB和funcC在這個語境裡就稱之為first class function。而funcA作為funcB和funcC的管理者,有個更高端的稱謂:high order function。

有了first class function和high order function,我們還會收獲另一個成果:語言的表達力更靈活,更簡潔,更強大了。舉個例子,我們寫一段代碼來實現一個功能:參加party前選一件衣服。用傳統的方式來寫:

func chooseColor(gender: Int) -> Int {
    return 0
}
func dressup(dressColor: Int) -> Int {
    return 1
}

//imperative
let dressColor = chooseColor(gender: 1)
let dress = dressup(dressColor: dressColor)
user.dress = dress

先定義函數,再分三步依次調用chooseColor, dressup,然後賦值。

如果用first class function和high order function的方式來寫就是:

func gotoParty(dressup: @escaping (Int) -> Int, chooseColor: @escaping (Int) -> Int) -> (Int) -> Int {
    return { gender in
        let dressColor = chooseColor(gender)
        return dressup(dressColor)
    }
}

//declarative
let prepare = gotoParty(dressup: { color in
    return 1
}, chooseColor: { gender in
    return 0
})
user.dress = prepare(1)

gotoParty函數柔和了dressup和chooseColor,gotoParty成了一個high order function,當我們讀gotoParty的代碼的時候,這單一一個函數就將我們的目的和結果都表明了。

這就是high order function的神奇之處,原先啰啰嗦嗦的幾句話變成一句話就說清楚了,它更接近我們自然語言的表達方式,比如gotoParty可以這樣閱讀:我要挑選一件顏色適合的衣服去參加party,這樣的代碼是不是語意更簡潔更美呢?

注意,functional programming並不會減少我們的代碼量,它改變的只是我們書寫代碼的方式。

這種更為強大的表達力我們也有個行話來稱呼它:declarative programming。而我們傳統的代碼表達方式(OOP當中所使用的方式)則叫做:imperative programming。

imperative programming更強調實現的步驟,而declarative programming則重在表達我們想要的結果。這句話理解起來可能有些抽象,實在理解不了也沒啥關系,只要記住declarative programming能更簡潔精煉的表達我們想要的結果即可。

以上都是我們將function變為一等公民所產生的結果,這一改變還有更多的妙用,比如lazy evaluation。

上述代碼中的dressup和chooseColor雖然都是function,但是他們在傳入gotoParty的時候並不會立馬執行(evaluation),而是等gotoParty被執行的時候再一起執行。這也很大程度上增強了我們的表達能力,dressup和chooseColor都具備了lazy evaluation的屬性,可以被拼裝,被delay,最後在某一時刻才被執行。

所以,functional programming改變了我們使用函數的方式,之前使用OOP,我們對於怎麼處理變量(定義變量,修改值,傳遞值,等)輕車熟路,到了函數式編程的世界,我們要學會如何同函數打交道了,要能像使用變量一樣靈活自如的使用函數,這在剛開始的時候確實需要一段適應期。

總結

函數式編程近幾年頗受技術圈的關注,Peak君覺得對於新接觸的知識,我們更應該關注其誕生的目的,及其背後隱含的思想,抓住了本質,理解那些令人望而生畏的技術術語就更有底氣了。

歡迎關注公眾號:MrPeakTech

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved