函数式编程之Currying&partial application

272 阅读2分钟

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#”。