I've been having trouble the past few days figuring out whether something I'm trying to do is actually feasible in Haskell.
Here is some context:
I am trying to code a little markup language (akin to ReST) where the syntax already enables custom extensions through directives.
For users to implement new directives, they should be able to add new semantic constructs inside the document datatype. For exemple if one wants to add a directive for displaying math, they might want to have a MathBlock String
constructor inside the ast.
Obviously data types are not extensible, and a solution where there is a generic constructor DirectiveBlock String
containing the name of the directive (here, "math"
) is undesirable as we would like to have in our ast only well-formed constructs (so only directives with well-formed arguments).
Using type families, I prototyped something like:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
-- Arguments for custom directives.
data family Args :: * -> *
data DocumentBlock
= Paragraph String
| forall a. Block (Args a)
Sure enough, if someone wishes to define a new directive for math display, they can do it as such:
data Math
-- The expected arguments for the math directive.
data instance Args Math = MathArgs String
doc :: [DocumentBlock]
doc =
[ Paragraph "some text"
, Block (MathArgs "x_{n+1} = x_{n} + 3")
]
So far so good, we can only construct documents where directive blocks receive the correct arguments.
The problem arises when one user wants to convert the internal representation of a document to some custom output, say, String. The user needs to provide a default output for all directives, since there will be many and some of them cannot be converted to the target. Furthermore, the user may wish to provide a more specific output for some directives:
class StringWriter a where
write :: Args a -> String
-- User defined generic conversion for all directives.
instance StringWriter a where
write _ = "Directive"
-- Custom way of showing the math directive.
instance StringWriter Math where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- Then to display a DocumentBlock
writeBlock :: DocumentBlock -> String
writeBlock (Paragraph t) = "Paragraph(" ++ t ++ ")"
writeBlock (Block args) = write args
main :: IO ()
main = putStrLn $ writeBlock (Block (MathArgs "a + b"))
With this example, the output is Block
and not Math(a+b)
, so the generic instance for StringWriter is always chosen. Even when playing with {-# OVERLAPPABLE #-}
, nothing succeeds.
Is the kind of behavior I'm describing possible at all in Haskell?
When trying to include a generic Writer inside the Block
definition, it also fails to compile.
-- ...
class Writer a o where
write :: Args a -> o
data DocumentBlock
= Paragraph String
| forall a o. Writer a o => Block (Args a)
instance {-# OVERLAPPABLE #-} Writer a String where
write _ = "Directive"
instance {-# OVERLAPS #-} Writer Math String where
write (MathArgs raw) = "Math(" ++ raw ++ ")"
-- ...