1. 程式人生 > IOS開發 >【譯】Swift和函數語言程式設計的精髓

【譯】Swift和函數語言程式設計的精髓

我想說這真的是一篇非常非常好的文章,它通過對一個例項的API的優化,教會我們如何寫出優美簡潔的Swift的函式式程式碼。但是這個文章是視訊中作者的口述,所以翻譯過程中難免有不當之處。大家可以對著視訊和原文進行觀看和對比。

原文地址

介紹

Swift第一次被公佈的一週後,我寫了一篇名為“Swift不是函式式”的博文。兩年後,Swift仍然不是函式式。這篇文章並沒有對此進行闡述,而是將討論函式式語言幾十年來的經驗和研究,我們可以用自己快速的方式將這些經驗和研究帶入Swift。

什麼是函數語言程式設計

它是單子、仿函式、haskell和優雅的程式碼。

函數語言程式設計不是一種語言或語法,而是一種思考問題的方式。函數語言程式設計在我們有單子

之前已經存在了幾十年。函數語言程式設計是一種思考如何分解問題,然後以結構化的方式將它們重新組合在一起的方法。

讓我們從swift中的一個簡單示例開始。

var persons: [Person] = []
for name in names {
    let person = Person(name: name)
    if person.isValid {
        persons.append(person)
    }
}
複製程式碼

這是一個簡單的迴圈,它做兩件事:將name轉換成Person,然後將這些人放入一個有效的陣列中。很簡單,但這裡有很多事情。為了理解發生了什麼,你需要在頭腦中執行這個。你需要思考,“這個陣列是做什麼的?“

它看起來很簡單,但可能會更簡單。我們可以把問題分開。我們可以把東西拆開。 我們現在有兩個迴圈,每個迴圈做的更少。每一個都很簡單,更簡單的程式碼讓我們找到模式:建立一個數組,遍歷一些值,對每個值執行一些操作,然後在最後將它們放到另一個數組中。

當你有一些事情做了很多次,你可能應該提取函式-Swift有一個,它被稱為mapmap將某物的列表轉換為其他某物的列表。重要的是它說明了它的含義:可能personsnamesperson的對映。這是一份根據人名列出的名單。我們不必在頭腦中執行任何東西,他就在程式碼中表述我們的意思。

let possiblePersons = names.map(Person
.init) let persons = possiblePersons.filter { $0.isValid } 複製程式碼

另一個迴圈也是一個非常常見的模式:它有一個名為“filter”的方法。filter接受一個謂詞,它是一個返回bool的函式。它使用函式給我們返回有效的資料。我們可以把這些結合在一起,從而獲得possiblePersons,然後把他們加入我們的過濾器。

從七行程式碼到兩行程式碼。另外,我們可以重新組合它們:這些都是我們可以重新組合起來的值。我們用鏈式把他們進行組合。

它很容易閱讀,我們可以一行一行地閱讀。

let persons = names
    .map(Person.init)
    .filter { $0.isValid }
複製程式碼

它十分易學並且很容易適應這種方式。在這樣的例子中你不必再寫一個for迴圈了。

函式式工具

1977年,John Backus(協助發明了Fortran 和 Algol)獲得了圖靈獎,發表演講“程式設計能從馮諾依曼風格中解放出來嗎?“我喜歡這個標題。“馮·諾依曼風格”是指Fortran和Algol。

這篇論文是他為發明它們而做的宣告。他表示指令式程式設計,一步一步地改變某種狀態,直到獲得你想要的最終狀態。當他說“函式式”的時候,那並不是指我們今天所說的函式式,但他啟發了許多函式式研究人員去研究和學習。

這篇論文引起了我的興趣,我們可以回到swift:我們如何將複雜的事情分解成簡單的事情。把這些簡單的東西泛化,然後用一些規則把它們粘在一起,比如代數。

代數是一套規則,用來把事物組合在一起,把它們拆開,然後轉換它們。我們可以想出可以用來操縱程式的規則。我們已經做到了:我們做了一個迴圈,我們把它分解成兩個簡單的迴圈,找到每一個迴圈的通用形式,然後使用鏈式將它們重新組合在一起。當haskell程式設計師第一次遇到swift時,他們往往會感到沮喪,因為試著做它們在Haskell語言中做的事。

在haskell中,與幾乎所有的函式式語言一樣,組成的基本單元是函式。有很多漂亮的方法來組合函式。我可以通過將foldr函式和+函式粘合在一起,並將其初始值設為0,來建立一個名為sum的新函式。不管你讀起來是否舒服,只要你這樣做了,它就相當漂亮了。

let sum = foldr (+) 0
  sum [1..10]
複製程式碼

你可以用swift來做,但是它會很難看,而且它不能很好地工作,因為你在用錯誤單元組合它們。swift不是函式式。

swift中的組成單位是型別。類、結構、列舉和協議,這些都是可以組合的。我們通常把兩種型別粘在一起。通過實現一個函式並將它們粘在一起,我們可以用更簡單的片段來構建它們。

swift中另一種非常常見的構圖是將型別放在語境中。你最常用的是optionals。可選型別是根據語境確定的,在語境中可能有值也可能沒有值。這是一個與型別相關的小資訊:它是否存在?這就是語境的含義。新增語境比其他跟蹤額外資訊的方法強大得多。

extension MyStruct<T>: Sequence {
    func makeIterator() -> AnyIterator<T> {
return ... }
}
複製程式碼

我們可以跟蹤一個事實,即不存在整數,或者根本不存在值,這是因為我們會從-1這樣的整數中竊取一個值,這意味著沒有值。但是現在你必須到處測試,這是醜陋的,容易出錯的,編譯器不能幫你。

