6

I've read this question about the Abstract factory pattern, but the only answer to it tries to emulate in Haskell what one would in OOP languages (although the forword is along the lines you don't need it in Haskell).

On the other hand, my intention is not really to force an OOP-specific pattern on a Functional language as Haskell. Quite the opposite, I'd like to understand how Haskell addresses the needs that in OOP are addressed via the Factory pattern.

I have the feeling that even these needs might not make sense in Haskell in the first place, but I can't formulate the question any better.

My understanding of the structure of the factory pattern (based on this video which seems pretty clear) is that

  1. there's a bunch of different products, all implementing a common interface
  2. there's a bunch of different creator classes, all implementing a common interface
  • each of the classes in 2 hides a logic to create a product of those in 1
    • (if I understand correctly, it's not necessary that there's a 1-to-1 mapping between objects and creators, as the creators could just hide different logics make different choices about which specific object to create based on user input/time/conditions/whatever.)
  • the client code will have the creators at its disposal, and everytime one of those is used it (the creator, not the client code) will know how to crate the product and which specific product.

How does all (or some) of this apply to Haskell?

Having several different product classes implementing a common product interface is a thing in Haskell, the interface being a typeclass, and the products being types (datas/newtypes/existing types). For instance, with reference to the spaceship-and-asteroids example from the linked video, we could have a typeclass for defining Obstacles anything that provides size, speed, and position,

class Obstacle a where
  size :: a -> Double
  speed :: a -> Int
  position :: a -> (Int,Int)

and Asteroid and Planet could be two concrete types implementing this interface in some way,

data Asteroid = Asteroid { eqside :: Double, pos :: (Int,Int) } deriving Show
instance Obstacle Asteroid where
  size a = eqside a ^ 3 -- yeah, cubic asteroids
  speed _ = 100
  position = pos

data Planet = Planet { radius :: Double, center :: (Int,Int) } deriving Show
instance Obstacle Planet where
  size a = k * radius a ^ 3
    where k = 4.0/3.0*3.14
  speed _ = 10
  position = center

So far I don't see any real difference between what I'd do in Haskell or in a OOP language. But it comes next.

At this point, following the example in the linked video, the client code could be a game that traverses some levels and generates different obstacles based on the number of the level; it could be something like this:

clientCode :: [Int] -> IO ()
clientCode levels = do
  mapM_ (print . makeObstacle) levels

where makeObstacle should be the creator function, or one of several, which given an input of type Int applies a logic to chose if it has to create an Asteroid or a Planet.

However I don't see how I can have a function that returns outputs of different types, Asteroid vs Planet (the fact they implement the same Obstacle interface doesn't seem to help), based on different values all of the same type, [Int], let alone understanding what the "factory" functions and their common interface should be.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • Well the data constructor is already a factory, since you can use `Planet` as a function to create a `Planet`. OO-languages often tend to have difficulty to see the constructor as a function. – Willem Van Onsem Feb 26 '21 at 21:53
  • @WillemVanOnsem, does that mean that in my example `Planet` and `Asteroid` should be two different value constructors of one single `data`type, which I'd call `Obstacle`, and that I should dispense with making an `Obstacle` type`class`? – Enlico Feb 26 '21 at 22:05
  • @Enlico I suspect that is the case, though I am unfamiliar with how OOP programs are structured so I can’t be entirely sure. – bradrn Feb 26 '21 at 23:56
  • @MilovanTomašević, so what? – Enlico Apr 18 '21 at 19:24

2 Answers2

6

Having several different product classes implementing a common product interface is a thing in Haskell, the interface being a typeclass

Not quite. It's true that typeclasses can express what interfaces do in OO languages, but this doesn't always make sense. Specifically, there's not really any point to a class where all methods have type signatures of the form a -> Fubar.

Why? Well, you don't need a class for that – just make it a concrete data type!

data Obstacle = Obstace
  { size :: Double
  , speed :: Int      -- BTW, why would speed be integral??
  , position :: (Int,Int) }

The record fields can also be functions, IO actions, etc. etc. – which is enough to emulate what the methods of an OO class can do. The only thing plain data can't express is inheritance – but even in OO, there's a bit of a mantra about that composition should be preferred over inheritance, so there's that.

Alternatively, you could make Obstacle a sum type

data Obstacle = Asteroid ...
              | Planet ...

It depends on the application which is better. Either way it's still a concrete data type, no class required.

With Obstacle being a simple data type, there's nothing that needs to be “abstract” about it's creators. Instead, you can simply have various functions -> Obstacle that create obstacles which happen to represent either asteroids, planets, or whatever.

You can also wrap your kind of “OO interface instances” into a data type, an existential

