The Artima Developer Community
Sponsored Link

Ruby Buzz Forum
BNL: The Solution - Update

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Jay Fields

Posts: 765
Nickname: jayfields
Registered: Sep, 2006

Jay Fields is a software developer for ThoughtWorks
BNL: The Solution - Update Posted: Oct 16, 2006 4:54 PM
Reply to this message Reply

This post originated from an RSS feed registered with Ruby Buzz by Jay Fields.
Original Post: BNL: The Solution - Update
Feed Title: Jay Fields Thoughts
Feed URL: http://blog.jayfields.com/rss.xml
Feed Description: Thoughts on Software Development
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by Jay Fields
Latest Posts From Jay Fields Thoughts

Advertisement
Business Natural Languages: Introduction
Business Natural Languages: The Problem
Business Natural Languages: The Solution

The Solution
A Business Natural Language is used to specify application behavior. The language should be comprised of descriptive and maintainable phrases. The structure of the language should be simple, yet verbose. Imagine each line of the specification as one complete sentence. Because a Business Natural Language is natural language it is important to limit the scope of the problem being solved. An application's requirements can often be split to categories that specify similar functionality. Each category can be a candidate for a simple Business Specific Language. Lucky for us, our current application has a very limited scope: calculating compensation. Therefore, our application will only require one Business Natural Language.

We've already seen the requirements:

employee John Jones
compensate $2500 for each deal closed in the past 30 days
compensate $500 for each active deal that closed more than 365 days ago
compensate 5% of gross profits if gross profits are greater than $1,000,000
compensate 3% of gross profits if gross profits are greater than $2,000,000
compensate 1% of gross profits if gross profits are greater than $3,000,000

A primary driver for using a Business Natural Language is the ability to execute the requirements as code. Lets change the existing application to work with a Business Natural Language.

To start with we can take the requirements and store them in a file named jjones.bnl. Now that we have our Business Natural Language file in our application we'll need to alter the existing code to read it. The first step is changing process_payroll.rb to search for all Business Natural Language files, create a vocabulary, create a parse tree, and report the results.

File: process_payroll.rb
Dir[File.dirname(__FILE__) + "/*.bnl"].each do |bnl_file|
vocabulary = CompensationVocabulary.new(File.basename(bnl_file, '.bnl'))
compensation = CompensationParser.parse(File.read(bnl_file), vocabulary)
puts "#{compensation.name} compensation: #{compensation.amount}"
end
Creating a vocabulary is nothing more than defining the phrases that the Business Natural Language should understand. Each phrase returns values that are appended together to create valid ruby syntax. The vocabulary also calls out to the SalesInfo class to return sales data for each employee.

File: compensation_vocabulary.rb
class CompensationVocabulary
extend Vocabulary

def initialize(data_for)
@data_for = data_for
end

phrase "active deal that closed more than 365 days ago!" do
SalesInfo.send(@data_for).year_old_deals.to_s
end

phrase "are greater than" do
" > "
end

phrase "deal closed in the past 30 days!" do
SalesInfo.send(@data_for).deals_this_month.to_s
end

phrase "for each" do
"*"
end

phrase "gross profits" do
SalesInfo.send(@data_for).gross_profit.to_s
end

phrase "if" do
" if "
end

phrase "of" do
"*"
end

end
The CompensationVocabulary class does extend Vocabulary, which is how the phrase class method is added to CompensationVocabulary.

File: vocabulary.rb
module Vocabulary

def phrase(name, &block)
define_method :"_#{name.to_s.gsub(" ","_")}", block
end

end
After the vocabulary is defined the CompensationParser class processes the BNL file.

File: compensation_parser.rb
class CompensationParser

class << self
def parse(script, vocabulary)
root = Root.new(vocabulary)
script.split(/\n/).each { |line| root.process(preprocess(line)) }
root
end

def preprocess(line)
line.delete!('$,')
line.gsub!(/(\d+)%/, '\1percent')
line.gsub!(/\s/, '._')
"_#{line.downcase}!"
end
end

