问题 如何将指定为免费monad的程序与预期指令的描述进行比较?


所以我正在尝试做一些小说(我想),但我不是 经验丰富的Haskell类型级编程可以自己解决。

我有一个免费的monad描述了要执行的一些效果(一个AST,如果那是 滚动的方式),我想解释它的一些描述 预期的效果。

到目前为止这是我的代码::

{-# LANGUAGE DeriveFunctor, FlexibleInstances, GADTs, FlexibleContexts #-}
import Control.Monad.Free -- from package 'free'

data DSL next
    = Prompt String (String -> next)
    | Display String next
    deriving (Show, Functor)

prompt p = liftF (Prompt p id)
display o = liftF (Display o ())

-- |Just to make sure my stuff works interactively
runIO :: (Free DSL a) -> IO a
runIO (Free (Prompt p cont)) = do
    putStr p
    line <- getLine
    runIO (cont line)
runIO (Free (Display o cont)) = do putStrLn o; runIO cont
runIO (Pure x) = return x

这是“核心”代码。这是一个示例程序:

greet :: (Free DSL ())
greet = do
    name <- prompt "Enter your name: "
    let greeting = "Why hello there, " ++ name ++ "."
    display greeting
    friendName <- prompt "And what is your friend's name? "
    display ("It's good to meet you too, " ++ friendName ++ ".")

为了测试这个程序,我想使用一个函数 runTest :: Free DSL a -> _ -> Maybe a,这应该采取一个程序和“预期效果”的一些规范模糊地像这样:

expect = (
    (Prompt' "Enter your name:", "radix"),
    (Display' "Why hello there, radix.", ()),
    (Prompt' "And what is your friend's name?", "Bob"),
    (Display' "It's good to meet you too, Bob.", ()))

并通过匹配它对下一个项目执行的每个效果来解释程序 expect 名单。然后应该将相关值(每对中的第二项)作为该效果的结果返回给程序。如果所有效果都匹配,则程序的最终结果应作为a返回 Just。如果有什么不匹配, Nothing 应该返回(稍后我将展开它,以便它返回一条信息性的错误消息)。

当然这个 expect 元组是没用的,因为它的类型是一个巨大的东西,我不能写一个泛型 runTest 功能结束。我遇到的主要问题是我应该如何表达这个预期意图序列,我可以编写一个函数,可以对任何程序使用任何序列 Free DSL a

  1. 我隐约知道Haskell中的各种高级类型级功能,但我还没有经验知道应该尝试使用哪些东西。
  2. 我应该使用HList还是其他东西 expected 序列?

我们非常感谢任何有关事物的提示。


3521
2017-09-19 20:55


起源

这好像是 expect 应该是类型类中的函数,以及各种类型 Prompt, Display等,对应于不同的类型类实例。但是,根据上下文,将一个标识符作为执行所有操作的魔术函数并不是一件好事 - 它会使您的代码的读者非常困惑。 - user2407038
你可以忽略了 expect 东西,这不是我的问题的主要部分2.我不知道为什么 expect 会在类型类中,因为我不确定它会有多个实例。我确实有 prompt 和 display 在类型类中,但这是出于与此问题无关的原因4。 expect 特别是在测试代码的上下文中 - 因为它特别是编写测试断言的一种方式,我不认为它不清楚。 - Christopher Armstrong
更新:我正在搞乱 operational 至少允许我以一种很好的方式描述我的数据类型的包,我很高兴...它让我基本上统一了我的 Intent 和 DSL 数据类型。我想我可能会越走越近了。 - Christopher Armstrong
你可能感兴趣 这个帖子,它定义了DSL和解释器数据类型并将这些数据类型配对。我得到的印象是你的主要问题是你期望元组中的第二个元素是变量类型 - 如果你通过将预期效果包装在一个新的数据类型中来解决这个问题(即 data ExpEffect = StrEffect ... | VoidEffect ...)我认为您可以使用任何类型的列表来保存这些值,因为您的语言是线性的。 - Sam van Herwaarden
谢谢你的链接,山姆。实际上,我的问题不仅仅是每个元组中的第二个值 - 第一个也有不同的类型(或者 Intent String 要么 Intent (),取决于使用的构造函数)。我会查看那篇文章,看看我是否可以学到任何东西。 - Christopher Armstrong


答案:


对程序的测试 Free f a 只是该计划的翻译 Free f a -> r 产生一些结果 r

您正在寻找的是为程序构建解释器的简单方法,该程序断言程序的结果是您所期望的。解释器的每一步都要打开一个 Free f 来自程序的指令或描述一些错误。他们会有类型

Free DSL a -> Either String (Free DSL a)
|                    |       ^ the remaining program after this step
|                    ^ a descriptive error
^ the remaining program before this step

我们将对每个构造函数进行测试 DSLprompt' 期待一个 Prompt 具有特定值并为函数提供响应值以查找下一步。

prompt' :: String -> String -> Free DSL a -> Either String (Free DSL a)
prompt' expected response f =
    case f of
        Free (Prompt p cont) | p == expected -> return (cont response)
        otherwise                            -> Left $ "Expected (Prompt " ++ show expected ++ " ...) but got " ++ abbreviate f

abbreviate :: Free DSL a -> String
abbreviate (Free (Prompt  p _)) = "(Free (Prompt "  ++ show p ++ " ...))"
abbreviate (Free (Display p _)) = "(Free (Display " ++ show p ++ " ...))"
abbreviate (Pure _)             = "(Pure ...)"

display' 期待一个 Display 具有特定价值。

display' :: String -> Free DSL a -> Either String (Free DSL a)
display' expected f =
    case f of
        Free (Display p next) | p == expected -> return next
        otherwise                             -> Left $ "Expected (Display " ++ show expected ++ " ...) but got " ++ abbreviate f

pure' 期待一个 Pure 具有特定价值

pure' :: (Eq a, Show a) => a -> Free DSL a -> Either String ()
pure' expected f = 
    case f of
        Pure a | a == expected -> return ()
        otherwise              -> Left $ "Expected " ++ abbreviate' (Pure expected) ++ " but got " ++ abbreviate' f

abbreviate' :: Show a => Free DSL a -> String
abbreviate' (Pure a) = "(Pure " ++ showsPrec 10 a ")"
abbreviate' f        = abbreviate f

prompt' 和 display' 我们可以轻松地建立一个风格的翻译 expect

expect :: Free DSL a -> Either String (Free DSL a)
expect f = return f >>=
           prompt' "Enter your name:" "radix" >>=
           display' "Why hello there, radix." >>=
           prompt' "And what is your friend's name?" "Bob" >>=
           display' "It's good to meet you too, Bob."

运行此测试

main = either putStrLn (putStrLn . const "Passed") $ expect greet

导致失败

Expected (Prompt "Enter your name:" ...) but got (Free (Prompt "Enter your name: " ...))

一旦我们将测试更改为在提示结束时期望空格

expect :: Free DSL a -> Either String (Free DSL a)
expect f = return f >>=
           prompt' "Enter your name: " "radix" >>=
           display' "Why hello there, radix." >>=
           prompt' "And what is your friend's name? " "Bob" >>=
           display' "It's good to meet you too, Bob."

运行它会导致

Passed

11
2017-09-20 17:48



我正在阅读/理解这个答案,但是对于可能感到困惑的旁观者:我已经改变了我的问题以使用Control.Monad.Operational MyEffect a,但之前它使用的是Control.Monad.Free的普通免费monad, Free DSL a,这就是Cirdec的答案所在的原因 Free DSL a。 - Christopher Armstrong
我对你的回答感到非常兴奋,Cirdec!我想我明白了 - 当我真的应该把它作为一个“程序”时,我试图将我的“期望”序列变成一个普通的数据结构!基本上, expect 是特定程序的临时测试解释器。我现在要尝试将其改编为基于运营的程序。 - Christopher Armstrong
我想你忘记了打电话 pure' "blacrg" 在你的 expect 定义到底? - Christopher Armstrong
a有一个潜在的优势 Free 解决方案:有了这个,您可以使用自动生成的方式共享大量代码 Prism来自 lens 库,当你获得更多构造函数时可能会有所帮助(通过一些其他更改为构造函数提供更多类似的结构,我设法写了一个 ex 这样的功能 prompt' = ex _Prompt 和 display' = ex _Display `flip` ())。有了像GADT这样的解决方案 operational,由于存在类型,这失败了。 - Ørjan Johansen
如果你想使用它(或将其编辑成答案),这里是: ex :: (Show e, Eq e) => Prism' (DSL (Free DSL a)) (e, r -> Free DSL a) -> e -> r -> Free DSL a -> Either String (Free DSL a); ex prism expected response f = case preview (_Free.prism) f of Just (p, cont) | p == expected -> return (cont response); otherwise -> Left $ "Expected (" ++ show (review prism (expected, undefined)) ++ ") but got " ++ abbreviate f - Ørjan Johansen