350 likes | 470 Vues
This document explores the construction of an internal Domain-Specific Language (DSL) in Ruby tailored for simple multiple-choice surveys. It provides an overview of DSL characteristics, including its declarative nature and tight focus on domain-specific tasks. The paper discusses the design strategy for the DSL, incorporating commonalities, variabilities, and Ruby's metaprogramming features. Included are examples demonstrating the creation and execution of survey logic using the DSL, highlighting how Ruby's flexible syntax can facilitate intuitive survey question handling and response collection.
E N D
A Little Language for Surveys: Constructing an Internal DSL in Ruby H. Conrad Cunningham Computer and Information ScienceUniversity of Mississippi
Domain-Specific Language (DSL) • “programming language or executable specification language that offers, through appropriate notations and abstractions, expressive power focused on, and usually restricted to, a particular problem domain” – van Deursen, Klint, and Visser • “little language” – J. Bentley
Typical DSL Characteristics • Not general-purpose (often not Turing-complete) • Small (at least initially) • Declarative • Semantically expressive of domain • Targeted at domain specialists, not necessarily programmers
External or Internal External: different from main programming language • make, yacc, pic, XML-based config files Internal (embedded): constrained use of the main programming language • use of Lisp macros, Haskell algebraic types, metaprogramming in Ruby, etc.
Defining Internal DSLs in Ruby (1 of 2) Flexible syntax enables convenient DSL statements • optional parentheses on method calls • variable number of arguments question “Male or female?” # has optional second arg
Defining Internal DSLs in Ruby (2 of 2) Blocks (closures) provide DSL structuring and delayed execution • anonymous function definitions • passed as arguments question “Male or female?”do response “Male” response “Female”do @fem = true end end
Implementing Internal DSLs in Ruby (1 of 2) Reflexive metaprogramming: writing programs that manipulate themselves as data • obj.instance_eval(str) executes string str as Ruby code in context of object obj • execute DSL statements dynamically • mod.class_eval(str) executes string str as Ruby code in context of module mod • declare new methods and classes dynamically
Implementing Internal DSLs in Ruby (2 of 2) • obj.method_missing(sym,*args) invoked when undefined method sym called with arguments args • take remedial action • obj.send(sym,*args) calls method sym on object obj with arguments args • dynamically “send a message”
Little Language for Surveys Domain: Specification and presentation of simple, multiple-choice surveys Tasks: • Analyze domain for language design • Design Ruby internal DSL • Implement DSL
Commonality/Variability Analysis • Determine domain boundaries (scope) • Define specialized terms and concepts (terminology) • Identify unchanging features among domain members (commonalities) • Identify features that may change among domain members (variabilities)
Scope • Support definition of simple, multiple-choice surveys • specification of survey • presentation of survey and collection of responses • Exclude tabulation of aggregate results for now
Terms and Commonalities • Survey has a title • Survey has a sequence of questions • Question has a sequence of responses • Use of conditional question depends upon previous responses • Response to silent question determined from previous responses • Survey execution presents appropriate questions to respondent and collects choices of responses
Variabilities • Texts for title, questions, and responses • Number and order of questions within survey • Number of responses expected for question • Number and order of responses in question • Condition under which question included • Method for answering silent question • Source of survey specification • Method for displaying questions and collecting responses during execution
DSL Design Strategy • Adopt declarative approach – structure explicit but processing implicit • Use terminology and commonalities to suggest language statements • Represent variabilities as values that survey programmers supply
Survey DSL Example (1 of 3) # C1 survey has title (V1) title“ACMSE Conference Survey” # C2 survey has seq of questions (V2) question“Are you an author?”do # (V3) # C3 question has seq of responses (V4) response "Yes" do # execute if chosen @author = true end response“No” do # execute if chosen @author = false end end
Survey DSL Example (2 of 3) # C4 conditional question (V5) question "What type of paper?" do condition { @author } # when execute? response "Regular" do @p = :rg end response "Student" do @p = :st end response "Work-in-progress" do @p = :wp end # V5/V6 block on response & action sets # state for conditions & silent choices action {@t = 25; @t = 15 if @p == :wp} end
Survey DSL Example (3 of 3) # C5 silent question calculates choice (V6) result "How long is presentation?“do condition { @author } alternative "25" do # when choose? @p == :rp || @p == :sp end alternative“15” do # when choose? @p == :wp end end
Two-Pass Implementation • Parse DSL and build abstract syntax tree (AST) • Execute survey by traversing AST AST DSL parse execute
First Pass: DSL Parsing • Use instance_eval to execute DSL input as Ruby code (V7) • Let Ruby interpreter do most of parsing • Add methods for each DSL statement – title, question, action, etc. • Check specialized syntax and build AST as 3-level tree (survey, question, response) • Defer conditions and actions by storing unevaluated blocks (i.e., closures)
Second Pass: DSL Interpretation • Traverse AST to display questions and collect responses • Execute deferred blocks needed for conditions and actions • Use missing_method and class_eval to create reader/writer methods for variables in blocks
Architecture First pass – use Object Scoping, Context Variable, and Memento patterns for safety and flexibility, Deferred Evaluation for actions Second pass – use Visitor pattern for flexibility (C6, V8)
Object Scoping (1 of 2) class SurveyDSL # METHOD BODIES OMITTED attr_accessor :context def title(text) def question(text,*args) # text, nsel, block def result(text,*args) # text, nsel, block def condition(&check) def action(&action) def response(resp_text,&action) def alternative(alt_text,&guard) end#SurveyDSL class SurveyBuilder < SurveyDSL
Object Scoping (2 of 2) class SurveyBuilder < SurveyDSL . . . def read_DSL(rb_dsl_file) # Omit checks for file existence rb_file = File.new(dsl_file) instance_eval(rb_file.read, dsl_file) # load/eval rb_file.close self end#read_DSL . . . end#SurveyBuilder
Parser Implementation (1 of 3) def title(text) @context.incr_ql_count @context.ql_error = false if @context.level == :survey_level && @context.survey.title == nil @context.survey.title = text.to_s else illegal_stmt_msg(”title") stmt_text_msg("Title",text) if @context.survey.title != nil at_most_one_msg("title","survey") end end self end#title
Parser Implementation (2 of 3) def question(text,*args) # text, nsel, block @context.incr_ql_count @context.ql_error = false if @context.level == :survey_level &&block_given? @context.level = :question_level @context.qtype = :question_type nsel = 1 nsel = args[0].to_i if args.size > 0 @context.question=QuestionNode.new(text.to_s,nsel) yield # execute the block on the question call nresp = @context.question.responses.size if nresp < @context.question.num_to_sel # error messages omitted end
Parser Implementation (3 of 3) if !@context.ql_error @context.survey.add_question(@context.question) else # error messages omitted end @context.level = :survey_level @context.qtype = :no_type else end @context.question = nil self end#question
Did Survey Language Follow Internal DSL Recommendations? JMock designers Freeman and Pryce • Separate syntax and interpretation into layers • Use, and abuse, the host language • Don't trap the user in the internal DSL • Map error reports to the syntax layer
Summary • Illustrated how commonality/variability analysis can be adapted for DSL design • Demonstrated how Ruby facilities can be used for internal DSL development • Explored how design patterns can help lead to safe and flexible DSL processors
Future Work • More systematic techniques to explore domain and discover needed constructs • Improved runtime error handling tied to DSL input • Better facilities for user extension • Investigation and comparison of other languages – Groovy, Scala, Haskell
Acknowledgements • H. C. Cunningham. A little language for surveys: Constructing an internal DSL in Ruby, In Proceedings of the ACM SouthEast Conference, 6 pages, March 2008. • Members of Fall 2006 graduate class on Ruby and Software Development • Suggestions on the paper by Chuck Jenkins, Yi Liu, Pallavi Tadepalli, and Jian Weng