Imperative OOP Ceremony: Fluent Interface Pt. 1

As of March 2020, School of Haskell has been switched to read-only mode.

Method Chaining

A Fluent Interface is a chain of object-modifications that return the modified object on each chained modification.

import Prelude hiding ((.))
x.f = f x

data Shape = Circle { radius :: Float } deriving (Show)
doubleShapeArea(Circle r) = Circle(r * sqrt(2))
area(Circle r) = r^2 * pi
circ = Circle(2)

main = do {
  print(circ);
  print(circ.doubleShapeArea);
  print(circ.doubleShapeArea.doubleShapeArea);

  print(circ.area);
  print(circ.doubleShapeArea.area);
  print(circ.doubleShapeArea.doubleShapeArea.area);
}

As we can see, doubleShapeArea can be chained as often as we want because it returns a (modified) Shape object.

Private Constructors, Private Accessors

In order to enforce our Fluent Interface, we make the constructor Circle and the "deconstructor" radius private. Otherwise anyone who imports our module could write their own doubleShapeArea.

-- Geometry.hs
module Geometry (Shape, doubleShapeArea, circ) where

data Shape = Circle { radius :: Float }
circ = Circle(2)
doubleShapeArea(Circle r) = Circle(r * sqrt(2))

-- Main.hs
module Main where

import Geometry -- without Circle, but with circ

main = do {
  print(circ);
  print(circ.doubleShapeArea);
}

In the export list of our module Geometry we have included only Shape but not Circle and radius, thereby making Shape public, leaving Circle and radius private. A user of Geometry now relies on imported Circle objects like circ and methods like doubleShapeArea. In other words, we have successfully enforced our Fluent Interface, but our module has become very inflexible, as it doesn't allow for constructing Circle objects.

Factories

We can easily implement and export a factory method makeCircle which constructs a Circle object but since it's technically not a constructor, it can't be used in pattern matching in order to access radius. In return we get a more flexible module with a Fluent Interface that is still enforced.

-- Geometry.hs
module Geometry (Shape, doubleShapeArea, makeCircle) where

data Shape = Circle { radius :: Float }
makeCircle = Circle
doubleShapeArea(Circle r) = Circle(r * sqrt(2))

-- Main.hs
module Main where

import Geometry -- without Circle, but with makeCircle
circ = makeCircle(2)

main = do {
  print(circ);
  print(circ.doubleShapeArea);
}

A Fluent Interface

With the Factory Pattern in place, we have implemented a Fluent Interface. Let's write an interface (FP lingo: type class) for Fluent Interfaces. Apparently, all we need is a factory method like makeCircle that returns an object. Let's call it make then.

import Prelude hiding ((.))
x.f = f x

doubleShapeArea(Circle(r)) = Circle(r * sqrt(2))
-- show
class Fluent object where
  make :: Float -> object

instance Fluent Shape where
  make = Circle

data Shape = Circle { radius :: Float } deriving (Show)

circ = make(2) :: Shape

main = do {
  print(circ.doubleShapeArea);
}
-- /show

Polymorphism

As we can see, this Fluent interface requires an implementation of a make method that takes a Float as an argument. We can get to a more generic solution with an additional type variable a instead of Float. (object is a type variable as well.) By doing so, we make Shape polymorphic, (Now we can instantiate Circle objects that have a Double precision radius.)

import Prelude hiding ((.))
x.f = f x

doubleShapeArea(Circle(r)) = Circle(r * sqrt(2))
-- show
class Fluent object where
  make :: a -> object(a)

instance Fluent Shape where
  make = Circle

data Shape a = Circle { radius :: a } deriving (Show)

circ = make(2) :: Shape(Double)

main = do {
  print(circ.doubleShapeArea);
}
-- /show

In the next tutorial we shall add even more flexibility to Fluent Interfaces by making them programmable.