Programmable Objects
In the last tutorial we created a Fluent Interface for a type Shape consisting only of the method doubleShapeArea and we enforced it by making the constructor Circle and deconstructor (accessor) radius private.
We then implemented a public make method, a generic constructor (factory) that doesn't leak implementation details of Fluent Interface objects (in our case: the radius property of Circle objects).
import Prelude hiding ((.))
x.f = f x
data Shape a = Circle { radius :: a } deriving (Show)
-- show
-- Fluent.hs
class Fluent object where
make :: a -> object(a)
-- Geometry.hs
instance Fluent Shape where
make = Circle
doubleShapeArea(Circle(r)) = Circle(r * sqrt(2)) -- private Fluent Interface
-- Main.hs
circ = make(2) :: Shape(Double)
main = do {
print(circ);
print(circ.doubleShapeArea); -- application of exported Fluent Interface
}We are going to add a second method bind, a generic deconstructor (accessor) of Fluent Interface objects which applies a function that must return a Fluent Interface object. By doing so we make our Fluent Interface programmable without leaking implementation details of objects that implement it.
-- Fluent.hs
class Fluent object where
make :: a -> object(a)
bind :: (a -> object(b)) -> object(a) -> object(b)
-- Geometry.hs
instance Fluent Shape where
make = Circle
bind(remake) = \(Circle(r)) -> remake(r)We can now implement our own doubleShapeArea' in terms of make and bind, with bind as a method that calls a "bindable" function doubleShapeAreaBindable.
import Prelude hiding ((.))
x.f = f x
data Shape a = Circle { radius :: a } deriving (Show)
class Fluent object where
make :: a -> object(a)
bind :: (a -> object(b)) -> object(a) -> object(b)
instance Fluent Shape where
make = Circle
bind(remake) = \(Circle(r)) -> remake(r)
-- show
-- Geometry.hs
doubleShapeArea(Circle(r)) = Circle(r * sqrt(2)) -- private Fluent Interface
-- Main.hs
doubleShapeAreaBindable(r) = make (r * sqrt(2)) -- customized Fluent Interface
doubleShapeArea' = bind(doubleShapeAreaBindable)
circ = make(2) :: Shape(Double)
main = do {
print(circ);
print(circ.doubleShapeArea); -- application of exported Fluent Interface
print(circ.doubleShapeArea'); -- application of programmed Fluent Interface
}As we can see, bind takes care of deconstructing Circle objects and the "bindable" function doubleShapeAreaBindable does the rest, i.e. reconstruct a new Circle object by means of the factory method make.
It is worth noting that a trivial deconstruction-reconstruction "cycle" with bind and make leaves the programmable object unchanged.
import Prelude hiding ((.))
x.f = f x
data Shape a = Circle { radius :: a } deriving (Show)
class Fluent object where
make :: a -> object(a)
bind :: (a -> object(b)) -> object(a) -> object(b)
instance Fluent Shape where
make = Circle
bind(remake) = \(Circle(r)) -> remake(r)
circ = make(2) :: Shape(Double)
-- show
main = do {
print(circ);
print(circ.bind(make))
}In FP lingo, a programmable object is called Monad. Monads are powerful enough to model computations. In the next tutorial we shall see how, and why ultimately a Programmable Fluent Interface is also referred to as Programmable Semicolons and why Haskell's make is in fact called return.