Reputation: 1728
Consider the following simple example.
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
As you can see the same pattern matching logic is repeated in two functions. If I would use OOP I would create interface IPaymentInstrument
, define two operations:
PrintInstrumentName
and PrintRequisites
and then implement classes - one per payment instrument. To instantiate instrument depending on some external conditions I would use (for example) the factory pattern (PaymentInstrumentFactory
).
If I would need to add a new payment instrument, I just need to add a new class which implements IPaymentInstrument
interface and update factory instantiating logic. Other code that uses these classes remains as is.
But if I use the functional approach I should update each function where pattern matching on this type exists.
If there will be a lot of functions using PaymentInstrument
type that will be a problem.
How to eliminate this problem using functional approach?
Upvotes: 8
Views: 600
Reputation: 1728
Using Mark Seemann's answer I came to such design decision.
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 }
And usage
let card = CreditCard("124", DateTime.Now)
let operations = getTypeOperations card
operations.PrintInstrumentName()
operations.PrintRequisites()
As you can see the getTypeOperations
function executes the role of factory pattern. To aggregate functions in a one bunch I use simple record type (however, according to F# design guidelines http://fsharp.org/specs/component-design-guidelines/ interfaces are preferred to such decision but I am interested to do it in functional approach for now to understand it better).
I got what I wanted - pattern matching is in only one place for now.
Upvotes: 1
Reputation: 233135
As Patryk Ćwiek points out in the comment above, you're encountering the Expression Problem, so you'll have to choose one or the other.
If the ability to add more data types is more important to you than the ability to easily add more behaviour, then an interface-based approach may be more appropriate.
In F#, you can still define object-oriented interfaces:
type IPaymentInstrument =
abstract member PrintInstrumentName : unit -> unit
abstract member PrintRequisites : unit -> unit
You can also create classes that implement this interface. Here's Check
, and I'll leave CreditCard
as an exercise to the reader:
type Check(number : string) =
interface IPaymentInstrument with
member this.PrintInstrumentName () = printfn "check"
member this.PrintRequisites () = printfn "check %s" number
Yet, if you want to go the object-oriented way, you should begin to consider the SOLID principles, one of which is the Interface Segregation Principle (ISP). Once you start applying the ISP aggressively, 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
You can still implement this in classes:
type Check2(number : string) =
interface IPaymentInstrumentNamePrinter with
member this.PrintInstrumentName () = printfn "check"
interface IPaymentInstrumentRequisitePrinter with
member this.PrintRequisites () = printfn "check %s" number
This is beginning to seem slightly ridiculous now. If you're using F#, then why go to all the trouble of defining an interface with a single member?
Why not, instead, use functions?
Both desired interface members have the type unit -> unit
(not a particularly 'functionally' looking type, though), so why not pass such functions around, and dispense with the interface overhead?
With the printInstrumentName
and printRequisites
functions from the OP, you already have the desired behaviour. If you want to turn them into polymorphic 'objects' that 'implement' the desired interface, you can close over them:
let myCheck = Check "1234"
let myNamePrinter () = printInstrumentName myCheck
In Functional Programming, we don't call these things objects, but rather closures. Instead of being data with behaviour, they're behaviour with data.
Upvotes: 17