過早的優化是萬惡的根源

有次我要開發一個規劃時間的演算法,需要將時間切割成一個一個單位做計算,這時普通的作法會是用布林陣列來儲存每個時間點,然而我需要對時間做一些 AND、OR、NOT 的布林運算,我當時突發奇想,假如用 BigInt 來儲存效能和記憶體都會省下不少,意思是將一個整數視作一個布林陣列,每一個位元就代表一個時間單位,這樣相比用布林陣列,足足省下至少 8 倍的記憶體。我心裡覺得這真是太酷了,於是就一股腦開發基於 BigInt 的演算法。 BigInt 由於本來就不是陣列,不論是讀值、改值都相當因難。以讀取第 i 項的值為例,需要寫成: // normal index array[i]; // bigint index const n = 0b10111000; (n >> i) % 2; // ??? 看起來不直觀、可讀性差、又容易出現差一錯誤。debug 時也相當難纏,直接把 BigInt 印出來,會得到一個巨大十進位整數(本來它就是 Big Int 嘛),需要額外呼叫方法將其以二進位顯示,後來我還將其封裝成一個類別,花了好大力氣寫 unit tests、各種 debug function,最後終於將演算法給實作出來了,花了整整兩週的時間(side project 嘛)。 正當我得意於自己的成果時,遭到現實衝擊——我們使用 React Native 來開發 Mobile App,而 BigInt 是新語法,React Native 還沒有支援。我耗費好幾個小時嘗試各種偏方,然而都不得好轉,最後在無耐之下只好將演算法用陣列重寫。原先以為會天崩地裂、再花上一週的時間,然而由於我有非常多的單元測試,我竟然只用一個下午就將整個演算法改寫完了,比我研究如何在 React Native 跑 BigInt 的時間還少,而改寫後程式的可讀性也提升了不少,那效能呢?由於這是個跑在 client 端的演算法,效能根本不重要,使用體驗毫無差別,省下那一點點記憶體也一點用都沒有。 「過早的優化是萬惡的根源」,在不存在效能瓶頸時就做優化,結果適得其反。比起效能,應該以可讀性優先。 此外,單元測試真的很重要。

August 3, 2021 · 1 分鐘 · wancat

在 Python 中實作對話型聊天機器人

當你在開發一個聊天機器人,有時候為了使用者體驗,你不能要使用者用像指令的方式,將所有資訊一次傳過來。舉例來說,若我們要開發一個猜數字遊戲運作如以下: user: guess bot: From what number? user:: 25 bot: To what number? user: 100 bot: Guess a number between 25 to 100 user: 64 bot: too small user: 91 bot: too large …… user: 83 bot: Correct! You spent 6 times to guess this number. 然而,我們在後端通常是「一個請求一個回覆」,如果要將這樣的行為拆成多個 handler 將會是場災難,為什麼?想想要怎麼存狀態,全域變數?資料庫?還是 Redis?每當你多問使用者一個問題,你就得在你的 state schema 新增一個欄位,讓你的程式碼越來越複雜。 接下來,我會告訴你如何用一個非常輕鬆的方式處理對話,讓你只要寫像以下一般的程式碼就能達成。 def guess(self): '''Game function''' min_value = self.ask_number('From what number?') max_value = self.ask_number('To what number?') secret = randint(min_value, max_value) msg = f'Guess a number between {min_value} to {max_value}' counter = 0 while True: counter += 1 answer = self....

July 29, 2021 · 3 分鐘 · wancat

基本收入不能解決貧富差距,又如何?

當我們用「翻轉」一詞,其實就已經隱含了單一的價值觀:有錢就是高社會階級

July 8, 2021 · 1 分鐘 · wancat

Django 將 Stdout 導向 Streaming Response

有時候後端要執行一個時間比較長的任務,而任務內容極為複雜,又容易出錯,因此希望讓使用者看到即時的 console log,讓我們函式中的 print 輸出能即時傳到使用者的瀏覽器。 以下將會以 Django, Thread, Queue 進行實做 StreamingHttpResponse 一般的網頁請求都是一次打包好所有資料,全部傳給使用者,有些情況我們不能等到所有資料準備好才一次傳,而要拿到一些就傳一些,這個時候我們就要使用串流輸出,在 Django 裡,就是使用 StreamingHttpResponse,以下簡稱 SHR。SHR 接收一個 Iterator 作為輸入,因此我們只要實做一個迭代器函式,其中每次 yield 就會由 SHR 傳送到瀏覽器 # Example of StreamingHttpResponse from django.http.response import StreamingHttpResponse def example(): for i in range(5): # Add <br> to break line in browser yield f'{i}<br>' def stream(request): return StreamingHttpResponse(example()) Output (in browser): 0 1 2 3 4 Thread 由於我們的程式需要一邊執行目標任務,一邊串流輸出,因此需要平行化執行。Python 中可以使用 threading, multiprocessing 等方式做平行化執行,本文將使用 threading。 # Example of threading from threading import Thread import time def example(times): for i in range(times): print(i) time....

May 25, 2021 · 3 分鐘 · wancat

減法的藝術

這時我才終於體會了老師口中的「有捨才有得」,若無狠心除去那些無關的枝葉,最可貴的美就會被埋沒。園藝是如此,創作又何嘗不是呢?

April 18, 2021 · 1 分鐘 · wancat

母語者心態

這些方法也許的確有效,但並不是所有人都能做到我們認為輕而易舉或理所當然的事,當帶著這樣的「母語者心態」看別人,就容易將他人的不成功歸因到不夠努力的結果,卻沒想到也許對方根本做不到。

March 19, 2021 · 1 分鐘 · wancat