如果我們將整數改為可選的,這時編譯器可以幫助您。你有這樣的語境“它存在嗎,它不存在嗎,我可以幫助你確保你不會忘記這一點。”如果你曾經使用過-1,很容易忘記檢查,然後你的程式變得不可控了。

例子

讓我們建立一個更復雜的例子。

func login(username: String,password: String,completion: (String?,Error?) -> Void)
login(username: "rob",password: "s3cret") {
    (token,error) in
    if let token = token {
        // success
    } else if let error = error {
// failure }
}
複製程式碼

這是一個非常常見的API。我們有一個帶有usernamepassword引數的login函式,在某個時刻,它將返回一個token和一個可能的錯誤。我認為我們可以通過考慮語境做得更好。

第一個問題 是這個completion。我說它是一個string,是什麼樣的string。她是一個token.我可以給它貼個標籤,但那沒用。在swift中,標籤不是型別的一部分。即使我這麼做了,還是這個string。字串可以是指很多東西。

Tokens有規則:例如,它們可能必須是固定長度,或者不能為空。這些都是可以用字串做的,但不能用tokens。我們希望有更多關於token的上下文;它有規則,所以我們希望捕獲這些規則。我們可以做到,我們可以給它更多的結構。這就是為什麼它被稱為struct

struct Token {
    let string: String
}
複製程式碼

我把字串放入結構體中。這不會花費任何成本,也不會導致間接或任何額外的記憶體使用,但現在我可以對此設定規則。

你只能用特定的字串來構造它們。我可以在上面加上一些擴充套件,這樣就沒有必要用任意的字串了。這要好多了,我可以對所有型別都這樣做;字串當然沒問題,也可以是字典、陣列和整數型別。

當您擁有這些型別時,您可以將它們提升到上下文中,並控制可以放在它們上的內容。你可以控制他表達的意思。我不需要標籤或註釋,因為第一個引數顯然是一個token,因為它的型別是Token

第二個問題 是我們要傳遞usernamepassword。在大多數有這些的程式中,您總是將它們一起傳遞;password本身尤其無用。我想建立一個規則,允許我用“and”組合使用者名稱和密碼,所以我需要一個“and”型別。我們有一個,它又是一個結構。

"AND"型別

struct Credential {
  var username: String
  var password: String
}
複製程式碼

結構體是"and"型別。 Credential 是由usernamepassword組成. “and”型別被稱為 “生產型別”. 我鼓勵你大聲說出來。例如:“憑證是使用者名稱和密碼。”這有意義嗎?如果它沒有意義,也許它是錯誤的型別,或者你建立它是錯誤的。

func login(credential: Credential,completion: (Token?,Error?) -> Void)


let credential = Credential(username: "rob",password: "s3cret")
login(credential: credential) { (token,error) in
    if let token = token {
        // success
    } else if let error = error {
// failure }
}
複製程式碼

現在我們可以交換Credential,而不是usernamepassword:這也使得我們的簽名更短更清晰。我們還提供了許多不錯的可能性:在Credentials上新增擴充套件,在其他型別的規則下交換它們。也許我們想要10次password,或者access token,或者facebook或者google等等。現在,我不需要更改程式碼的任何其他部分,因為我只是傳遞credentials

不過,它也有問題。我們通過了元組(Token?,Error?) –元組是“and”型別。它們是匿名結構。我們的意思是“也許是token,也許是error”?有四種可能性:兩者都有,或者兩者都沒有,或者一個或另一個。只有兩種可能性是有意義的。如果我得到了一個tokenerror?這是一個錯誤條件嗎?我需要一個致命的錯誤嗎?我需要忽略它嗎?你需要考慮一下這個問題,並可能針對它編寫測試。

“OR” Type (Sum)

問題是你不是說“也許”什麼的-你是指一個token或一個error。我們有可以使用的“或”型別嗎?

enum Result<Value> {
    case success(Value)
    case failure(Error)
}
複製程式碼

It is an enum - enums are “or” types (this or that),whereas structs are “and” types (this and that). Like “and” types are called “product types”,“or” types are called “sum types”. 這是一個列舉 - enums是 “or”型別,就像結構體是“and”型別。正如“and”型別被稱作“生產型別”,“or”型別被稱為“和型別”

func login(credential: Credential,completion: (Result<Token>) -> Void)
login(credential: credential) { result in
    switch result {
    case .success(let token): // success
    case .failure(let error): // failure
    }
}
複製程式碼

我想建立這個result型別。這讓我很困擾,因為它不是內建在swift中的。它很容易建造。我們將提升我們的值,給它更多的語境。它從一種值轉變為一個成功的值。

我們的錯誤變成一個失敗的錯誤,我們有更多的語境。如果我們將result、生成的token扔進我們的api中,那麼我們必須針對所有這些情況編寫測試來保證所有不可能的情況都會消失。我們不必擔心他們,因為他們是不可能的。我希望錯誤不可能,而不是編寫測試用例。

我喜歡這個API。我用credential登入,它會給我一個生成的token

這個課程

這是函數語言程式設計的真正精髓,也是我們應該帶給swift的:複雜的事情可以分解成更小、更簡單的事情。

我們可以為這些簡單的事情找到通用的解決方案,我們可以使用一致的規則將這些簡單的事情重新組合起來,讓我們對我們的程式進行推理。這使得編譯器更容易查出bug,我認為70年代的John Backus完全同意這一點。把它拆了,把它造起來。