end
The CompensationParser class is responsible for parsing the script and converting the BNL syntax into valid ruby. The parse class method of CompensationParser creates a new instance of Root, splits the script, sends the preprocessed line to the process method of the instance of Root, and then returns the instance of Root. The CompensationParser preprocess method removes the superfluous characters, replaces special characters with alphabetic representations, converts the line into a chain of methods being called on the result of the previous method call, and appends underscores and an exclamation point to avoid method collisions and signal the end of a phrase.

For example, when preprocess is called on:
"compensate 5% of gross profits if gross profits are greater than $1,000,000"
it becomes:
"_compensate._5percent._of._gross._profits._if._gross._profits._are._greater._than._1000000!"

The Root class is responsible for processing each line of the Business Natural Language.

File: root.rb
class Root
extend Vocabulary

def initialize(vocabulary)
@compensations = []
@vocabulary = vocabulary
end

def name
@employee.name
end

def amount
@compensations.collect do |compensation|
compensation.amount
end.inject { |x, y| x + y }
end

def process(line)
instance_eval(line)
end

phrase :employee do
@employee = Employee.new
end

phrase :compensate do
@compensations << Compensation.new(@vocabulary)
@compensations.last
end

end
Root is responsible for storing reference to the child objects of the parse tree. The references are maintained via instance variables that are initialized when the methods that correspond to phrases of the language are processed. For example, the 'employee' phrase creates and stores an instance of the Employee class. Root processes each line of the Business Natural Language by passing the line to the instance_eval method.

When the first line is sent to instance_eval, 'employee' returns an instance of the Employee class. The Employee class instance stores each subsequent message in an array and always returns self.

File: employee.rb
class Employee

def initialize
@name_parts = []
end

def method_missing(sym,*args)
@name_parts << sym.to_s.delete('_!')
self
end

def name
@name_parts.collect { |part| part.to_s.capitalize }.join(' ')
end

end
When the second and each following line are sent to instance_eval a Compensation instance is created. The primary task of the instance of Compensation is to build phrases and reduce them to ruby code when possible. Each message sent to compensation from the instance_eval is handled by method_missing. The method_missing method attempts to reduced the phrase instance variable each time method_missing is executed. A phrase can be reduced if the entire phrase is a number or if the vocabulary contains the phrase. If a phrase can be reduced the result is appended to the compensation_logic instance variable and the phrase is reset to empty. If a phrase cannot be reduced it is simply stored awaiting the next call to method_missing. If the phrase contains the end of phrase delimiter (the exclamation point) and it cannot be reduced an exception is thrown.

File: compensation.rb
class Compensation

def initialize(vocabulary)
@phrase, @compensation_logic = '', ''
@vocabulary = vocabulary
end

def method_missing(sym, *args)
@phrase = reduce(@phrase + sym.to_s)
if @phrase.any? && sym.to_s =~ /!$/
raise NoMethodError.new("#{@phrase} not found")
end
self
end

def reduce(phrase)
case
when phrase =~ /^_\d+[(percent)|!]*$/
append(extract_number(phrase))
when @vocabulary.respond_to?(phrase)
append(@vocabulary.send(phrase))
else phrase
end
end

def append(piece)
@compensation_logic += piece
""
end

def extract_number(string)
string.gsub(/(\d+)percent$/, '0.0\1').delete('_!')
end

def amount
instance_eval(@compensation_logic) || 0
end

end
After making all of the above changes a quick run of payroll_process.rb produces the following output:

Jackie Johnson compensation: 256800.0
John Jones compensation: 88500.0

We still have a lot of work to do, but we've proven our concept. Our application behavior is entirely dependent on the compensation specifications that can be altered by our subject matter experts.

In the upcoming chapters we will discuss moving our application to the web, putting the specifications in a database instead of using flat files, providing syntax checking and contextual grammar recommendations, and many other concepts that will take us through developing a realistic Business Natural Language application.

Read: BNL: The Solution - Update

Topic: Signals & Slots in Ruby Previous Topic   Next Topic Topic: Rails applciation caching with memcache

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use