una historia de ds ls en ruby

Post on 20-Oct-2014

380 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

Casos de uso y tips de implementacion de DSLs en Ruby Presentacion hecha en el meetup lenguajes dinamicos, Enero 2013

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