27

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?

3
  • 5
    How much sense does it make to view a functional language from an OOP perspective? Anyway, the real power of pattern matching comes with nested patterns. Just checking the outermost constructor is possible, but by no means the whole story.
    – Ingo
    Commented Mar 3, 2014 at 18:29
  • 1
    This -- 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. -- sounds too dogmatic. Sometimes, you want to separate your ops from your hierarchy: maybe 1) you can't add an op to a hierarchy b/c you don't own the hierarchy; 2) the classes you want to have the op don't match your hierarchy; 3) you could add the op to your hierarchy, but don't want to b/c you don't want to clutter your hierarchy's API with a bunch of crap that most clients don't use.
    – user39685
    Commented Mar 3, 2014 at 20:34
  • 4
    Just to clarify, Some and None aren't types. They're both constructors whose types are forall a. a -> option a and forall a. option a (sorry, not sure what the syntax for type annotations is in F#).
    – user39685
    Commented Mar 6, 2014 at 19:47

4 Answers 4

26

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.

2
  • 3
    Although you're correct, I'd like to add that this isn't so much an issue of OO vs FP as it is an issue of objects vs sum types. OOP's obsession with them aside, there's nothing about objects that makes them un-functional. And if you jump through enough hoops, you can implement sum types in mainstream OOP languages too (though it won't be pretty).
    – Doval
    Commented Mar 3, 2014 at 20:23
  • 1
    "And if you jump through enough hoops, you can implement sum types in mainstream OOP languages too (though it won't be pretty)." -> I guess you'll end up with something similar to how F# sum types are encoded in .NET's type system :)
    – Tarmil
    Commented Mar 4, 2014 at 9:25
8

You have correctly observed that pattern matching (essentially a supercharged switch statement) and dynamic dispatch have similarities. They also coexist in some languages, with a very enjoyable result. However, there are slight differences.

I might use the type system to define a type that can only have a fixed number of subtypes:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

There will never be another subtype of Bool or Option, so subclassing does not appear to be useful (some languages like Scala have a notion of subclassing that can handle this – a class can be marked as “final“ outside of the current compilation unit, but subtypes can be defined inside this compilation unit).

Because the subtypes of a type like Option are now statically known, the compiler can warn if we forget to handle a case in our pattern match. This means that a pattern match is more like a special downcast that forces us to handle all options.

Furthermore, dynamic method dispatch (which is required for OOP) also implies a runtime type check, but of a different kind. It is therefore fairly irrelevant if we do this type check explicitly through a pattern match or implicitly through a method call.

1
  • "This means that a pattern match is more like a special downcast that forces us to handle all options" - in fact, I believe that (as long as you're only matching against constructors and not values or nested structure) it is isomorphic to placing an abstract virtual method in the superclass.
    – Jules
    Commented May 18, 2017 at 19:48
2

F# pattern matching is typically done with a discriminated union rather than with classes (and thus isn't technically a type-check at all). This allows the compiler to give you a warning when you have unaccounted for cases in a pattern-match.

Another thing to note is that in a functional style, you organize things by functionality rather than by data, so pattern matches allow you to gather the different functionality into one place rather than scattered across classes. This also has the advantage that you can see how other cases are handled right next to where you need to make your changes.

Adding a new option then looks like:

  1. Add a new option to your discriminated union
  2. Fix all of the warnings on incomplete pattern matches
2

Partially, you see it more often in functional programming because you use types to make decisions more often. I realize you probably just chose examples more or less at random, but the OOP equivalent to your pattern matching example would more often look like:

if (opt != null)
    opt.Something()
else
    Different()

In other words, it's relatively rare to use polymorphism to avoid routine things like null checks in OOP. Just like an OO programmer doesn't create a null object in every little situation, a functional programmer doesn't always overload a function, especially when you know your list of patterns is guaranteed to be exhaustive. If you use the type system in more situations, you're going to see it used in ways you are not accustomed to.

Conversely, the idiomatic functional programming equivalent to your OOP example would most likely not use pattern matching, but would have fooRoute and barRoute functions that would get passed as arguments to the calling code. If someone used pattern matching in that situation, it would usually be considered wrong, just like someone switching on types would be considered wrong in OOP.

So when is pattern matching considered good functional programming code? When you're doing more than just looking at the types, and when extending the requirements won't require adding more cases. For example, Some val doesn't merely check that opt has type Some, it also binds val to the underlying type for type-safe use on the other side of the ->. You know you'll most likely never have a need for a third case, so it's a good use.

Pattern matching may superficially resemble an object-oriented switch statement, but there is a lot more going on, especially with longer or nested patterns. Make sure you take everything it's doing into account before declaring it equivalent to some poorly-designed OOP code. Often, it is succinctly handling a situation that is not able to be cleanly represented in an inheritance hierarchy.

1
  • I know you know this, and it probably slipped your mind while writing your answer, but note that Some and None aren't types, so you are not pattern matching on types. You pattern-match on constructors of the same type. This is not like asking "instanceof".
    – Andres F.
    Commented Sep 24, 2015 at 14:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.