restapi(7)- 談談函數式編程的思維模式和習慣

  • 2019 年 10 月 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!")              }            }          }~

是不是很容易讀懂理解?實際上我們把複雜的細節函數藏在背後。而這些函數是高度可重複利用的,這也是我們在動手之前通盤考慮的成果。

 

Tags: