Pattern Matching in OOP

Tim McIver

The Question

Can we attain the same level of generalized dispatch on values of a type using an Object-Oriented language that our friends using functional languages enjoy?

Answer: Yes!

Well, almost.

Dispatch in FP

In Functional Programming there are several ways to achieve dispatch:

  • multimethods (C#, Clojure, Elixir)
  • typeclasses (Haskell, Scala via implicits)
  • pattern matching (a.k.a. case analysis) (Haskell, Scala, Clojure, Elixir)

We're only going to look at pattern matching here.

Pattern Matching 101

Given the following Scala code:

case class Point(x: Double, y: Double)

sealed trait Shape
final case class Circle(radius: double) extends Shape
final case class Square(side: double) extends Shape
final case class Triangle(p1: Point, p2: Point, p3: Point) extends Shape

You can use pattern matching to implement the area function:

object Shape {

  def area(shape: Shape) shape match {
    case Circle(r) => Pi * r * r
    case Square(side) => side * side
    case Triangle(p1, p2, p3) => ??? // some calculation involving p1, p2 and p3 :)
  }
}

Pattern Matching 101

Underscore matches anything:

object MyShapeApp {

  def hasCorners(shape: Shape) = shape match {
    case Circle(_) => false
    case _ => true
  }
}

Pattern Matching 101

You can match on type:

object MyShapeApp {

  def circles(shapes: List[Shape]): List[Circle] = shapes match {
    case List(c: Circle, rest) => c :: circles(rest)
    case List(_, rest) => circles(rest)
    case Nil => Nil
  }
}

Pattern Matching 101

It's recursive:

object MyShapeApp {

  def countCircles(shapes: List[Shape]): Int = shapes match {
    case List(Circle(_), rest) => 1 + countCircles(rest)
    case List(_, rest) => countCircles(rest)
    case Nil => 0
  }
}

Pattern Matching 101

And exhaustive. The following gives a warning:

object MyShapeApp {

  def area(shape: Shape) shape match {
    case Circle(r) => Pi * r * r
    case Square(side) => side * side
    // what about Triangle?
  }
}

Dispatch in OOP

Dispatching on Values in PHP

interface Shape {
  function area();
}

class Circle implements Shape {
  private $radius;
  public function area() { return 3.1416 * $this->radius * $this->radius; }
}

class Square implements Shape {
  private $side;
  public function area() { return $this->side * $this->side; }
}

class Triangle implements Shape {
  private $p1, $p2, $p3;
  public function area() { return /* something involving $p1, $p2 and $p3 :) */; }
}

// caller doesn't necessarily know the runtime
// type of $myShape - and shouldn't care.
$myShape->area();

Similarly for the hasCorners method:

interface Shape {
  function area();
  function hasCorners();
}

class Circle implements Shape {
  private $radius;
  public function area() { return 3.1416 * $this->radius * $this->radius; }
  public function hasCorners() { return false; }
}

class Square implements Shape {
  private $side;
  public function area() { return $this->side * $this->side; }
  public function hasCorners() { return true; }
}

class Triangle implements Shape {
  private $p1, $p2, $p3;
  public function area() { return /* something involving $p1, $p2 and $p3 :) */; }
  public function hasCorners() { return true; }
}

But if you could, should you?

Doing this in an OO language has several drawbacks:

  • Requires you to modify the existing classes
  • Adds bloat to those classes
  • Places logic far from the module in which it is used

Use in a Client Application

But what if you can't? How can you implement the hasCorners function if you can't modify the shapes library?

No choice but to resort to using instanceof

class SomeShapeApp {

  function hasCorners($shape) {
    return $shape instanceof Square
        || $shape instanceof Triangle;
  }
}

OK, that's not so bad but if new versions of the shape library add shapes with corners, this code will be broken.

Towards An Answer

Towards An Answer

What if Shape looked like the following?

interface Shape {
  function callYourFunction($functionMap);
}

class Circle implements Shape {
  function callYourFunction($functionMap) {
    return call_user_func($functionMap['Circle'], [$this]);
  }
}

class Square implements Shape {
  function callYourFunction($functionMap) {
    return call_user_func($functionMap['Square'], [$this]);
  }
}

class Triangle implements Shape {
  function callYourFunction($functionMap) {
    return call_user_func($functionMap['Triangle'], [$this]);
  }
}

Towards An Answer (Continued)

Then we define and use a function map:

$hasCornersFunctionMap = [
  'Circle' => function($circle) {
    return false;
  }
  'Square' => function($square) {
    return true;
  }
  'Triangle' => function($triangle) {
    return true;
  }
];

$hasCorners = $shape->callYourFunction($hasCornersFunctionMap);

Towards An Answer (Continued)

Instead of a map, let's use an interface

interface ShapeFunctions {
  function functionForCircle($circle);
  function functionForSquare($square);
  function functionForTriangle($triangle);
}

And implement that interface:

class HasCornersFunctions implements ShapeFunctions {
  function functionForCircle($circle) { return false; }
  function functionForSquare($square) { return true; }
  function functionForTriangle($triangle) { return true; }
}

Towards An Answer (Continued)

And change Shape to the following:

interface Shape {
  function callYourFunction($shapeFunctions);
}

class Circle implements Shape {
  function callYourFunction($shapeFunctions) {
    return $shapeFunctions->functionForCircle($this);
  }
}

class Square implements Shape {
  function callYourFunction($shapeFunctions) {
    return $shapeFunctions->functionForSquare($this);
  }
}

class Triangle implements Shape {
  function callYourFunction($shapeFunctions) {
    return $shapeFunctions->functionForTriangle($this);
  }
}

Towards An Answer (Continued)

Finally, use HasCornersFunctions:

$hasCorners = $shape->callYourFunction(new HasCornersFunctions());

The Answer

This is a Known Pattern

It's the Visitor Pattern!

Usually the names are a little different:

class Element {
  public function accept($visitor);
}

class ConcreteElement1 extends Element {
  public function accept($visitor) {
    return $visitor->visitConcreteElement1($this);
  }
}

class ConcreteElement2 extends Element {
  public function accept($visitor) {
    return $visitor->visitConcreteElement2($this);
  }
}

interface Visitor {
  function visitConcreteElement1($concreteElement1);
  function visitConcreteElement2($concreteElement2);
}

For the case of the shapes library it looks like this:

interface Shape {
  public function accept($visitor);
}

class Circle implements Shape {
  public function accept($visitor) {
    return $visitor->visitCircle($this);
  }
}

class Square implements Shape {
  public function accept($visitor) {
    return $visitor->visitSquare($this);
  }
}

interface ShapeVisitor {
  function visitCircle($circle);
  function visitSquare($square);
}

Now, the area implementation looks like this:

class AreaVisitor implements ShapeVisitor {

  public function visitCircle($circle) {
    return 3.1416 * $circle->getRadius() * $circle->getRadius()
  }

  public function visitSquare($square) {
    return $square->getSide() * $square->getSide();
  }

  public function visitTriangle($triangle) {
    /* The correct implementation */
  }
}

$shapeArea = $shape->accept(new AreaVisitor());

Similarly for hasCorners:

class HasCornersVisitor implements ShapeVisitor {
  public function visitCircle($circle) { return false; }

  public function visitSquare($square) { return true; }

  public function visitTriangle($triangle) { return true; }
}

$hasCorners = $shape->accept(new HasCornersVisitor());

Notice we implmented both area and hasCorners as visitors; Shape has only one public method: accept!

In fact, we could implement all of Shape's behavior as visitors and not add a single additional public method!

This suggests that we have some choices:

  • Implement all functionality using visitors; no public methods (other than accept)
  • Implement all functionality using only public methods; no visitors (has the drawbacks mentioned earlier)
  • Something in between (sweet spot!)

The End?

Normally, that's the end of the story for Visitor Pattern.

But let's try changing the names again:

interface Shape {
  public function match($matcher);
}

class Circle implements Shape {
  public function match($matcher) {
    return $matcher->caseCircle($this);
  }
}

class Square implements Shape {
  public function match($matcher) {
    return $matcher->caseSquare($this);
  }
}

interface ShapeMatcher {
  function caseCircle($circle);
  function caseSquare($square);
}

The area implementation now looks like this:

class AreaMatcher implements ShapeMatcher {

  public function caseCircle($circle) {
    return 3.1416 * $circle->getRadius() * $circle->getRadius()
  }

  public function caseSquare($square) {
    return $square->getSide() * $square->getSide();
  }

  public function caseTriangle($triangle) {
    /* The correct implementation */
  }
}

$shapeArea = $shape->match(new AreaMatcher());

Or using PHP 7's anonymous classes:

$shapeArea = $shape->match(new ShapeMatcher() {
  public function caseCircle($circle) {
    return 3.1416 * $circle->getRadius() * $circle->getRadius() }
  public function caseSquare($square) {
    return $square->getSide() * $square->getSide(); }
  public function caseTriangle($triangle) { /* The correct implementation */ }
  }
);

Compare this to the original Scala version:

object Shape {

  def area(shape: Shape) shape match {
    case Circle(r) =>
      Pi * r * r
    case Square(side) =>
      side * side
    case Triangle(p1, p2, p3) => ??? // some calculation involving p1, p2 and p3 :)
  }
}

Mind, Blown!

mind-blown.gif

Summary

  • You can get very close to FP-like pattern matching in an Object-Oriented language
  • Be nice to your users (you're one of them!) and add support for the Visitor Pattern to your class hierarchies - even if you don't immediately use it.

Questions?

Resources

design-pattern-book.jpg