我可以在运行时从Haskell程序中反映消息吗?

我可以在运行时从Haskell程序中反映消息吗?,haskell,reflection,monads,Haskell,Reflection,Monads,我正在编写一个程序,根据许多复杂的规则验证复杂的数据结构。它输入数据并输出指示数据问题的消息列表 按照以下思路思考: 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 Seve

我正在编写一个程序,根据许多复杂的规则验证复杂的数据结构。它输入数据并输出指示数据问题的消息列表

按照以下思路思考:

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"
我可以将消息从
checkPerson
移动到一些外部数据结构中,但我喜欢在使用消息的地方定义消息

我可以(也可能应该)在编译时从AST中提取消息

但是Haskell吹嘘的灵活性让我思考:我能在运行时实现这一点吗?也就是说,我可以写一个函数吗

allMessages :: (Person -> Validator ()) -> [Message]
这样,
allMessages checkPerson
会给我上面的列表吗

当然,
checkPerson
Validator
不必保持不变

我几乎(不太清楚)看到了如何制作一个自定义的
验证器
monad,它带有一个“后门”,以某种“反射模式”运行
checkPerson
,遍历所有路径并返回遇到的所有
消息。我必须编写一个自定义的
when
函数,它知道在某些情况下(哪些情况下?)忽略它的第一个参数。所以,一种DSL。也许我甚至可以模仿模式匹配

那么:我能做这样的事情吗?我要如何做?我要牺牲什么


请随意提出任何解决方案,即使它们不完全符合上述描述。

这种半静态分析基本上就是箭头发明的目的。让我们做一支箭吧!我们的箭头基本上只是一个
编写器
动作,但它能记住在任何给定时刻可能发出的消息。首先,一些样板:

{-# LANGUAGE Arrows #-}

import Control.Arrow
import Control.Category
import Control.Monad.Writer
import Prelude hiding (id, (.))
现在,上面描述的类型:

data Validator m a b = Validator
    { possibleMessages :: [m]
    , action :: Kleisli (Writer m) a b
    }

runValidator :: Validator m a b -> a -> Writer m b
runValidator = runKleisli . action
这里有一些简单的例子。特别有趣的是:两个验证器的组合会记住来自第一个操作和第二个操作的消息

instance Monoid m => Category (Validator m) where
    id = Validator [] id
    Validator ms act . Validator ms' act' = Validator (ms ++ ms') (act . act')

instance Monoid m => Arrow (Validator m) where
    arr f = Validator [] (arr f)
    first (Validator ms act) = Validator ms (first act)

instance Monoid m => ArrowChoice (Validator m) where
    left (Validator ms act) = Validator ms (left act)
所有的魔力都在于操作,它实际上让你报告一些事情:

reportWhen :: Monoid m => m -> (a -> Bool) -> Validator m a ()
reportWhen m f = Validator [m] (Kleisli $ \a -> when (f a) (tell m))
这是一个操作,当您要输出一条可能的消息时,它会注意到,并记录下来。让我们复制您的类型,并演示如何将
checkPerson
编码为箭头。我对您的消息进行了一点简化,但没有什么重要的区别——只是在示例中减少了语法开销

type Message = String
data Name = FullName String String | NickName String -- http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
data Person = Person { name :: Name, age :: Maybe Int }

checkPerson :: Validator Message Person ()
checkPerson = proc person -> do
    case age person of
        Nothing -> returnA -< ()
        Just years -> do
            "negative age"  `reportWhen` (<  0) -< years
            "age too large" `reportWhen` (>200) -< years
    case name person of
        FullName firstName lastName -> do
            "empty first name" `reportWhen` null -< firstName
        NickName nick -> do
            "empty nickname"   `reportWhen` null -< nick

一般来说,这是一个相当棘手的问题,本质上,您希望为您的DSL编写一个静态分析工具。实际上,您可以很容易地使用免费的monad在Haskell中编写这样的DSL,但是执行分析以提取所有可能的消息,但是这将很困难,因为消息的值只能在运行时确定。如果使用简单的求和数据类型限制
标题
s和
代码
s,则会更容易一些,但仍然存在一些问题,即某些值只能由运行时值确定。@bheklillr我希望我的回答会让你大吃一惊。=)@丹尼尔瓦格纳有点,是的!我根本不会想到这种方法。而且,我喜欢你偷偷地把链接带到那里。
type Message = String
data Name = FullName String String | NickName String -- http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
data Person = Person { name :: Name, age :: Maybe Int }

checkPerson :: Validator Message Person ()
checkPerson = proc person -> do
    case age person of
        Nothing -> returnA -< ()
        Just years -> do
            "negative age"  `reportWhen` (<  0) -< years
            "age too large" `reportWhen` (>200) -< years
    case name person of
        FullName firstName lastName -> do
            "empty first name" `reportWhen` null -< firstName
        NickName nick -> do
            "empty nickname"   `reportWhen` null -< nick
> runWriter (runValidator checkPerson (Person (NickName "") Nothing))
((),"empty nickname")
> possibleMessages checkPerson 
["empty nickname","empty first name","age too large","negative age"]