230 likes | 365 Views
Ruby Metaprogramming. and other languages… . Metaprogramming. Writing programs that write programs (cool!) Often used to create domain-specific languages. Example*. class Module private def readonly (* syms ) # do nothing if no args return if syms.size == 0
E N D
Ruby Metaprogramming and other languages…
Metaprogramming Writing programs that write programs (cool!) Often used to create domain-specific languages
Example* class Module private def readonly(*syms) # do nothing if no args return if syms.size == 0 # code starts as empty string code = "" # generate Ruby code to define # attr reader methods # Notice how the symbol is # interpolated into the code syms.each do |s| code << "def #{s}; @#{s}; end\n" end # use class_eval to create instance # methods class_eval code end # this method is like attr_accessor def readwrite(*syms) return if syms.size == 0 code = "" syms.each do |s| code << "def #{s}; @#{s}; end\n" code << "def #{s}=(value); @#{s} = value; end\n" end class_eval code end end * from The Ruby Programming Language
Quick Exercise Create a small class with a few instance variables Use the code on the previous slide to create “setters” and “getters” Nothing to submit
Domain-Specific Language * from http://en.wikipedia.org/wiki/Domain-specific_language • Programming or specification language dedicated to a particular problem domain • Examples: • html for web pages • VHDL hardware description language • Mathematica for symbolic mathematics • GraphViz for graph layout • YACC for creating parsers • Regular expressions for lexers
Quick Ex Read http://en.wikipedia.org/wiki/Domain-specific_language Read the Usage Patterns. Read the Design Goals. What’s the most interesting example of a DSL?
External DSL Program written in DSL Program that interprets DSL Grammar Interpretation From: http://jroller.com/rolsen/entry/building_a_dsl_in_ruby As defined by Martin Fowler:
Internal DSL • Also from Fowler • Start with some implementation language, such as Ruby • Bend that language into your DSL • THE IDEA: anyone who writes a program in your small domain-specific language is actually writing a bit of Ruby – but without knowing it. • What features of Ruby would make that possible?
A Ruby Example From: http://jroller.com/rolsen/entry/building_a_dsl_in_ruby Quiz program. Example interaction:
The Data (no, program) file question 'Who was the first president of the USA?' wrong 'Fred Flintstone' wrong 'Martha Washington' right 'George Washington' wrong 'George Jetson' question 'Who is buried in Grant\'s tomb?' right 'U. S. Grant' wrong 'Cary Grant' wrong 'Hugh Grant' wrong 'W. T. Grant' question, right, and wrong are methods! Users create their own question BUT, we don’t parse as data… we execute!
First, just read the data def question(text) puts "Just read a question: #{text}" end def right(text) puts "Just read a correct answer: #{text}“ end def wrong(text) puts "Just read an incorrect answer: #{text}" end # The load command executes code! load 'questions.qm'
Result runs question “Who was the first president of the USA?” runs wrong “Fred Flintstone” runs wrong “Martha Washington” runs right “George Washington” Can we do this in Java? Other languages?
Responding to the DSL What should this quiz DSL really do? It doesn’t specify how to run a quiz It does specify the questions and related right/wrong answer – i.e., the program data. So question should create a new entry in the list of questions Then right and wrong are the “values” for that entry
Store all the questions require 'singleton' class Quiz include Singleton # here’s where we’ll store the questions def initialize @questions = [] end # add a question – question+right+wrong def add_question(question) @questions << question end # we sometimes need to update the last question def last_question @questions.last end end # we’ll add more in a minute
Handle one question For now, let’s store it class Question #@text is the question, @answers holds all the answers def initialize( text ) @text = text @answers = [] end def add_answer(answer) @answers << answer end end # we’ll also add more here soon.
What’s an answer? • Need the answer text, also whether it is right or wrong • Note: we’re mapping from two DSL statements (right and wrong) to one data type, with an attribute to distinguish class Answer attr_reader :text, :correct def initialize( text, correct ) @text = text @correct = correct end end
Let’s load the data require 'quiz' def question(text) # Quiz is singleton Quiz.instance.add_questionQuestion.new(text) end def right(text) Quiz.instance.last_question.add_answerAnswer.new(text,true) end def wrong(text) Quiz.instance.last_question.add_answerAnswer.new(text,false) end load 'questions.qm'
Are we loading the data? Add to questions.rb require 'test/unit' class QuizTest < Test::Unit::TestCase def test_load_quiz # ensure that two questions were loaded questions = Quiz.instance.questions assert_equal(2, questions.length) # ensure that the last question has 4 answers question = Quiz.instance.last_question answers = question.answers assert_equal(4, answers.length) # check the text of the answers assert_equal("U. S. Grant", answers[0].text) assert_equal("Cary Grant", answers[1].text) assert_equal("Hugh Grant", answers[2].text) assert_equal("W. T. Grant", answers[3].text) # ensure right/wrong loaded correctly assert_equal(true, answers[0].correct) assert_equal(false, answers[1].correct) assert_equal(false, answers[2].correct) assert_equal(false, answers[3].correct) end end
Now let’s make it a quiz In Quiz: def run_quiz count=0 @questions.each { |q| count += 1 if q.ask } puts "You got #{count} answers correct out of #{@questions.size}." end In Question: def ask puts "" puts "Question: #{@text}" @answers.size.times do |i| puts "#{i+1} - #{@answers[i].text}" end print "Enter answer: " answer = gets.to_i - 1 return @answers[answer].correct end
Run it in questions.rb load 'questions.qm' Quiz.instance.run_quiz
More metaprogramming examples We’ll do more of this after we study FP and Haskell! • Can use Proc to create a proc p = Proc.new {|x,y| print x,y } p.call(1,2) • Can use instance_eval to evaluate Ruby code in the context of a given object. require 'singleton' class TryIt include Singleton def show_what n puts n end end TryIt.instance_eval("TryIt.instance.show_what 22")