我可以在运行时从 Haskell 程序中反映消息吗?
Can I reflect messages out of a Haskell program at runtime?
我正在编写一个根据许多复杂规则验证复杂数据结构的程序。它输入数据并输出指示数据问题的消息列表。
沿着这些思路思考:
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"
为了文档,我还想编译一个该程序可以输出的所有消息的列表。也就是我要获取的值:
[ Message E 1001 "negative age"
, Message W 1002 "age too large"
, Message E 1003 "empty first name"
, Message E 1004 "empty nickname"
]
我可以将消息从 checkPerson
移到一些外部数据结构中,但我喜欢在使用它们的地方定义消息。
我可以(而且可能应该)在编译时从 AST 中提取消息。
但是 Haskell 吹捧的灵活性让我开始思考:我可以在 运行 时间 达到 吗?也就是我能不能写一个函数
allMessages :: (Person -> Validator ()) -> [Message]
这样 allMessages checkPerson
会给我上面的列表吗?
当然,checkPerson
和Validator
不需要保持不变。
我几乎(不完全)看到我如何制作一个带有“后门”的自定义 Validator
monad,它会 运行 checkPerson
处于某种“反射模式, ” 遍历所有路径并返回遇到的所有 Message
s。我将不得不编写一个自定义 when
函数,它知道在某些情况下(哪些情况?)忽略它的第一个参数。所以,一种DSL。也许我什至可以模拟模式匹配?
所以:我可以做这样的事情吗?怎么做,我必须牺牲什么?
请随时提出任何解决方案,即使它们不完全符合上述描述。
这种半静态分析基本上正是发明箭头的目的。所以让我们做一个箭头!我们的箭头基本上只是一个 Writer
动作,但它会记住它在任何给定时刻可能吐出的消息。首先,一些样板文件:
{-# 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
我希望您同意,此语法与您最初编写的内容 相差无几。让我们看看它在 ghci 中的作用:
> runWriter (runValidator checkPerson (Person (NickName "") Nothing))
((),"empty nickname")
> possibleMessages checkPerson
["empty nickname","empty first name","age too large","negative age"]
我正在编写一个根据许多复杂规则验证复杂数据结构的程序。它输入数据并输出指示数据问题的消息列表。
沿着这些思路思考:
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"
为了文档,我还想编译一个该程序可以输出的所有消息的列表。也就是我要获取的值:
[ Message E 1001 "negative age"
, Message W 1002 "age too large"
, Message E 1003 "empty first name"
, Message E 1004 "empty nickname"
]
我可以将消息从 checkPerson
移到一些外部数据结构中,但我喜欢在使用它们的地方定义消息。
我可以(而且可能应该)在编译时从 AST 中提取消息。
但是 Haskell 吹捧的灵活性让我开始思考:我可以在 运行 时间 达到 吗?也就是我能不能写一个函数
allMessages :: (Person -> Validator ()) -> [Message]
这样 allMessages checkPerson
会给我上面的列表吗?
当然,checkPerson
和Validator
不需要保持不变。
我几乎(不完全)看到我如何制作一个带有“后门”的自定义 Validator
monad,它会 运行 checkPerson
处于某种“反射模式, ” 遍历所有路径并返回遇到的所有 Message
s。我将不得不编写一个自定义 when
函数,它知道在某些情况下(哪些情况?)忽略它的第一个参数。所以,一种DSL。也许我什至可以模拟模式匹配?
所以:我可以做这样的事情吗?怎么做,我必须牺牲什么?
请随时提出任何解决方案,即使它们不完全符合上述描述。
这种半静态分析基本上正是发明箭头的目的。所以让我们做一个箭头!我们的箭头基本上只是一个 Writer
动作,但它会记住它在任何给定时刻可能吐出的消息。首先,一些样板文件:
{-# 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
我希望您同意,此语法与您最初编写的内容 相差无几。让我们看看它在 ghci 中的作用:
> runWriter (runValidator checkPerson (Person (NickName "") Nothing))
((),"empty nickname")
> possibleMessages checkPerson
["empty nickname","empty first name","age too large","negative age"]