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);
}
-- /showPolymorphism
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);
}
-- /showIn the next tutorial we shall add even more flexibility to Fluent Interfaces by making them programmable.