為複雜性語言辯護:類的意義 - viralinstruction

banq發表於2022-04-07

在2014/15年的冬天,我是一名大學生,我的特點是手上有太多的空閒時間,卻沒有足夠的錢讓自己在空閒時間裡忙碌。無聊又沒錢,程式設計是一個完美的愛好。如果你已經擁有一臺電腦,它是免費的,而且當你與無聊作鬥爭時,時間的投入並不令人氣餒。我是在別人的推薦下選擇學習Python的,我可以發自內心地把這個推薦轉發給初學者。學習曲線很平緩,當你只需要搞清楚for迴圈是如何工作的時候,這門語言大多是令人愉快的,沒有太多的干擾。我進步得夠快了。

然而,有一個概念我在理解上有很大的困難:類。不是在類的實現的深層的黑暗魔法,而是簡單的類的概念,因為它出現在它的表面上。

我的學習材料用這樣的陳詞濫調來介紹類:
類允許你在你的程式碼中直接為物件建模。假設你寫了一些與你的狗Rex有關的程式碼,它能吠叫。在這種情況下,你可以寫

class Dog:
    def __init__(self, name, weight_kg):
        self.name = name
        self.weight_kg = weight_kg

    def bark(self):
        print("WOOF!" if self.weight_kg > 25 else "Woof")

rex = Dog("Rex", 35)
rex.bark()


你能發現為什麼上述內容對那些從未接觸過“類”概念的人來說是一個糟糕的介紹嗎?花點時間反思一下吧。

banq注:“類”是大道至簡的語文中修辭手法:比喻。見:資料模型是一種羞恥手法


如果你不瞭解一個解決方案所解決的問題,你就無法理解它。如果你先提出解決方案,就很難發現一開始就有一個問題。(banq:建模對映是初級程式設計師缺乏的認知)

這就是我在閱讀《小狗雷克斯》的例子時遇到的問題。並不是說我不明白程式碼是如何工作的,例子中的類確實看起來像一隻35公斤重的狗,名叫Rex,會叫。但是你知道什麼也像一隻35公斤重的狗,叫Rex,會叫嗎?兩個變數和一個函式。

def bark(weight_kg):
    print("WOOF!" if weight_kg > 25 else "Woof")
    
name = "Rex"
weight_kg = 35
bark(weight_kg)


比較上面的兩個程式碼片斷。它們在功能上是相同的。後者要短得多,而且直奔主題。
對於一個已經學過函式和變數的學生來說,它沒有引入新的語法或規則。

請暫停一下,反思一下 Python 的類語法是多麼的奇怪和不直觀。__init__?真的嗎?那什麼是self呢?為什麼一個沒有引數的函式被定義為有一個引數的函式?bark(weight) 和 dog.bark() 在功能上有什麼區別?

從各方面來看,第二個程式碼片段都是更好的程式碼。
所以我認為我可以沒有“類”。
我認為,如果 Python 類有存在的理由,它們就必須有一些我看不到的特殊行為!這就是我的想法。

從現在來看,很明顯我的假設是錯誤的。
當然,“類”確實釋放了一些新的行為,如果不使用類就無法實現,但這並不是類有用的原因。事實上,我現在寫的幾乎所有的類都沒有做任何用內建型別和函式呼叫無法實現的事情。

Rex the Dog這個例子的問題在於,類並不是為了能夠代表你的狗。
它代表關於完全不同的東西:封裝、模組化、抽象。
我們不要對這些術語的確切含義挑三揀四,它們其實都是關於同一件事:管理你自己程式碼的複雜性

軟體不是無限的
作為一種人工製品,軟體與其他工藝的實物創作有很大的不同。生產它不需要消耗任何原材料。它不需要專門的工具來製造甚至最高質量的程式碼。產品沒有重量,其物理分佈幾乎不費吹灰之力。生產數以百萬計的拷貝並將它們運往世界各地幾乎沒有任何成本。

那麼,如果沒有這些限制,軟體是無限制的、無限的嗎?不是的,它受到其他約束的限制。有時,軟體會受到它所執行的機器的物理能力、磁碟空間、記憶體使用或計算速度的限制。我並不想貶低這些物理約束。畢竟,我在這個部落格上寫的很多東西都是關於效能的。但最主要的是,軟體受到其建立過程的限制。程式設計師建立程式碼的時間有限,而維護程式碼的時間尤其有限。

你看,維護程式碼,比如說修復錯誤,需要程式設計師詳細瞭解程式碼的內部運作情況。程式碼越多,複雜程度越高,理解的時間就越長,程式設計師由於沒有理解程式碼執行時正在發生或可能發生的事情而增加的錯誤就越多。

與其他創造性的勞動產品相比,這並不是說軟體被詛咒為特別難以理解。正是因為沒有其他障礙,程式設計師們才可以不斷地創造,不斷地創造,直到我們擁有一個比我們所能建造的任何物理裝置都要多的活動部件,並不斷地創造,直到我們無法將自己的創造留在腦海中,並在複雜的泥潭中慢下來,耗盡了時間。

