Pattern Matching – Is Pattern-Matching Against Types Idiomatic or Poor Design?

%ffunctional programmingobject-orientedpattern matching

It seems like F# code often pattern matches against types. Certainly

match opt with 
| Some val -> Something(val) 
| None -> Different()

seems common.

But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on. To spell it out, in OOP you'd probably prefer to use overloading:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

This is certainly more code. OTOH, it seems to my OOP-y mind to have structural advantages:

  • extension to a new form of T is easy;
  • I don't have to worry about finding duplication of the route-choosing control flow; and
  • route choice is immutable in the sense that once I have a Foo in hand, I need never worry about Bar.Route()'s implementation

Are there advantages to pattern-matching against types that I'm not seeing? Is it considered idiomatic or is it a capability that is not commonly used?

Best Answer

You are correct in that OOP class hierarchies are very closely related to discriminated unions in F# and that pattern matching is very closely related to dynamic type tests. In fact, this is actually how F# compiles discriminated unions to .NET!

Regarding extensibility, there are two sides of the problem:

  • OO lets you add new sub-classes, but makes it hard to add new (virtual) functions
  • FP lets you add new functions, but makes it hard to add new union cases

That said, F# will give you a warning when you miss a case in pattern matching, so adding new union cases is actually not that bad.

Regarding finding duplications in root choosing - F# will give you a warning when you have a match that is duplicate, e.g.:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

The fact that "route choice is immutable" might also be problematic. For example, if you wanted to share the implementation of a function between Foo and Bar cases, but do something else for the Zoo case, you can encode that easily using pattern matching:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

In general, FP is more focused on first designing the types and then adding functions. So it really benefits from the fact that you can fit your types (domain model) in a couple of lines in a single file and then easily add the functions that operate on the domain model.

The two approaches - OO and FP are quite complementary and both have advantages and disadvantages. The tricky thing (coming from the OO perspective) is that F# usually uses the FP style as the default. But if there really is more need for adding new sub-classes, you can always use interfaces. But in most systems, you equally need to add types and functions, so the choice really does not matter that much - and using discriminated unions in F# is nicer.

I'd recommend this great blog series for more information.

Related Topic