Another option is to use existentially quantified data types. Let's take XMonad as an example. There is an (frobby) interface for layouts – LayoutClass typeclass:
-- | Every layout must be an instance of 'LayoutClass', which defines
-- the basic layout operations along with a sensible default for each.
--
-- ...
--
class Show (layout a) => LayoutClass layout a where
...
and existential data type Layout:
-- | An existential type that can hold any object that is in 'Read'
-- and 'LayoutClass'.
data Layout a = forall l. (LayoutClass l a, Read (l a)) => Layout (l a)
that can wrap any (foo or bar) instance of LayoutClass interface. It is itself a layout:
instance LayoutClass Layout Window where
runLayout (Workspace i (Layout l) ms) r = fmap (fmap Layout) `fmap` runLayout (Workspace i l ms) r
doLayout (Layout l) r s = fmap (fmap Layout) `fmap` doLayout l r s
emptyLayout (Layout l) r = fmap (fmap Layout) `fmap` emptyLayout l r
handleMessage (Layout l) = fmap (fmap Layout) . handleMessage l
description (Layout l) = description l
Now it is possible to use Layout data type generically with only LayoutClass interface methods. Appropriate layout which implements LayoutClass interface will be selected at run-time, there is a bunch of them in XMonad.Layout and in xmonad-contrib. And, of course, it is possible to switch between different layouts dynamically:
-- | Set the layout of the currently viewed workspace
setLayout :: Layout Window -> X ()
setLayout l = do
ss@(W.StackSet { W.current = c@(W.Screen { W.workspace = ws })}) <- gets windowset
handleMessage (W.layout ws) (SomeMessage ReleaseResources)
windows $ const $ ss {W.current = c { W.workspace = ws { W.layout = l } } }