這就是為什麼類在 Python 中很重要:它們可以幫助我們在程式成長的過程中,使我們的程式更容易受到控制。
作為一個初學者,我不知道,因為我還沒有經歷過我的一個程式的複雜性像滾雪球一樣失控的情況。我從來沒有見過一扇鎖著的門,所以我不明白為什麼有人會想要一把鑰匙。

此後,我有幸向其他初學者教授 Python。在這些課程中,我優先考慮在課程結束時給學生布置一個大型的個人專案,即使這意味著必須在課程的其他方面進行削減。在指導學生時,這提供了一個獨特的機會,讓他們知道,而不是告訴他們,良好的編碼實踐可以幫助他們解開面條狀的程式碼,重新控制他們正在下沉的專案。

一個沒有類的 Python會怎麼樣?
那麼,如果我們禁止 Python 中的類,會發生什麼呢?

哦,這將使語言變得非常簡單 就像我舉的 Rex 的例子一樣--剩下的將是純粹的領域邏輯、商業邏輯,真正的交易!幾乎沒有程式碼浪費在模板或儀式上。幾乎沒有程式碼被浪費在模板或儀式上! 沒有那麼多奇怪的語法要教,沒有像我這樣的新手在學習時被卡住的路障。最重要的是,幾乎不會有任何功能的損失,因為類大多不會給你帶來任何新的行為。

可能還會發生的是,使用者會發現變數的數量會變得無法控制。直到有一天,一些程式設計師想到了一個很好的主意,他們可以減少儲存在頭腦中的變數的數量,只要他們將變數分組在一個dict中。

def bark(dog_dict):
    print("WOOF!" if dog_dict["weight_kg"] > 25 else "Woof")

rex = {"name": "Rex", "weight_kg": 35}


因此,他們會意外地重新引入類,只是這一次,類的存在在程式碼中是隱含的,它們的行為是臨時定義的,它們的不變數散佈在原始檔中,而且沒有語言級的工具或內省來幫助程式設計師。

類仍然存在,但是作為隱含的模式。

這類結構會自發地、不斷地出現在程式碼中。

以前的程式語言不支援函式,但後來人們發現,指令往往是按功能分組的,而且將其概念化為一個函式可以使程式碼更容易推理。函式的引入並沒有使程式設計變得更復雜,相反,它在重要的方面變得更簡單。

以前的語言沒有結構,但後來程式設計師們發現了將資料集歸為一種抽象的、更高階的資料的有用性,這就是結構。而且,這一特性並沒有使程式變得更加複雜,而是使其更加簡單。

Julia和Rust
與那個時代的語言相比,現代的程式語言被塞得滿滿的。我個人碰巧喜歡Rust和Julia,這兩種語言都是(在)有名的複雜和有特色的。

Julia有一個複雜的型別系統。像AbstractSet{Union{Nothing, <:Integer}}這樣的型別並不容易學會解析,也不容易在現有程式碼中進行推理。但是這種型別的結構,以及它的複雜性,只是程式設計師對它所代表的資料的意圖的例項化。在一個更簡單的型別系統中,這個型別將不存在,但同樣的意圖還是存在的。

Python曾經沒有一個足夠豐富的型別系統來表達這樣的概念,因此程式設計師在閱讀Python時,不得不自己去解決一個特定的變數隱含地符合該型別的約束--如果讀者幸運的話,可以從程式碼註釋中讀到這些資訊,但大多數時候,這些知識只能通過在你的頭腦中保留周圍程式碼對變數的所有隱含假設來獲得。

這真是太糟糕了。

並非巧合的是,最近的Python版本引入了由複雜的型別系統支援的型別提示,這樣,程式設計師現在就可以用collections.abc.Set[typing.Option[numbers.Integral]]來表達同一個想法。儘管這個新的型別系統很複雜,但 Python 也因此而變得更好。事實上,自從學習 Julia 以來,我對 Python 最好的體驗就是開啟我的一箇舊的 Python 程式碼庫,用型別提示來註釋所有的東西,然後在上面執行一個型別檢查器。

著名的是,Rust的編譯器執行了 "所有權 "的概念--一塊資料可以對另一塊資料負責。但Rust並沒有發明這個概念。它是支撐物件導向的核心思想之一,比Rust的出現還要早幾十年。Julia語言沒有所有權的概念,而FASTA.Record的檔案串卻說。

看到了嗎?Julia確實有一個 "所有權 "的概念,只是沒有,你知道,在實際的語言中。但是使用FASTA.Record的程式設計師必須跟蹤誰擁有它的資料,而這種心理記賬的方式使FASTA.Record更難使用,因為關於它的程式碼更難遵循。Rust的所有權模型的複雜性並沒有增加到一個簡單的程式中,它只是編譯器對你的程式碼遵守它無論如何都要遵守的規則非常迂腐而已。

