可重复的模式匹配

时间:2015-10-23 08:57:46

标签: f# functional-programming

考虑以下简单示例。

type PaymentInstrument =
    | Check of string
    | CreditCard of string * DateTime

let printInstrumentName instrument = 
    match instrument with 
    | Check number-> printfn "check"
    | CreditCard (number, expirationDate) -> printfn "card"

let printRequisites instrument =
    match instrument with 
    | Check number -> printfn "check %s" number
    | CreditCard (number, expirationDate) -> printfn "card %s %A" number expirationDate

正如您所看到的,在两个函数中重复相同的模式匹配逻辑。如果我使用OOP,我会创建接口IPaymentInstrument,定义两个操作:

PrintInstrumentNamePrintRequisites

然后实现类 - 每个支付工具一个。要根据某些外部条件实例化仪器,我会使用(例如)工厂模式(PaymentInstrumentFactory)。

如果我需要添加新的支付工具,我只需要添加一个实现IPaymentInstrument接口的新类并更新工厂实例化逻辑。使用这些类的其他代码保持不变。

但是如果我使用函数方法,我应该更新每个函数,其中存在此​​类型的模式匹配。

如果有很多使用PaymentInstrument类型的函数会出现问题。

如何使用功能方法消除此问题?

2 个答案:

答案 0 :(得分:17)

正如PatrykĆwiek在the comment above中指出的那样,你遇到Expression Problem,所以你必须选择其中一个。

如果添加更多数据类型的能力比轻松添加更多行为的能力更重要,那么基于接口的方法可能更合适。

在F#中,您仍然可以定义面向对象的接口:

type IPaymentInstrument =
    abstract member PrintInstrumentName : unit -> unit
    abstract member PrintRequisites : unit -> unit

您还可以创建实现此接口的类。这是Check,我会将CreditCard作为练习留给读者:

type Check(number : string) =
    interface IPaymentInstrument with
        member this.PrintInstrumentName () = printfn "check"
        member this.PrintRequisites () = printfn "check %s" number

然而,如果你想采用面向对象的方式,你应该开始考虑SOLID principles,其中一个是Interface Segregation Principle(ISP)。一旦您开始积极地应用ISP,you'll ultimately end up with interfaces with a single member, like this

type IPaymentInstrumentNamePrinter =
    abstract member PrintInstrumentName : unit -> unit

type IPaymentInstrumentRequisitePrinter =
    abstract member PrintRequisites : unit -> unit

您仍然可以在课程中实现此功能:

type Check2(number : string) =
    interface IPaymentInstrumentNamePrinter with
        member this.PrintInstrumentName () = printfn "check"
    interface IPaymentInstrumentRequisitePrinter with
        member this.PrintRequisites () = printfn "check %s" number

现在开始显得有些荒谬了。如果你正在使用F#,那么为什么要解决与单个成员定义接口的所有麻烦?

为什么不使用功能

两个所需的接口成员都具有类型unit -> unit(虽然不是特别'功能'看起来类型),所以为什么不传递这些函数,并省去接口开销?

使用OP中的printInstrumentNameprintRequisites函数,您已经拥有了所需的行为。如果你想把它们变成“实现”所需界面的多态“对象”,你可以关闭它们:

let myCheck = Check "1234"
let myNamePrinter () = printInstrumentName myCheck

在功能编程中,我们不会将这些内容称为 objects ,而是将闭包。它们不是带有行为的数据,而是带有数据的行为

答案 1 :(得分:1)

使用Mark Seemann的回答我做出了这样的设计决定。

type PaymentInstrument =
    | Check of string
    | CreditCard of string * DateTime

type Operations =
    { 
      PrintInstrumentName : unit -> unit
      PrintRequisites : unit -> unit
    }

let getTypeOperations instrument =
    match instrument with 
    | Check number-> 
        let printCheckNumber () = printfn "check"
        let printCheckRequisites () = printfn "check %s" number
        { PrintInstrumentName = printCheckNumber; PrintRequisites = printCheckRequisites }
    | CreditCard (number, expirationDate) -> 
        let printCardNumber () = printfn "card"
        let printCardRequisites () = printfn "card %s %A" number expirationDate
        { PrintInstrumentName = printCardNumber; PrintRequisites = printCardRequisites }

使用

let card = CreditCard("124", DateTime.Now)
let operations = getTypeOperations card
operations.PrintInstrumentName()
operations.PrintRequisites()

如您所见,getTypeOperations函数执行工厂模式的角色。为了聚合函数,我使用简单的记录类型(但是,根据F#设计指南http://fsharp.org/specs/component-design-guidelines/接口比这种决定更受欢迎,但我现在有兴趣在功能方法中做到这一点,以便更好地理解它。) / p>

我得到了我想要的东西 - 模式匹配现在只在一个地方。