Создание DSL "Полунатуральный язык" в Ruby - PullRequest
5 голосов
/ 08 февраля 2010

Я заинтересован в создании DSL на Ruby для анализа обновлений микроблогов. В частности, я подумал, что мог бы перевести текст в строку Ruby так же, как в геме Rails разрешено "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 ]

5 голосов
/ 08 февраля 2010

Правильно ли я вас понимаю: вы хотите взять у пользователя строку и вызвать ее поведение?

Основываясь на двух перечисленных вами примерах, вы, вероятно, сможете обойтись с помощью регулярных выражений.

Например, для разбора этого примера:

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

с рубином:

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 голос
/ 09 февраля 2010

Это перекликается с некоторыми моими мыслями о тангентальном проекте (текст в стиле старого MOO).

Я не уверен, что синтаксический анализатор будет лучшим способом для программы справиться с капризами английского текста. Мои нынешние мысли заставили меня разделить понимание английского языка на отдельные объекты - так, чтобы блок понимал «открытую коробку», но не «нажмите кнопку» и т. Д. - и затем объекты использовали некоторый вид DSL для вызова централизованного кода, который на самом деле делает вещи

Я не уверен, что вы дошли до того, что поняли, как DSL на самом деле поможет вам. Возможно, вам нужно сначала посмотреть, как английский текст превращается в DSL. Я не говорю, что вам не нужен DSL; Вы вполне можете быть правы.

Что касается подсказок о том, как это сделать? Ну, я думаю, что на вашем месте я бы искал конкретные глаголы. Каждый глагол будет «знать», чего ожидать от текста вокруг него. Так что в вашем примере «to» и «from» ожидают, что пользователь сразу же последует.

Это не особенно отличается от кода, который вы разместили здесь, ИМО.

Вы можете получить некоторую оценку, если посмотрите на ответы на мой вопрос . Один комментатор указал мне на образец интерпретатора, который я нашел особенно поучительным: здесь есть хороший пример Ruby .

0 голосов
/ 09 февраля 2010

Опираясь на ответ @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 :)

...