"一種簡單的語言?Go語言簡單是一個謊言"
這就是Zig語言網頁頂部附近用大字寫的。Zig是最近進行的一項實驗,目的是為了消除困擾現代程式語言的所有被嘲笑的複雜性。封閉、函式特性、運算子過載--程式設計已經夠難的了,為什麼我們不能至少用一種簡單的語言程式設計,而不需要所有這些垃圾呢?

說實話,Zig看起來是一種很酷的語言,我想有一天能學會它,但我不能說它的簡單性是它最有吸引力的品質。在標題的下面,網站上寫著。

專注於除錯你的應用程式而不是除錯你的程式語言知識。
我為什麼要這樣呢?現代語言複雜性的全部意義在於減少你的應用程式所需的除錯量,因為它的複雜性是由語言適當管理的。

Zig使用手動記憶體管理,並且沒有像Rust那樣在編譯時保證記憶體安全。當然,這意味著,與編寫Rust程式碼相比,編寫Zig程式碼可能更簡單,它可以編譯,但在執行時崩潰,之後你就可以專注於 "除錯你的應用程式"。

對我來說,這是個文字遊戲。在Zig中找出如何滿足例如所有權規則的困難,僅僅是慣例,是對 "應用程式 "的除錯,而在Rust中,同樣的困難是對 "程式語言 "的除錯。

當然,Zig並不是第一種明確追求簡單性的語言。Go在Zig之前就這樣做了,其動機基本相同。擺脫語言的束縛。在某些方面,他們是成功的。Go被譽為一種容易學習的語言。但另一方面......好吧,讓我用別人的話來結束。

Go語言的每一份文件都反覆強調它是 "簡單 "的,這是個謊言。或者說,這是一個半真半假的謊言,它方便地掩蓋了這樣一個事實:當你把某樣東西變得簡單時,你就把複雜性轉移到其他地方。

總結
以 "為......辯護 "為格式的帖子沒有太多的細微差別,當然,本帖的問題也不是一目瞭然的。"更多的語言特點 "並不等於 "更多的好",現代語言複雜性的詆譭者確實有值得考慮的觀點,至少是孤立的。

上述例子中的所有語言功能--類、高階型別和借貸檢查器--都有一個重要的共同特徵:它們都感覺是自發地從現有的程式碼中產生的,與語言設計者是否考慮過它們無關。從這個意義上說,它們是最好的一種特性;它們沒有增加新的東西來擔心,而只是提供一個詞彙和工具來處理已經存在的問題。

不是所有的語言特性都是這樣的。例如,Julia 有四種不同的方法來定義一個函式,而for 迴圈的外觀也有同樣多的變化。人們可以將一個型別定義為結構體、可變結構體、抽象型別和原始型別(所有前者都可能是引數化的)。型別可以作為具體型別、抽象型別、聯合型別或聯合所有型別放置在型別層次中。型別的表示(即型別的型別)可以是DataType、Union、UnionAll或Bottom。

這樣的複雜性並不完全是多餘的,但肯定是要學習的,而且我不清楚這樣的設計是最乾淨的。當然,感覺上,這並不需要如此複雜。

最糟糕的一種功能是重複的API,通常是因為一箇舊的、設計不良的API一直存在,只是為了滿足向後的相容性,也許還有一小部分使用者拒絕停止使用它。我並不喜歡語言中的這種複雜性,人們避開它是正確的。

然而,在一個更基本的層面上,反對者是對的,即使語言中合理的複雜性也會給使用者帶來成本。我喜歡Rust,但我寫它超過兩個小時就會希望我寫的是Julia,因為編譯器讓我試圖寫一些該死的程式碼來工作的努力受挫。即使一個嚴格的編譯器只執行你要手動執行的不變性,編譯器也是愚鈍的,而且極難說服你,不,這種反模式在這種情況下實際上是合適的。當一個程式的結構由人類自由控制時,人類可以選擇捷徑和簡單的解決方案。Rex the Dog可以只保留兩個變數,而且真的會減少模板,即使也會有一些地雷。

這篇文章的開始是我向一個程式設計初學者推薦Python,這並不是偶然的。語言的學習已經需要大量的時間投入,而一種語言中塞進更多的東西,即使是設計良好的東西,也需要更大的投入。大型Python專案因其自身難以管理的重量而僵化而臭名昭著,但從另一個角度看,學習Python所需的時間相對較少。我當然不會推薦Rust作為任何人的第一種語言。

我們很容易理解負責綠地專案的程式設計師團隊的困境,他們面臨著這樣的選擇:是花幾個月的工資來招收新員工,讓他們學習一門難學的語言,然後有一半的人離開,還是選擇一門容易上手的語言。

那麼,在現代程式語言的複雜性方面既有缺點也有優點,我們應該得出什麼結論?恐怕這篇博文不可能有令人滿意的結論。雖然我不相信答案只是一個意見問題,但也不完全是一個事實問題。唯一的解決方法是我們作為專業人士要運用我們的判斷力。

為複雜性語言辯護:類的意義 - viralinstruction

相關文章