restapi(7)- 談談函數語言程式設計的思維模式和習慣
國慶前,參與了一個c# .net 專案,真正重新體驗了一把搬磚感覺:在一個多月時間好像不加任何思考,不斷敲鍵盤加程式碼。我想,這也許是行業內大部分中小型公司程式猿的真實寫照:都是坐在電腦前的搬磚工人。不過也不是沒有任何收穫,在搬磚的過程中我似乎發現了一些現象和造成這些現象背後的原因及OOP思維、習慣模式。和大部分IT公司一樣,這間公司在行業裡存在了一定時間(不是初創)所以在產品和技術方面有一定的積累,通俗點就是一堆現成的c# .net 程式碼。然後就是專案截止日期壓力。為了按時完成任務的我只能在原有程式碼基礎上不斷加功能,根本沒有機會去考慮用什麼樣的程式碼模式、結構去達到更好的效果。在這個過程中有個有趣的現象引起了我的注意:基本上我只需按照某種流程(多數是業務需求)一個個增加環節就可以實現一項完整功能,當然我是不會計較這些環節對軟體其它部分是否產生影響,又或者以後程式碼維護會不會很麻煩,只要能及時交貨就行。想想這種做法恰恰是面向物件程式設計或所謂行令式程式設計的特點,即:通過逐行執行命令載入程式的狀態改變,最終狀態就是執行程式的結果了,或者就是功能的實現了。通過一行行增加程式碼最終總會到達預期的狀態,不是嗎。這正是OO程式設計的思維模式:因為程式狀態體現在每行程式碼上,隨時可以檢查,驗證思路,所以OOP比較容易上手(相對函數語言程式設計而言)。
回顧一下函數語言程式設計:好像很難按照自然邏輯思維順序來實現一個功能,這是因為函數語言程式設計是一種巢狀式間接性的程式設計模式,即程式是在某種巢狀裡執行的。函數語言程式設計又被稱為monatic-programming,即在monad裡程式設計。monad就是我所說的巢狀,是一種型別結構,最常用的是Future型別。在現代程式設計裡多執行緒程式設計非常普遍,實際上往往我們離不開各種各樣的Future。舉個形象的例子:如果實現把髒水從A點引到B點輸出純淨水作為某種函式式程式,程式設計如同搭建管道網。必須首先準備好各式各樣功能的喉管,實現每種喉管的特殊功能如過濾、消毒等,然後再連線組合形成送水管道。
我在進行函數語言程式設計時總是要把所以問題前前後後都考慮清楚了才能開始動手。首先會把一項功能的所有環節先總結出來,這些都是一些函式。然後嘗試把這些函式的型別統一了,就像上面提到的喉管一樣,因為不同規格的喉管是無法連線的。同樣,不同型別的巢狀monad是無法實現函式組合的。然後先根據需求實現這些函式的輸入輸出,最後把這些函式組合起來形成完整功能。你看,在函數語言程式設計裡是無法做到隨意想到那就寫到那的,必須先進行整體的思量。所以,函數語言程式設計在程式碼重用和維護上有先天的優勢。這個例子也體現了函數語言程式設計的思維模式。
下面我想用一個實際的例子來示範函數語言程式設計模式:前面幾篇討論的例子裡有一個是把前端httpclient上傳httpserver的圖片存放入伺服器端mongodb資料庫的。現在發現客戶端上傳圖片資料流有困難,希望上傳一個圖片下載網址,由httpserver自行下載圖片並寫入mongodb。單從這個功能來講,應該由幾個環節組成:
1、從上傳的資料中抽出圖片下載網址
2、下載圖片,通過http的request請求,從response裡獲取圖片資料流
3、通過mongodb的count功能獲取圖片系列序號
4、將圖片寫入mongodb
首先,我需要把這幾個環節形成函式,然後統一函式型別。無可爭議,最好選擇Future[A]這樣的函式返回型別:
假設資料是用json格式傳上來的,那得有個型別作為資料結構:
case class UpData (pid: String, url: String)
可以如下獲取上傳的資料:
entity(as[String]) { json => val upData: UpData = fromJson[UpData](json) ... }
獲取圖片系列序號:返回Future[Long]
repository.count(upData.pid).toFuture[Long]
下載圖片:這個返回Future[ByteString]
import akka.actor.ActorSystem import akka.http.scaladsl.model._ import akka.http.scaladsl.Http def downloadPicture(url: String)(implicit sys: ActorSystem): Future[ByteString] = { val dlRequest = HttpRequest(HttpMethods.GET, uri = url) Http(sys).singleRequest(dlRequest).flatMap { case HttpResponse(StatusCodes.OK, _, entity, _) => entity.dataBytes.runFold(ByteString()) { case (hd, bs) => hd ++ bs } case _ => Future.failed(new RuntimeException("failed getting picture!")) } }
寫入mongodb:這個函式也返回Future[?]
def addPicuture(pid: String,seqno: Int, optDesc: Option[String] ,optWid:Option[Int],optHgh:Option[Int], bytes: Array[Byte]):Future[Completed] ={ var doc = Document( "pid" -> pid, "seqno" -> seqno, "pic" -> bytes ) if (optDesc != None) doc = doc + ("desc" -> optDesc.get) if (optWid != None) doc = doc + ("width" -> optWid.get) if (optHgh != None) doc = doc + ("height" -> optHgh.get) repository.insert(doc).toFuture[Completed] }
好了,現在這幾個函式都是Future型別的,可以進行組合了:
val futSeqno: Future[Long] = for { cnt <- repository.count(upData.pid).toFuture[Long] barr <- downloadPicture(upData.url) _ <- addPicuture(upData.pid, cnt.toInt, None, None, None, barr.toArray) } yield cnt
futSeqNo是個組合的運算流程。注意它的型別還是future:意味這我們無法預測這個運算什麼時候會完成,特別如果下載一張超大圖片又或者網速緩慢的話,很可能在下載完成之前就執行了complete()。所以我們必須保證圖片下載完成後才向終端httpclient返回response,就用onComplete來實現:
onComplete(futSeqno) { case Success(lv) => complete(lv.toString()) case _ => complete("error saving picture!") }
所以整段巨集觀程式碼如下:
post { entity(as[String]) { json => val upData: UpData = fromJson[UpData](json) val futSeqno: Future[Long] = for { cnt <- repository.count(upData.pid).toFuture[Long] barr <- downloadPicture(upData.url) _ <- addPicuture(upData.pid, cnt.toInt, None, None, None, barr.toArray) } yield cnt onComplete(futSeqno) { case Success(lv) => complete(lv.toString()) case _ => complete("error saving picture!") } } }~
是不是很容易讀懂理解?實際上我們把複雜的細節函式藏在背後。而這些函式是高度可重複利用的,這也是我們在動手之前通盤考慮的成果。
&n