函數語言程式設計之Currying&partial application

slaveoftime發表於2018-12-23

currying與partial application威力巨大,假設要解決一個領域設計裡的問題:

module rec WeatherDemo =
    type GenerateWeatherInfo = Location -> WeatherInfo

    type Location = string
    type LocationCode = int
    type Temperature = float
    type HtmlString = string
    type WeatherInfo = HtmlString // Html doc
複製程式碼

實際流程的設計可以是:

    type GenerateWeatherInfoFlow =
        GetLocationCode
            -> GetWeather
            -> RenderWeatherInfo
            -> GenerateWeatherInfo

    type GetLocationCode = Location -> LocationCode option
    type GetWeather = LocationCode -> Temperature option
    type RenderWeatherInfo = Location -> Temperature option -> WeatherInfo
複製程式碼

GenerateWeatherInfoFlow其實展開就是:

    type GenerateWeatherInfoFlow =
        (Location -> LocationCode option)
            -> (LocationCode -> Temperature option)  
            -> (Location -> Temperature option -> WeatherInfo)
            -> Location -> WeatherInfo
複製程式碼

但是抽象成不展開的樣子有很多好處,設計領域的時候簡潔清楚,程式碼類似設計文件,而且用領域裡的語言描述,對於新進工程師也方便上手,一看就知道整個流程是什麼樣。

另外測試也方便,以前為了測試,很多依賴注入的東西都需要繁雜的mock,但是現在測試可以如下:

    [<Fact>]
    let ``should generate weather info success`` () =
        generateWeatherInfo
            (fun _ -> Some 1)
            (fun _ -> Some -1.)
            (fun x y -> sprintf "%s %f" x (y |> Option.get))
            "shanghai"
        |> should equal "shanghai -1" 
複製程式碼

主流程的實現可以是:

    let generateWeatherInfo: GenerateWeatherInfoFlow =
        fun getLocationCode
            getWeather
            renderWeatherInfo 
            location -> 
            
            location
            |> getLocationCode
            |> Option.bind getWeather
            |> renderWeatherInfo location
複製程式碼

一切按照定義好的圖紙來,所以簡單得像樂高積木一樣,比如最後的組裝可以是:

    let generateWeatherInfoApi db mojiKey location =
        generateWeatherInfo
            (getLocationFromDb db)
            (getWeatherFromMojitianqi mojiKey)
            (simpleRenderWeatherInfo)
            location
複製程式碼

另外有人可能會疑惑(getLocationFromDb db),不是前面的定義是

type GetLocationCode = Location -> LocationCode option
複製程式碼

麼,沒有db啊,妙的地方就在此:

    let getLocationFromDb db: GetLocationCode =
        fun location ->
            (query {
                for lc in db.LocationCodes do
                where (lc.Location.Contains location)
                select lc.Code
            }).FirstOrDefault()
            |> mapToOption
複製程式碼

各個功能的實現可以是有副作用的如訪問資料庫getLocationFromDb,發http請求 getWeatherFromMojitianqi等,也可以是純函式simpleRenderWeatherInfo,拼接簡單的html字串。我們可以組織好這些功能的實現,實現各種不同的組合形式來滿足專案經理各種無理的需求變更。

當然如果你的流程很複雜,你可以設計很多子流程然後嵌入到主流程裡面,這樣可以減少一個函式需要的引數。

總之,個人經驗感覺currying與partial application是functional programming裡非常炫酷有用的東西,定義簡單,方便測試,單一職責,靈活面對需求的更改。。更多的可以參見一本書“Domain Modeling Made Functional - Tackle Software Complexity with Domain-Driven Design and F#”。

相關文章