una historia de ds ls en ruby
Post on 20-Oct-2014
380 Views
Preview:
DESCRIPTION
TRANSCRIPT
Leo Soto M.ContinuumLenguajes Dinámicos, Enero 2013
Una historia de DSLs en Ruby
Saturday, February 9, 13
DSLs?
Saturday, February 9, 13
Muchas libs Ruby son en el fondo, DSLs
Saturday, February 9, 13
Capistrano
namespace :deploy do task :start, :roles => :app, :except => { :no_release => true } do run "sudo monit start miapp_unicorn" run "sudo monit -g resque start all" endend
Saturday, February 9, 13
describe Math do describe "#pow" do it "computes the n-th-power of the receiver" do 3.pow(3).should == 27 end endend
RSpec
Saturday, February 9, 13
Bundler
gem 'rails', '3.2.11'gem 'pg'gem 'jquery-rails'gem "unicorn", ">= 4.3.1"gem "haml", ">= 3.1.7"gem "devise", ">= 2.1.2"gem "simple_form", ">= 2.0.4"gem "figaro", ">= 0.5.0"gem "foreigner", ">= 1.3.0"
group :assets do gem 'sass-rails', '~> 3.2.3' gem 'compass-rails' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3'end
Saturday, February 9, 13
Sinatra
require 'sinatra'
get '/hi' do "Hello World!"end
Saturday, February 9, 13
Routing en Rails
Saturday, February 9, 13
Modelos en Rails
Saturday, February 9, 13
Casi todo en Rails
Saturday, February 9, 13
Lo que en J__A “resuelven” con X_Len Ruby se suele hacer con DSLs internos
Saturday, February 9, 13
Enter Pharmmd Rules
Saturday, February 9, 13
age >= 18 and (drug_cat(“XYZ”) or drug_cat(“ABC”))and dosage(“PARACETAMOL”) > 1000
Saturday, February 9, 13
Solución:
Saturday, February 9, 13
# patient_dsl.rbclass PatientDSL
attr_reader :patient
def initialize(patient) @patient = patient end
delegate :age, :allergies, :gender, :labs, :medications, :prescriptions, :providers, :visits, :survey, :to => :patient
def drug_cat(name) (1..6).to_a.reverse.detect do |level| medication = medications.detect do |m| m.send("lvl#{level}conceptname") == name end and break medication end end
def dosage(name) drug(name).try(:dosage) || 0 end
# Many, many more methods
end
Saturday, February 9, 13
PatientDSL.new(patient).instance_eval(rule)
Saturday, February 9, 13
Recuerden, las reglas las ingresas usuarios, en una gran caja de texto en la interfaz web del sistema
¿Que problemas pueden ocurrir con la solución hasta ahora?
Saturday, February 9, 13
age >= 18 and patient.destroy!
Saturday, February 9, 13
Patient.destroy_all
Saturday, February 9, 13
system(“rm -‐rf /”)
Saturday, February 9, 13
PatientDSL.new(patient).instance_eval(rule)
Saturday, February 9, 13
“Cuando me equivoco en tipear la reglame aparece un error ‘No Method no se cuantito’”
Saturday, February 9, 13
Solución, versión 2:
Saturday, February 9, 13
# pharmmd_dsl.treetop
grammar PharmmdDsl rule expression spaces? boolean_expression spaces? end
rule boolean_expression boolean_term (spaces logical_binary_operator spaces boolean_term)* end
rule logical_binary_operator "and" / "&&" / "or" / "||" end
rule boolean_term ("not " / "!") spaces? boolean_expression / (numeric_value spaces? boolean_operator spaces? numeric_value) / ("(" boolean_expression ")") / function_call end # ...
Saturday, February 9, 13
# pharmmd_dsl.treetop (cont.) rule boolean_operator ">=" / "<=" / ">" / "<" / "==" / "!=" end
rule function_call function_name:([a-zA-Z_] [a-zA-Z_0-9]*) arguments:("(" argument ("," spaces? argument)* ")")? <FunctionNode> end
rule argument string / date / numeric_value end
Saturday, February 9, 13
# pharmmd_dsl.treetop (cont.)
rule numeric_value function_call / number / "(" spaces? numeric_value spaces? ")" end
rule number float / integer end
rule integer "-"? digits end
rule float "-"? (digits)? "." digits end
rule digits [0-9]+ end
Saturday, February 9, 13
# pharmmd_dsl.treetop (cont.)
rule spaces [\s\n]+ end
rule string ['"] [^'"]* ['"] end
rule date [0-9]+ "." time_unit "s"? ".ago" end
rule time_unit "day" / "month" / "year" end
end
Saturday, February 9, 13
Por cierto, el lenguaje de gramáticas de treetop es un DSL “externo”
Saturday, February 9, 13
Treetop hace el parsing extremadamente natural.
¡Sigamos el proceso a mano!
(si es que tenemos pizarra a mano)
Saturday, February 9, 13
age >= 18 and (drug_cat(“XYZ”) or drug_cat(“ABC”))and dosage(“PARACETAMOL”) > 1000
Saturday, February 9, 13
¿Pero qué ganamos?
Saturday, February 9, 13
age >= 18 and patient.destroy # invalidoPatient.destroy_all # invalidosystem(“rm -‐rf /”) # valido!
Saturday, February 9, 13
eval(“Patient.destroy_all”) # oops
Saturday, February 9, 13
¡Rara vez la validación sintáctica es suficiente!
“Saltarina casa llovió perros perrunos” (¡Español sintácticamente válido!)
Saturday, February 9, 13
rule function_call function_name:([a-zA-Z_] [a-zA-Z_0-9]*) arguments:("(" argument ("," spaces? argument)* ")")? <FunctionNode> end
Saturday, February 9, 13
require 'treetop'; require 'pharmmd_dsl'
class FunctionNode < Treetop::Runtime::SyntaxNode; end
class PharmmdDslValidator attr_accessor :dsl, :errors
def initialize(dsl) @dsl = dsl; @errors = [] end
def valid_dsl? parser = PharmmdDslParser.new parse_tree = parser.parse(@dsl) if parse_tree.nil? errors << "You have a syntax error: #{parser.failure_reason}" else validate_functions(parse_tree) end errors.empty? end
def valid_functions @valid_functions ||= (PatientDSL.instance_methods - Object.instance_methods) end
Saturday, February 9, 13
# (cont.)
def validate_functions(parse_tree) element = parse_tree if element.is_a? FunctionNode name = element.function_name.text_value unless valid_functions.include? name errors << ("Function name #{element.text_value} is not a valid function call") end end if element.elements parse_tree.elements.each do |element| validate_functions(element) end end endend
Saturday, February 9, 13
age >= 18 and patient.destroy # invalidoPatient.destroy_all # invalidosystem(“rm -‐rf /”) # invalido!
Saturday, February 9, 13
Errores amigables:
“Se esperaba ‘(‘ en linea X, columna Y”
“La función ‘system’ no es válida”
Saturday, February 9, 13
Y mucho más: ¡Solucion v3!(Sólo una mirada rápida, que se nos acaba el tiempo)
Saturday, February 9, 13
¡DSL híbrido!
Saturday, February 9, 13
(quantity("lipitor") > 10).or(drug("vicodin"))
quantity("lipitor") > 10 or drug("vicodin")
(PharmdDSLValidator)
(PharmdDSLPreProcessor)
(PharmdDSL)
(NumericExpr(20, ["Lipitor 50mg"]) > 10).or( BooleanExpr(true, ["Vicodin 20mg"]))
quantity("lipitor") > 10 or drug("vicodin")
Exte
rnal
D
SLIn
tern
al
DSL
tre
eto
p,
pars
e t
ree
sru
by o
bjs
/me
tod
os
Saturday, February 9, 13
(quantity("lipitor") > 10).or(drug("vicodin"))
quantity("lipitor") > 10 or drug("vicodin")
(PharmdDSLValidator)
(PharmdDSLPreProcessor)
(DenominatorQuery)
(OrCondition( Condition('lipitor', {'$gt' => '10'}), Condition('vicodin', {'$exists' => true}))
quantity("lipitor") > 10 or drug("vicodin")
Exte
rnal
D
SLIn
tern
al
DSL
tre
eto
p,
pars
e t
ree
sru
by o
bjs
/me
tod
os
Saturday, February 9, 13
MongoDB no tenía OR en esa época, por lo que optimizabamos el árbol de expresiones para
dejar los ORs lo mas “arriba” posible.
Ejemplo:
((X or Y) and (Z)
((X and Z) or (Y and Z))
...y mas
(Condition#optimize)
Saturday, February 9, 13
Conclusión
• Parsing, árboles de expresiones, compiladores, etc no fue tiempo perdido en la U :)
• Pero siempre hay tradeoffs
• Probar un DSL interno primero. La solución más simple que podría funcionar
• Luego un DSL externo, treetop lo hace fácil
• Finalmente un híbrido, si no queda otraSaturday, February 9, 13
top related