Answer: Yes!
Well, almost.
In Functional Programming there are several ways to achieve dispatch:
We're only going to look at pattern matching here.
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 :) } }
Underscore matches anything:
object MyShapeApp { def hasCorners(shape: Shape) = shape match { case Circle(_) => false case _ => true } }
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 } }
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 } }
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? } }
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; } }
Doing this in an OO language has several drawbacks:
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.
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]); } }
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);
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; } }
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); } }
Finally, use HasCornersFunctions:
$hasCorners = $shape->callYourFunction(new HasCornersFunctions());
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:
accept)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 :) } }