每日一問:如果世界是個函數

若是每個現在都會對應到唯一的未來,那每個過去也都會導向唯一的現在,因此回推到宇宙的開端,就可以得到一個結論:現在發生的一切都是在大霹靂時就註定了。那我們所謂的自主意識,也全都只是空談。

March 7, 2021 · 1 分鐘 · wancat

每日一問:為什麼新年定在 1/1?

Photo by Aaditya Arora from Pexels 這個標題也許令人誤解,1/1 本來就是一年的開始啊?當然,但為什麼我們會選擇一個冬天的日子作為一年的開始,而不是某個春天、夏天、秋天的日子呢?陽曆以地球的公轉為依循,地球每繞太陽一圈就是一年,一年之中所謂「特別」的日子,應該會是春分、夏至、秋分、冬至等等,但 1/1 卻不是什麼特別的日子,甚至以季節來說,我們通常會認為 12、1、2 月是冬天,3 月開始才是春天,為什麼會將新年定在一個季節的「中間」呢? 下一個猜想:也許是由於某種歐洲世界的歷史因素?直覺會想到的可能是「耶穌誕生」,畢竟西元的紀年「名義上」就是以耶穌誕生為始(儘管這與史實有些差距),但這也有矛盾之處:我們之所以能說「耶穌於 12/25 日出生」,就證明了陽曆是早於耶穌誕生的,否則就應該會是「我們將耶穌誕生之日定為 1/1」。 如果把陽曆跟農曆做比較,就會發現這個巧合更加神奇,農曆是以月亮的公轉作為依循,一年的長度與陽曆不盡相同,然而農曆的「新年」和陽曆定在差不多的時間,都是在冬天的中間,但是農曆的新年是有天文意義的,新年之時月亮是朔月,但一年有 12 個月,為什麼不定在其他的月份?不同文化將「新年」這個概念定在差不多的時候,這背後有什麼關聯嗎? 我想到的可能性是農業社會的生活型態,在較高緯度的農業社會中,冬天是休耕期,一年到頭的辛勤的工作,在冬天時應當能稍微喘息,而歷經了作物一年的生長與收割,在這個時候特別會有「結束」之感,而實務上來說,若要「慶祝新年」,選擇在冬天的中間進行也很合理,因為工作量比較少,慶祝完後就可以迎接春天的到來,因此自然而然成為了北半球新年的「熱門選擇」。由此推廣,南半球的文明是否也會選擇當地冬天轉春天的時候作為新年呢? 知道為什麼新年要定在冬天了,但是為什麼恰好定在現在的 1/1,而不是早一天或晚一天呢?這聽起來或許強詞奪理,但我們要意識到,人類社會要取得廣泛的共識是很難的,通常能夠廣泛被認可的制度都來自某些自然觀察,例如十進制來自於人類有十根手指、一週七天來自地球上可以肉眼觀察到的「特殊星球」有七顆,否則就是出於歷史因素,並由強勢文化擴散到世界各地。 現代公曆的歷史 根據維基百科,我們現今所使用的公曆——格里曆,是在公元 1582 年由義大利醫生阿洛伊修斯.里利烏斯改良儒略曆所制定、並由教皇所頒布的。其改良內容主要是對閏年規則的修正,沒有改變新年的時間點,因此再往上追,儒略曆是羅馬的羅馬共和國獨裁官儒略.凱撒採納希臘數學家索西琴尼所計算出的曆法,在那之前羅馬的曆法相當的混亂,與太陽的公轉週期已經相差甚遠,而 1/1 則是定在凱撒執政官上任之日。 我們發現這跟先前的推測相當接近,1/1 的制定是出自歷史因素,且格里曆其實是相當晚期才被全世界採用,日本於 1873 年採用格里曆,而中國則是在中華民國於 1912 年成立後開始採用格里曆,在這個時機做出改變,明顯是來自歐洲強勢文化的影響,這也驗證了我們先前的猜想。 現代的不同曆法 其實在現代,也有國家是採取不同的曆法,例如伊朗所使用的伊朗曆,就是以春分作為一年開始的太陽曆(取自天文觀察);或是許多伊斯蘭國家所使用的伊斯蘭曆,跟中國傳統的農曆一樣是以月亮的公轉作為依循,並將新月定為一個月的開始(農曆是朔)。 除此之外,現代也有對曆法改革的提議,對紀年的改革有學者塞薩爾.埃米利安尼提出的人類紀年(又稱全新世紀年),將現有的年加上 10000 作為新的紀年,例如 2021 就會變成 12021,選擇這個時間點,是因為人類紀年的元年是最後一次冰河期的結束,也是新石器時代的開端。此外這種曆法包含了 0 年,讓數軸變得完整。 對紀月的改革有企業家喬治.伊士曼所推行的國際固定曆,將每個月定為 4 個星期 28 天,一年 13 個月,這樣只有 364 天,因此再添加最後一天「年日」作為一年的結束,年日不屬於星期中的任何一天,國際固定曆的特色是日期將始終落在一週中的同一天,且非常易於計算(除以 7 的餘數)。 這些提案雖然到現在仍處於討論階段,尚未獲得共識,但打破了我過去對曆法「本來就是這樣」的想法。曆法是一種制度,是由人所制定的,過去曾經歷多次的曆法改革,現今也有不同國家使用著不同的曆法,這些都告訴我們即使像曆法這般有如文化根基的制度,也是可以被改變的。如果是你,會期待什麼樣的曆法呢? 參考資料 曆法 格里曆 儒略曆 伊朗曆 伊斯蘭曆 國際固定曆 人類紀年(全新世紀年)

January 23, 2021 · 1 分鐘 · wancat