Business Natural Languages: The Problem
Business Natural Languages: The Solution
Business Natural Languages: DRY code, DAMP BNL
Business Natural Langauges: Moving to the web and multiple contexts
Business Natural Languages: Limitations
Business Natural Languages: Common Pieces
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|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.
vocabulary = CompensationVocabulary.new(File.basename(bnl_file, '.bnl'))
compensation = CompensationParser.parse(File.read(bnl_file), vocabulary)
puts "#{compensation.name} compensation: #{compensation.amount}"
end
File: compensation_vocabulary.rb
class CompensationVocabularyThe CompensationVocabulary class does extend Vocabulary, which is how the phrase class method is added to 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
File: vocabulary.rb
module VocabularyAfter the vocabulary is defined the CompensationParser class processes the BNL file.
def phrase(name, &block)
define_method :"_#{name.to_s.gsub(" ","_")}", block
end
end
File: compensation_parser.rb
class CompensationParserThe 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.
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
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 RootRoot 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.
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
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 EmployeeWhen 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.
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
File: compensation.rb
class CompensationAfter making all of the above changes a quick run of payroll_process.rb produces the following output:
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
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.
Ah-ha!
ReplyDeleteI've tried for so long to avoid any pre-processing, but as you've highlighted here it's a necessity if you want to deal with numbers :(