建筑a";“半自然语言”;Ruby中的DSL

建筑a";“半自然语言”;Ruby中的DSL,ruby,regex,parsing,dsl,Ruby,Regex,Parsing,Dsl,我感兴趣的是在Ruby中构建一个用于解析微博更新的DSL。具体来说,我认为我可以将文本转换为Ruby字符串,就像Rails gem允许的“4.days.ago”一样。我已经有了可以翻译文本的正则表达式代码 @USER_A: give X points to @USER_B for accomplishing some task @USER_B: take Y points from @USER_A for not giving me enough points 变成 Scorekeeper.n

我感兴趣的是在Ruby中构建一个用于解析微博更新的DSL。具体来说,我认为我可以将文本转换为Ruby字符串,就像Rails gem允许的“4.days.ago”一样。我已经有了可以翻译文本的正则表达式代码

@USER_A: give X points to @USER_B for accomplishing some task
@USER_B: take Y points from @USER_A for not giving me enough points
变成

Scorekeeper.new.give(x).to("USER_B").for("accomplishing some task").giver("USER_A")
Scorekeeper.new.take(x).from("USER_A").for("not giving me enough points").giver("USER_B")
我可以接受将更新的语法形式化,以便只提供和解析标准化的文本,从而使我能够灵活地处理更新。因此,似乎更多的是如何实现DSL类的问题。我有以下存根类(删除了所有错误检查,并用注释替换了一些以最小化粘贴):

产生预期结果:

"@USER_B is taking 4 points from @USER_A for not giving me enough points"
"#<Scorekeeper:0x100152010 @reason=\"not giving me enough points\", @user=\"USER_A\", @score=4, @sender=\"USER_B\">"
“@USER\u B因为没有给我足够的分数而从@USER\u A处扣了4分”
"#"
因此,我的问题主要是,我是否在这个实现的基础上做了任何事情来打击我自己?是否有人有任何改进DSL类本身的例子或给我任何警告


顺便说一句,为了获取eval字符串,我主要使用sub/gsub和regex,我认为这是最简单的方法,但我可能错了。

我理解正确吗:您想从用户处获取字符串并使其触发某些行为

根据您列出的两个示例,您可能可以使用正则表达式

例如,要分析此示例,请执行以下操作:

@USER_A: give X points to @USER_B for accomplishing some task
使用Ruby:

input = "@abe: give 2 points to @bob for writing clean code"
PATTERN = /^@(.+?): give ([0-9]+) points to @(.+?) for (.+?)$/
input =~ PATTERN
user_a = $~[1] # => "abe"
x      = $~[2] # => "2"
user_b = $~[3] # => "bob"
why    = $~[4] # => "writing clean code"
但是如果有更复杂的情况,在某个时候,您可能会发现使用真正的解析器更容易、更易于维护。如果您想要一个与Ruby配合良好的解析器,我推荐Treetop:

拿一个字符串并将其转换为要求值的代码的想法让我感到紧张。使用eval是一个很大的风险,如果可能的话应该避免。还有其他方法可以实现你的目标。如果你愿意,我很乐意给你一些建议


关于您建议的DSL的一个问题:您是否打算在应用程序的另一部分中以本机方式使用它?或者只是计划在将字符串转换为所需行为的过程中使用它?如果不了解更多,我不确定什么是最好的,但如果您只是解析字符串,可能就不需要DSL。

这与我对一个切线项目(老式文本MOO)的一些想法相呼应

我不认为编译器风格的解析器是程序处理英文文本的最佳方式。我现在的想法是,我把对英语的理解分成不同的对象——一个盒子可以理解“打开的盒子”,但不能理解“按下按钮”,等等——然后让这些对象使用某种DSL来调用集中的代码,从而真正使事情发生

我不确定您是否已经了解DSL实际上将如何帮助您。也许你需要先看看英文文本是如何变成DSL的。我不是说你不需要DSL;你很可能是对的

至于如何做到这一点的提示?好吧,我想如果我是你,我会寻找特定的动词。每个动词都“知道”它应该从周围的文本中期待什么样的东西。因此,在您的示例中,“to”和“from”期望用户立即跟随

依我看,这与你在这里发布的代码并没有什么不同