{-# LANGUAGE GADTs #-}

class Obstacle a where ...

data AnObstacle where
  AnObstance :: Obstacle a => a -> AnObstacle

But don't do that unless you exactly know this is what you want.

Enlico
  • 23,259
  • 6
  • 48
  • 102
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • So, in a way, subclasses with different implementations of a common interface in OOP languages correspond to objects (of the same type) with different values of their function members. Is this correct? – Enlico Mar 02 '21 at 07:50
  • 1
    Yes – different values, and possibly also different implementations just like in OO: if the fields are functions, they may access persistent values stored in some closure behind the scenes, which really isn't so different from how an OO class can have private member variables. Those can even be `IORef`s to emulate mutability. – leftaroundabout Mar 02 '21 at 08:37
  • Would you say that [the code in this gist](https://gist.github.com/Aster89/b2769814993cec1608e1e6e815b112de) is a good example of how a factory pattern for OS-dependent GUI would translate in Haskell? – Enlico Mar 07 '21 at 11:15
  • @Enlico [commented on Gist] – leftaroundabout Mar 07 '21 at 11:20
6

I would typically reach for a factory in OOP when I need to create values of types that are unknown until runtime.

A traditional example is a dynamic UI: I read in a description of the UI layout, and create various subclasses of some UIComponent base class accordingly—Label, Field, Button, and so on. UIComponent would provide some common interface for rendering, responding to events, and so on.

An abstract factory would then be just a level of indirection atop that: having different types of factories for different platforms (Windows/Mac/Linux), rendering formats (GUI/text/HTML), and the like.

So it looks like you’re starting from a couple of common OOP-centric assumptions about how to model this:

  • That each UI component is implemented as a separate type

  • That types with a common interface or base class should be made instances of a typeclass

Following this line of reasoning would lead you to the implementation described in your question, which is known as the “existential antipattern”:

-- There are some component types:

data Label = …
data TextField = …
data Button = …
data SubmitButton = …
…

-- There’s a common interface for components:

class Component c where
  render :: c -> IO ()
  handleEvent :: c -> Event -> IO ()
  getBounds :: c -> (Word, Word)
  …

-- Each component type implements that interface:

instance Component Label where
  render = renderLabel
  handleEvent _ _ = pure ()
  getBounds (Label text) = computeTextDimensions text
  …

instance Component TextField where …

…

-- Abstract components are created dynamically:

data SomeComponent where
  SomeComponent :: (Component c) => c -> SomeComponent

-- A UI is a collection of abstract components:

type UI = [SomeComponent]

In order to handle the dynamic typing from parsing the UI description, you need the wrapper SomeComponent, which is a pair of a value of some abstract (“existential”) type c, and its instance of Component. This is akin to an OOP vtable. But since the only thing you can do with this value is apply the methods of Component to it, it’s exactly equivalent to just a record of functions:

-- A component is described by just its operations:

data Component = Component
  { render :: IO ()
  , handleEvent :: Event -> IO ()
  , getBounds :: (Word, Word)
  …
  }

-- There are no separate component types, only functions
-- that construct different ‘Component’ values. Fields
-- are just captured variables.

label :: String -> Component
label text = Component
  { render = renderLabel text
  , handleEvent = \ _ _ -> pure ()
  , getBounds = computeTextDimensions text
  …
  }

textField :: … -> Component
button :: … -> Component
…

-- A UI is a collection of components of uniform type:

type UI = [Component]

And this is a direct analogue of an abstract factory: a concrete Component factory is just a function that dynamically builds a Component, and an abstract factory is a function that dynamically builds concrete factories.

-- A common dynamic description of the constructor
-- argument values of a UI element.
data ComponentDescription
  = Label String
  | TextField …
  | Button …
  …

-- Parse such a description from JSON.
instance FromJSON ComponentDescription where …

-- Construct a component from its constructor values.
type ComponentFactory = ComponentDescription -> Component

-- A dynamic description of a platform.
data Platform = Windows | Mac | Linux

-- Construct a component factory for a platform theme.
type AbstractComponentFactory = Platform -> ComponentFactory

The design pattern almost disappears. It’s still present, but it’s just different uses of functions. In a way, this is also reminiscent of an “entity component system” architecture, whereby objects are described as data that represents compositions of behaviors. (It differs in that there’s no “system” here.)

This formulation is mainly useful when you want the set of components to be open; but more commonly, we use closed data modelling in Haskell with sum types by default. In that case, we’d directly use a sum type like the ComponentDescription type above, with suitable fields, as the representation of components.

Adding a new operation on components is easy: just pattern-match on ComponentDescription. Adding a new type of component requires updating all existing functions (unless they have wildcard matches), but in practice it’s often desirable for the compiler to tell you everything that needs to be updated.

Extensibility in both operations and component types is achievable too, but a good description is out of scope for this answer. To learn more, search for the “expression problem”; in particular, tagless final style is considered a good traditional solution in Haskell, which uses typeclasses in a different way.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • so can we say that the key distinction is Haskell's early binding (with both approaches you show) vs OOP Factory's late binding? – Will Ness Feb 28 '21 at 09:38
  • @WillNess: The factories in both examples are late-bound, being arbitrary closures; as for data representations, the sum-type version is early-bound, but while the “scrap your typeclasses” version *looks* early-bound in the *nominal* types, it still has the same late binding as the OOP style when it comes to actual “domain objects” like `Label` & `Button`. Since it fills in a “vtable” dynamically, in a way it’s *more* flexible than the usual OOP approach, namely, selecting from a set of static vtables—which is exactly what the “existential antipattern” is an unwieldy encoding of. – Jon Purdy Mar 01 '21 at 07:04