vendredi 26 juin 2015

Can I reflect messages out of a Haskell program at runtime?

I’m writing a program that validates a complex data structure according to a number of complex rules. It inputs the data and outputs a list of messages indicating problems with the data.

Think along these lines:

import Control.Monad (when)
import Control.Monad.Writer (Writer, tell)

data Name = FullName String String | NickName String
data Person = Person { name :: Name, age :: Maybe Int }

data Severity = E | W | C   -- error/warning/comment
data Message = Message { severity :: Severity, code :: Int, title :: String }
type Validator = Writer [Message]

report :: Severity -> Int -> String -> Validator ()
report s c d = tell [Message s c d]

checkPerson :: Person -> Validator ()
checkPerson person = do
  case age person of
    Nothing -> return ()
    Just years -> do
      when (years < 0) $ report E 1001 "negative age"
      when (years > 200) $ report W 1002 "age too large"
  case name person of
    FullName firstName lastName -> do
      when (null firstName) $ report E 1003 "empty first name"
    NickName nick -> do
      when (null nick) $ report E 1004 "empty nickname"

For documentation, I also want to compile a list of all messages this program can output. That is, I want to obtain the value:

[ Message E 1001 "negative age"
, Message W 1002 "age too large"
, Message E 1003 "empty first name"
, Message E 1004 "empty nickname"
]

I could move the messages out of checkPerson into some external data structure, but I like it when the messages are defined right at the spot where they are used.

I could (and probably should) extract the messages from the AST at compile time.

But the touted flexibility of Haskell made me thinking: can I achieve that at runtime? That is, can I write a function

allMessages :: (Person -> Validator ()) -> [Message]

such that allMessages checkPerson would give me the above list?

Of course, checkPerson and Validator need not stay the same.

I can almost (not quite) see how I could make a custom Validator monad with a “backdoor” that would run checkPerson in a sort of “reflection mode,” traversing all paths and returning all Messages encountered. I would have to write a custom when function that would know to ignore its first argument under some circumstances (which ones?). So, a kind of a DSL. Perhaps I could even emulate pattern matching?

So: can I do something like this, how, and what would I have to sacrifice?

Please feel free to suggest any solutions even if they do not exactly fit the above description.





Aucun commentaire:

Enregistrer un commentaire