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?
Repeating patterns are patterns where a group of elements repeat themselves as the pattern extends. Some examples of repeating patterns are: ABABABAB… AABAABAABAABAAB… ABCCABCC… As we consider these patterns we need to present them in a variety of different modes.
A repeat is an expression that is repeated an arbitrary number of times. An expression followed by '*' can be repeated any number of times, including zero. An expression followed by '+' can be repeated any number of times, but at least once.
A regular expression is a pattern of text that consists of ordinary characters, for example, letters a through z, and special characters. Character(s) Matches in searched string.
* - means "0 or more instances of the preceding regex token"
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With