用Ruby构建“半自然语言”DSL

时间:2010-02-08 19:06:11

标签: 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.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类的问题。我有以下存根类(删除所有错误检查并用注释替换一些以最小化粘贴):

class Scorekeeper

  attr_accessor :score, :user, :reason, :sender

  def give(num)
    # Can 'give 4' or can 'give a -5'; ensure 'to' called
    self.score = num
    self
  end

  def take(num)
    # ensure negative and 'from' called
    self.score = num < 0 ? num : num * -1
    self
  end

  def plus
    self.score > 0
  end

  def to (str)
    self.user = str
    self
  end

  def from(str)
    self.user = str
    self
  end

  def for(str)
    self.reason = str
    self
  end

  def giver(str)
    self.sender = str
    self
  end

  def command
    str = plus ? "giving @#{user} #{score} points" : "taking #{score * -1} points from @#{user}"
    "@#{sender} is #{str} for #{reason}"
  end

end

运行以下命令:

t = eval('Scorekeeper.new.take(4).from("USER_A").for("not giving me enough points").giver("USER_B")')
p t.command
p t.inspect

产生预期结果:

"@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\">"

所以我的问题主要在于,我是否正在通过构建这种实现来做自己的事情?有没有人有任何改进DSL类本身的例子或任何警告?

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

3 个答案:

答案 0 :(得分:5)

我是否正确理解您:您想从用户那里获取一个字符串并导致它触发某些行为?

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

例如,要解析此示例:

@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:http://treetop.rubyforge.org/

获取字符串并将其转换为代码以使其被唤醒的想法让我感到紧张。使用eval是一个很大的风险,如果可能应该避免使用。还有其他方法可以实现您的目标。如果你愿意,我会很乐意提供一些想法。

关于您建议的DSL的问题:您是否会在应用程序的其他部分本地使用它?或者只是计划将它作为过程的一部分,将字符串转换为您想要的行为?我不确定什么是最好的,如果不知道更多,但如果你只是解析字符串,你可能不需要DSL。

答案 1 :(得分:1)

这与我在tangental项目(旧式文本MOO)上的一些想法相呼应。

我不相信编译器式解析器将成为程序处理英文文本的最佳方式。我目前的想法是让我将对英语的理解分成单独的对象 - 所以一个盒子理解“打开盒子”而不是“按钮”等等 - 然后让对象使用某种DSL来调用集中式代码实际上让事情发生了。

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

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

这与你在这里发布的代码,IMO并没有特别的不同。

你可能会因为my question的答案而感到不快。一位评论者向我指出了解释器模式,我发现它特别具有启发性:有一个很好的Ruby示例here

答案 2 :(得分:0)

在@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-&gt; points”和“points-&gt; name”语法来提取核心信息。它不允许我抓住原因,但这太复杂了,现在我只是存储整个更新,因为除了异常情况之外,整个更新都与每个分数的上下文有关。获得“给予者”用户名也可以在其他地方完成。

我也写了a description of these expressions,希望其他人可能会觉得有用(并且我可以回到它并记住那长串的gobbledygook意味着什么:)。