你可能会从这些问题的答案中得到一些启发。一位评论者向我指出了解释器模式,我发现这一模式特别有启发性:有一个很好的Ruby示例。

基于@David_James的回答,我提出了一个只适用于正则表达式的解决方案,因为我实际上并没有在其他任何地方使用DSL来建立分数,而只是解析给用户的分数。我将使用两种模式进行搜索:

SEARCH_STRING = "@Scorekeeper give a healthy 4 to the great @USER_A for doing something 
really cool.Then give the friendly @USER_B a healthy five points for working on this. 
Then take seven points from the jerk @USER_C."

PATTERN_A = /\b(give|take)[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)[\s\w]*\b(to|from)[\s\w]*@([a-zA-Z0-9_]*)\b/i

PATTERN_B = /\bgive[\s\w]*@([a-zA-Z0-9_]*)\b[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)/i

SEARCH_STRING.scan(PATTERN_A) # => [["give", "4", "to", "USER_A"],
                              #     ["take", "seven", "from", "USER_C"]]
SEARCH_STRING.scan(PATTERN_B) # => [["USER_B", "five"]]
正则表达式可能会被清理一些,但这使我的语法允许使用一些有趣的形容词,同时仍然使用“name->points”和“points->name”语法提取核心信息。它不允许我抓住原因,但这太复杂了,所以现在我只存储整个更新,因为除了异常情况之外,所有更新都将与每个分数的上下文相关。获取“给予者”用户名也可以在其他地方进行


我也写了这篇文章,希望其他人会发现这很有用(这样我就可以回到这篇文章,并记住那长串gobbledygook的意思:)

我应该补充一点,我之所以使用链式方法,只是因为我不知道如何使用像eval这样的简单字符串发送它(“Scorekeeper给用户_A 4表示做某事”),因为我不知道如何将包含空格的字符串放入方法参数列表中。这方面的想法非常受欢迎。我真的只是想想出一个健壮的方法来聪明地解析字符串,比如“Scorekeeper,给@USER_A加Y分,给@USER_B加X分表示你是个混蛋。“从这个字符串中,我需要拉出“+Y->USER_A”和“-X->USER_B表示你是个混蛋”。使用正则表达式变得不合适(也就是说,我会给USER_A X分B/c,我在“points+USER”之前解析了“USER+points”——显然我不是正则表达式大师)。我只是认为探索DSL选项可能是明智的,但由于我没有在其他地方实际使用它,也许健壮的正则表达式是一个更好的选择。我会喜欢一些想法。谢谢。我刚刚在上面的答案中发布了一个正则表达式示例。(我尝试在这里粘贴它,但格式不是很好。)谢谢!这是一个比我干净得多的正则表达式。我试图通过创建DSL来回答的一个问题是如何巧妙地解析该正则表达式和最后一个注释示例。例如,允许在用户“give@bob 4 points”之后加分也应该是有效的“是可选的,等等。但是我现在所拥有的匹配较小的位和匹配完整的语句,正如您所写的,可能允许我更好地匹配多个可能的模式,而不必,比如说,给错误的个人打错误的分数。谢谢。@mettadore我
input = "@abe: give 2 points to @bob for writing clean code"
PATTERN = /^@(.+?): give ([0-9]+) points to @(.+?) for (.+?)$/
input =~ PATTERN
user_a = $~[1] # => "abe"
x      = $~[2] # => "2"
user_b = $~[3] # => "bob"
why    = $~[4] # => "writing clean code"
SEARCH_STRING = "@Scorekeeper give a healthy 4 to the great @USER_A for doing something 
really cool.Then give the friendly @USER_B a healthy five points for working on this. 
Then take seven points from the jerk @USER_C."

PATTERN_A = /\b(give|take)[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)[\s\w]*\b(to|from)[\s\w]*@([a-zA-Z0-9_]*)\b/i

PATTERN_B = /\bgive[\s\w]*@([a-zA-Z0-9_]*)\b[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)/i

SEARCH_STRING.scan(PATTERN_A) # => [["give", "4", "to", "USER_A"],
                              #     ["take", "seven", "from", "USER_C"]]
SEARCH_STRING.scan(PATTERN_B) # => [["USER_B", "five"]]